Вопрос
Как заставить 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) )