简体   繁体   English

如何使setsockopt IP_ADD_MEMBERSHIP接受本地接口地址以仅在一个特定接口上接收多播数据包?

[英]How to make setsockopt IP_ADD_MEMBERSHIP honor the local interface address to receive multicast packets on only one specific interface?

Question

How to make setsockopt IP_ADD_MEMBERSHIP honor the local interface address to receive multicast packets on only one specific interface? 如何使setsockopt IP_ADD_MEMBERSHIP接受本地接口地址以仅在一个特定接口上接收多播数据包?

Summary 摘要

In a single Python process, I create three receive sockets (sockets S1, S2, and S3). 在一个Python过程中,我创建了三个接收套接字(套接字S1,S2和S3)。

Each socket joins the same multicast group (same multicast address, same port) using setsockopt IP_ADD_MEMBERSHIP. 每个套接字使用setsockopt IP_ADD_MEMBERSHIP加入相同的多播组(相同的多播地址,相同的端口)。

However, each socket joins that multicast group on a different local interface by setting imr_address in struct ip_mreq (see http://man7.org/linux/man-pages/man7/ip.7.html ) to the address of particular local interface: socket S1 joins on interface I1, socket S2 joins on interface I2, and socket S3 joins on interface I3. 但是,每个套接字通过将struct ip_mreq(请参见http://man7.org/linux/man-pages/man7/ip.7.html )中的imr_address设置为特定本地接口的地址,从而在不同的本地接口上加入该多播组。 :套接字S1在接口I1上联接,套接字S2在接口I2上联接,套接字S3在接口I3上联接。

Here is the code for creating a receive socket: 这是用于创建接收套接字的代码:

def create_rx_socket(interface_name):
    local_address = interface_ipv4_address(interface_name)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except AttributeError:
        pass
    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    except AttributeError:
        pass
    sock.bind((MULTICAST_ADDR, MULTICAST_PORT))
    report("join group {} on {} for local address {}".format(MULTICAST_ADDR, interface_name,
                                                             local_address))
    req = struct.pack("=4s4s", socket.inet_aton(MULTICAST_ADDR), socket.inet_aton(local_address))
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req)
    return sock

where the IP address of the local interface is determined as follows (I used debug prints to verify it obtains the correct address): 本地接口的IP地址是按以下方式确定的(我使用调试打印来验证其是否获得了正确的地址):

def interface_ipv4_address(interface_name):
    interface_addresses = netifaces.interfaces()
    if not interface_name in netifaces.interfaces():
        fatal_error("Interface " + interface_name + " not present.")
    interface_addresses = netifaces.ifaddresses(interface_name)
    if not netifaces.AF_INET in interface_addresses:
        fatal_error("Interface " + interface_name + " has no IPv4 address.")
    return interface_addresses[netifaces.AF_INET][0]['addr']

On the sending socket side, I use setsockopt IP_MULTICAST_IF to choose the outgoing local interface on which outgoing multicast packets are to be sent. 在发送套接字端,我使用setsockopt IP_MULTICAST_IF选择要在其上发送传出多播数据包的传出本地接口。 This works fine. 这很好。

def create_tx_socket(interface_name):
    local_address = interface_ipv4_address(interface_name)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except AttributeError:
        pass
    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    except AttributeError:
        pass
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(local_address))
    # Disable the loopback of sent multicast packets to listening sockets on the same host. We don't
    # want this to happen because each beacon is listening on the same port and the same multicast
    # address on potentially multiple interfaces. Each receive socket should only receive packets
    # that were sent by the host on the other side of the interface, and not packet that were sent
    # from the same host on a different interface. IP_MULTICAST_IF is enabled by default, so we have
    # to explicitly disable it)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0)
    sock.bind((local_address, MULTICAST_PORT))
    sock.connect((MULTICAST_ADDR, MULTICAST_PORT))
    return sock

When the Python process receives a single individual multicast packet on (say) interface I1, all three socket (S1, S2, and S3) report a received UDP packet. 当Python进程在(例如)接口I1上接收到单个单独的多播数据包时,所有三个套接字(S1,S2和S3)都报告收到的UDP数据包。

The expected behavior is that only socket S1 reports a received packet. 预期的行为是只有套接字S1报告收到的数据包。

Here is some output from my script, along with some output from tcpdump, to illustrate the problem (<<< are manually added annotations): 这是我的脚本的一些输出,以及tcpdump的一些输出,以说明问题(<<<是手动添加的注释):

beacon3: send beacon3-message-1-to-veth-3-1a on veth-3-1a from ('99.1.3.3', 911) to ('224.0.0.120', 911)     <<< [1]
TX veth-3-1a: 15:49:13.355519 IP 99.1.3.3.911 > 224.0.0.120.911: UDP, length 30   <<< [2]
RX veth-1-3a: 15:49:13.355558 IP 99.1.3.3.911 > 224.0.0.120.911: UDP, length 30   <<< [3]
beacon1: received beacon3-message-1-to-veth-3-1a on veth-1-2 from     99.1.3.3:911   <<< [4a]
beacon1: received beacon3-message-1-to-veth-3-1a on veth-1-3a from 99.1.3.3:911   <<< [4b]
beacon1: received beacon3-message-1-to-veth-3-1a on veth-1-3b from 99.1.3.3:911   <<< [4c]

