Как заставить setsockopt IP_ADD_MEMBERSHIP учитывать адрес локального интерфейса для получения многоадресных пакетов только на одном конкретном интерфейсе? - PullRequest
0 голосов
/ 07 января 2019

Вопрос

Как заставить setsockopt IP_ADD_MEMBERSHIP учитывать адрес локального интерфейса для получения многоадресных пакетов только на одном конкретном интерфейсе?

Основная информация

В одном процессе Python я создаю три принимающих сокета (сокеты S1, S2 и S3).

Каждый сокет присоединяется к одной и той же группе многоадресной рассылки (тот же адрес многоадресной рассылки, тот же порт) с использованием setsockopt IP_ADD_MEMBERSHIP.

Однако каждый сокет присоединяется к этой многоадресной группе на другом локальном интерфейсе, устанавливая imr_address в struct ip_mreq (см. http://man7.org/linux/man-pages/man7/ip.7.html) для адреса конкретного локального интерфейса: сокет 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, чтобы проиллюстрировать проблему (<<< - аннотации, добавленные вручную): </p>

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 на каждый из его напрямую подключенных интерфейсов.

Все пакеты UDP, отправленные любым процессом Python на любом интерфейсе, всегда используют один и тот же адрес многоадресной рассылки (224.0.0.120) и один и тот же порт назначения (911).

Обновление:

Я заметил следующий комментарий в функции ospf_rx_hook в файле packet.c в BIRD (https://github.com/BIRD/bird):

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?

Некоторые опции:

  • Использовать IP_PKTINFO (http://man7.org/linux/man-pages/man7/ip.7.html и комментарии в Привязка сокета Python 0.0.0.0, проверить входящее сообщение ). Похоже, что это может поддерживаться не во всех операционных системах и / или в Python.

  • Проверьте, находится ли адрес источника в той же подсети, что и интерфейс. Это работает, только если пакеты отправляются за один переход, что действительно имеет место в моем сценарии. Но работает ли он для IPv6 (да, я так думаю) и для ненумерованных интерфейсов (явно нет, но я не буду их поддерживать)?

Некоторые интересные наблюдения о ПТИЦЕ:

  • 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 голосов
/ 08 января 2019

Я «отвечаю» на свой вопрос, документируя обходные пути, которые я использовал в итоге.

Обратите внимание, что это только обходной путь. В этом обходном пути мы обнаруживаем пакеты, полученные на «неправильном» сокете, и игнорируем их. Если у кого-то есть «реальное» решение (то есть такое, которое в первую очередь позволяет избежать приема пакетов в неправильный сокет), я бы хотел услышать его.

Допустим, мы присоединяемся к многоадресной группе, когда создаем сокет получения следующим образом:

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)

Чтобы извлечь if_index из вспомогательных данных PKTINFO при получении пакета:

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

Полный рабочий пример и важную информацию об авторском праве и BSD-лицензии с 2 пунктами для источника этого кода см. В файле https://github.com/brunorijsman/python-multicast-experiments/blob/master/beacon.py

...