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

时间:2019-01-07 16:58:14

标签: python linux sockets networking multicast

问题

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

摘要

在一个Python进程中,我创建了三个接收套接字(套接字S1,S2和S3)。

每个套接字都使用setockopt IP_ADD_MEMBERSHIP加入相同的多播组(相同的多播地址,相同的端口)。

但是,每个套接字通过将struct ip_mreq(请参见http://man7.org/linux/man-pages/man7/ip.7.html)中的imr_address设置为特定本地接口的地址,从而在不同的本地接口上加入该多播组:套接字S1在接口I1上加入,套接字S2在接口上加入I2,并且套接字S3加入接口I3。

以下是用于创建接收套接字的代码:

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

本地接口的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']

在发送套接字一侧,我使用setsockopt IP_MULTICAST_IF选择要在其上发送传出多播数据包的传出本地接口。效果很好。

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

当Python进程在(例如)接口I1上收到单个单独的多播数据包时,所有三个套接字(S1,S2和S3)都报告收到的UDP数据包。

预期的行为是只有套接字S1报告收到的数据包。

以下是我的脚本的一些输出以及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]

对套接字发送[1]的单次调用导致在电线[2]上发送单个数据包,该数据包在电线上被接收一次[3],但导致三个套接字唤醒并报告接收到的数据包[4abc] ]。

预期的行为是,只有一个接收套接字会唤醒并报告接收到的数据包,即,一个在接口上加入多播组的套接字

拓扑详细信息

我在以下网络拓扑中连接了三个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

每个Python脚本都在网络名称空间(netns-1,netns-2,netns-3)中运行。

网络名称空间使用veth接口对(veth-x-y)相互连接。

每个Python进程都会在其直接连接的每个接口上定期发送UDP多播数据包。

任何接口上的任何Python进程发送的所有UDP数据包始终使用相同的目标多播地址(224.0.0.120)和相同的目标端口(911)。

更新

我注意到BIRD(https://github.com/BIRD/bird)文件ospf_rx_hook中函数packet.c的以下注释:

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);

很显然,BIRD存在相同的问题(至少在BSD上),并通过检查接收到的数据包到达哪个接口来解决该问题,并在它不是预期的接口时忽略它。

更新

如果BIRD注释为真,那么问题就变成了:如何确定接收到的UDP数据包在哪个接口上接收?

一些选项:

关于BIRD的一些有趣观察:

  • BIRD将原始套接字用于OSPF(因为OSPF直接通过IP运行,而不是通过UDP运行),并且在函数sk_setup(文件io.c)中,它调用sk_request_cmsg4_pktinfo,该函数在Linux上设置IP_PKTINFO套接字选项,在BSD上设置IP_RCVIF套接字选项。

  • BIRD具有一个宏INIT_MREQ4来初始化对IP_ADD_MEMBERSHIP的请求。有趣的是,对于Linux,它设置多播地址(imr_multiaddr)和本地接口 index (imr_ifindex),而对于BSD,它设置多播地址(imr_multiaddr)和本地接口 address (imr_interface)

1 个答案:

答案 0 :(得分:0)

我通过记录最终使用的变通方法来“回答”自己的问题。

请注意,这只是解决方法。在这种解决方法中,我们检测到“错误”套接字上接收到的数据包,并忽略它们。如果某人有一个“真实的”解决方案(即首先避免在错误的套接字上接收到数据包的解决方案),我很想听听它。

假设我们创建接收套接字时加入了一个多播组,如下所示:

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

在这里,MULTICAST_ADDR是多播地址,而local_address是加入该组的本地接口的IP地址。

显然,这实际上意味着该接口上接收到的IP多播数据包将被接受并传递到更高的层,在这种情况下为UDP。

但是请注意,如果存在多个UDP套接字S1,S2,S3,...,它们在不同的接口I1,I2,I3,...上加入了相同的多播组(相同的多播地址和相同的端口)。然后,如果我们在接口I1,I2,I3的任何一个上接收到多播数据包,则将通知所有套接字S1,S2,S3 ...都已收到数据包。

我的解决方法如下:

(1)对于我们加入多播组的每个套接字,请跟踪我们进行加入的接口的if_index,并且

interface_index = socket.if_nametoindex(interface_name)

(2)使用IP_PKTINFO套接字选项来确定实际在哪个接口索引上接收到数据包,

要设置选项:

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

要在收到数据包时从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)如果套接字的if_index与接收数据包的if_index不匹配,则忽略接收的数据包。

一个复杂的因素是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])

有关此代码源的完整工作示例以及有关版权和2条款BSD许可证的重要信息,请参见文件https://github.com/brunorijsman/python-multicast-experiments/blob/master/beacon.py