A single call to socket send [1] causes a single packet to be sent on the wire [2] which is received on the wire once [3] but which causes three sockets to wake up and report the received packet [4abc]. 一次对套接字发送[1]的调用会导致在电线[2]上发送单个数据包,该消息在电线[1]上一次被接收,但会导致三个套接字唤醒并报告接收到的数据包[4abc]。

The expected behavior is that only one of the receive sockets would wake up and report the received packet, namely the one socket that joined the multicast group on the interface 预期的行为是,只有一个接收套接字会唤醒并报告接收到的数据包,即在接口上加入多播组的一个套接字

Topology details 拓扑详细信息

I have three Python scripts (1, 2, and 3) that are connected in the following network topology: 我有以下三个网络拓扑中连接的三个Python脚本(1、2和3):

              veth-1-2      veth-2-1
          (1)------------------------(2)
          | |                         |
veth-1-3a | | veth-1-3b               | veth-2-3
          | |                         |
          | |                         | 
veth-3-1a | | veth-3-1b               |
          | |                         |
          (3)-------------------------+
              veth-3-2

Each Python script is running in a network namespace (netns-1, netns-2, netns-3). 每个Python脚本都在网络名称空间(netns-1,netns-2,netns-3)中运行。

The network namespaces are connected to each other using veth interface pairs (veth-xy). 网络名称空间使用veth接口对(veth-xy)相互连接。

Each Python process periodically sends a UDP multicast packet on each of its directly connected interfaces. 每个Python进程都会在其每个直接连接的接口上定期发送UDP多播数据包。

All UDP packets sent by any Python process on any interface always uses the same destination multicast address (224.0.0.120) and the same destination port (911). 任何Python进程在任何接口上发送的所有UDP数据包始终使用相同的目标多播地址(224.0.0.120)和相同的目标端口(911)。

Update: 更新:

