Search code examples
pythonsocketsudpmulticastigmp

Every sent UDP multicast message is received twice because of two IGMPv2 join messages. How to avoid?


I have a Python program that uses a sockets to send and receive UDP multicast messages on multicast IP address 224.0.1.1 and UDP port 20001.

On the receive the side, I create a single receive socket, and call socket.setsockopt once using socket option IP_ADD_MEMBERSHIP to join the IP multicast group.

However, Wireshark reports that the single call to setsockopt causes two separate join (IGMPv2 membership report) messages to be sent out:

  • One join message using Ethernet source address 01:00:52:00:01:01, which is the Ethernet multicast address corresponding to the IP multicast group.

  • One join message using Ethernet source address a8:66:7f:3a:2b:1a, which is the Ethernet unicast address corresponding to the physical "en0" interface over which the join message was sent.

On the send side, I create a single send socket, an call socket.connect to associate the socket with multicast IP address 224.0.1.1 and UDP port 20001.

I then call socket.send once to send a single test message. Because of the two separate join messages, the sent test message appears TWICE on the wire, once with destination Ethernet address 01:00:52:00:01:01 and once with destination Ethernet address a8:66:7f:3a:2b:1a.

On the receiving side, both messages are received separately. Thus each sent message is received TWICE.

The question is: how can I prevent this from happening?

A minimal example that reproduces the behavior is as follows:

import socket
import struct
import time

mcast_ipv4_address = "224.0.1.1"
port = 20001
group = (mcast_ipv4_address, port)

txsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
txsock.connect(group) 

rxsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
req = struct.pack("=4sl", socket.inet_aton(mcast_ipv4_address), socket.INADDR_ANY)
rxsock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req)
rxsock.bind(group)

time.sleep(1.0)

print("Sending single message...")
msg = b"test"
txsock.send(msg)
print("Sent {}".format(msg))

print("Receiving first message...")
msg = rxsock.recv(65535)
print("Received {}".format(msg))

print("Receiving second message...")
msg = rxsock.recv(65535)
print("Received {}".format(msg))

time.sleep(0.1)

Additional details:

1) The operating system is macOS High Sierra 10.13.5

2) The Python version is 3.5.1

3) The first sleep is essential; without it the problem does not occur because it takes some time for the join messages to be sent

4) The second sleep is not essential; it is there to make sure both test messages are seen by wireshark before the program terminates and the leave messages are sent.

5) I tried using the real IP address of the outgoing interface in stead of INADDR_ANY in the req structure, but it does not make any difference.


Solution

  • I found the answer to my own question:

    If you disable the IP_MULTICAST_LOOP option on the sending socket, then:

    1) Wireshark will STILL report two IGMPv2 join messages, same as before

    2) Wireshark will STILL report two UDP multicast messages, same as before

    3) However, the receiving socket will only receive a single UDP multicast message (the example program will block on "Receiving second message...")

    Here is the updated code for macOS:

    txsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 
    socket.IPPROTO_UDP)
    txsock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0)  # <<< FIX
    txsock.connect(group)
    

    Annoyingly, the behavior on Linux is the opposite:

    • If you leave IP_MULTICAST_LOOP to its default value of enabled, as in the original example program, you will receive exactly one copy of the sent packet.

    • If you disable IP_MULTICAST_LOOP, as in the "fixed" example program, you will not receive any copy of the sent packet (at least not on AWS).

    After further investigation, I found that the behavior does not depend on the platform on which the code runs (macOS vs Linux) but on the router to which the platform is connected.