I noticed the following comment in function ospf_rx_hook in file packet.c in BIRD ( https://github.com/BIRD/bird ): 我在BIRD( https://github.com/BIRD/bird )的文件packet.c中的函数ospf_rx_hook中注意到以下注释:

int
ospf_rx_hook(sock *sk, uint len)
{
  /* We want just packets from sk->iface. Unfortunately, on BSD we cannot filter
     out other packets at kernel level and we receive all packets on all sockets */
  if (sk->lifindex != sk->iface->index)
    return 1;

  DBG("OSPF: RX hook called (iface %s, src %I, dst %I)\n",
      sk->iface->name, sk->faddr, sk->laddr);

Apparently BIRD has the same issue (at least on BSD) and solves it by checking which interface a received packet arrived on, and ignoring it when it is not the expected interface. 显然,BIRD存在相同的问题(至少在BSD上),并通过检查接收的数据包到达哪个接口来解决该问题,并在它不是预期的接口时忽略它。

Update: 更新:

If the BIRD comment is true, then the question becomes: how can I determine which interface a packet was received on for a received UDP packet? 如果BIRD注释是正确的,那么问题就变成了:如何确定接收到的UDP数据包在哪个接口上接收?

Some options: 一些选项:

Some interesting observations on BIRD: 关于BIRD的一些有趣的观察:

  • BIRD uses raw sockets for OSPF (because OSPF runs directly over IP, not over UDP) and in function sk_setup (file io.c) it calls sk_request_cmsg4_pktinfo which on Linux set the IP_PKTINFO socket option and on BSD sets the IP_RCVIF socket option. BIRD将原始套接字用于OSPF(因为OSPF直接通过IP运行,而不是通过UDP运行),并且在函数sk_setup(文件io.c)中调用sk_request_cmsg4_pktinfo,在Linux上设置IP_PKTINFO套接字选项,在BSD上设置IP_RCVIF套接字选项。

  • BIRD has a macro INIT_MREQ4 to initialize the request for IP_ADD_MEMBERSHIP. BIRD具有一个宏INIT_MREQ4来初始化对IP_ADD_MEMBERSHIP的请求。 Interestingly, for Linux it sets the multicast address (imr_multiaddr) and the local interface index (imr_ifindex) whereas for BSD it sets the multicast address (imr_multiaddr) and the local interface address (imr_interface) 有趣的是,对于Linux,它设置多播地址(imr_multiaddr)和本地接口索引 (imr_ifindex),而对于BSD,它设置多播地址(imr_multiaddr)和本地接口地址 (imr_interface)

I am "answering" my own question by documenting the work-around that I ended up using. 我通过记录最终使用的变通方法来“回答”自己的问题。

Note that this is only a work-around. 请注意,这只是解决方法。 In this work-around we detect packets that are received on the "wrong" socket and ignore them. 在这种解决方法中,我们检测到“错误”套接字上接收到的数据包,并忽略它们。 If someone has a "real" solution (ie one that avoids the packets being received on the wrong socket in the first place), I would love to hear it. 如果有人有一个“真正的”解决方案(即首先避免从错误的套接字上接收到数据包的解决方案),我很想听听它。

Let's say that we join a multicast group when we create a receive socket as follows: 假设我们如下创建接收套接字时加入了一个多播组:

req = struct.pack("=4s4s", socket.inet_aton(MULTICAST_ADDR), socket.inet_aton(local_address))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req)

Here, MULTICAST_ADDR is the multicast address and local_address is the IP address of the local interface on which the group is joined. 此处,MULTICAST_ADDR是多播地址,而local_address是加入该组的本地接口的IP地址。

Apparently, what this really means is that IP multicast packets received on that interface will be accepted and passed on to the higher layer, which is UDP in this case. 显然,这实际上意味着在该接口上接收到的IP多播数据包将被接受并传递到更高的层,在本例中为UDP。

Note, however, that if there are multiple UDP sockets S1, S2, S3, ... that have joined the same multicast group (same multicast address and same port) on different interfaces I1, I2, I3, ... then if we receive a multicast packet on ANY of the interfaces I1, I2, I3... then ALL the sockets S1, S2, S3, ... will be notified that a packet has been received. 但是请注意,如果有多个UDP套接字S1,S2,S3,...在不同的接口I1,I2,I3,...上加入了相同的多播组(相同的多播地址和相同的端口),则如果在接口I1,I2,I3的任何一个上接收到多播数据包,则将通知所有套接字S1,S2,S3,...已收到数据包。

My work-around works as follows: 我的解决方法如下:

(1) For each socket on which we join the mulitcast group, keep track of the if_index for the interface on which we did the join, and (1)对于我们要加入多播组的每个套接字,请跟踪我们进行加入的接口的if_index,以及

interface_index = socket.if_nametoindex(interface_name)

(2) Use the IP_PKTINFO socket option to determine on which interface index the packet was actually received, (2)使用IP_PKTINFO套接字选项确定实际在哪个接口索引上接收到数据包,

To set the option: 设置选项:

sock.setsockopt(socket.SOL_IP, socket.IP_PKTINFO, 1)

To retrieve the if_index from the PKTINFO ancillary data when the packet is received: 要在收到数据包时从PKTINFO辅助数据中检索if_index,请执行以下操作:

rx_interface_index = None
for anc in ancillary_messages:
    if anc[0] == socket.SOL_IP and anc[1] == socket.IP_PKTINFO:
        packet_info = in_pktinfo.from_buffer_copy(anc[2])
        rx_interface_index = packet_info.ipi_ifindex

(3) Ignore the received packet if the if_index of the socket does not match the if_index on which the packet was received. (3)如果套接字的if_index与接收数据包的if_index不匹配,则忽略接收的数据包。

A complicating factor is that neither IP_PKTINFO nor in_pktinfo are part of the standard Python socket module, so both must be manually defined: 一个复杂的因素是IP_PKTINFO和in_pktinfo都不是标准Python套接字模块的一部分,因此必须手动定义它们:

SYMBOLS = {
    'IP_PKTINFO': 8,
    'IP_TRANSPARENT': 19,
    'SOL_IPV6': 41,
    'IPV6_RECVPKTINFO': 49,
    'IPV6_PKTINFO': 50
}

uint32_t = ctypes.c_uint32

in_addr_t = uint32_t

class in_addr(ctypes.Structure):
    _fields_ = [('s_addr', in_addr_t)]

class in6_addr_U(ctypes.Union):
    _fields_ = [
        ('__u6_addr8', ctypes.c_uint8 * 16),
        ('__u6_addr16', ctypes.c_uint16 * 8),
        ('__u6_addr32', ctypes.c_uint32 * 4),
    ]

class in6_addr(ctypes.Structure):
    _fields_ = [
        ('__in6_u', in6_addr_U),
    ]

class in_pktinfo(ctypes.Structure):
    _fields_ = [
        ('ipi_ifindex', ctypes.c_int),
        ('ipi_spec_dst', in_addr),
        ('ipi_addr', in_addr),
    ]

for symbol in SYMBOLS:
    if not hasattr(socket, symbol):
        setattr(socket, symbol, SYMBOLS[symbol])

For a complete working example and for important information about the copyright and 2-clause BSD-license for the source of this code see file https://github.com/brunorijsman/python-multicast-experiments/blob/master/beacon.py 有关此代码源的完整工作示例以及有关版权和2条款BSD许可证的重要信息,请参见文件https://github.com/brunorijsman/python-multicast-experiments/blob/master/beacon.py

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM