All interfaces not being used despite multiple sockets - python

Despite binding two UDP sockets to 2 different addresses on different interfaces of a host, traffic flows through a single interface.
The topology of the network is as follows:
2 links between h2 and s1
1 link between h1 & s1
________
| |
h2 s1_______h1
|________|
I'm emulating 2 hosts and one switch on mininet. h1 is running a UDP server at 10.0.0.1:4243. The other host has 2 interfaces, with ips 10.0.0.2 & 10.0.0.4 . I'm making 2 sockets on h2, binding one to (10.0.0.2,9999) & the other socket to (10.0.0.4,8888). I'm running the code below, which should send packets on both interfaces alternately.
Instead, the first packets are sent on both the interfaces. All subsequent packets are sent over a single interface.
Client Code (Running on h2)
def client():
sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock1.bind(("10.0.0.2",9999))
sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock2.bind(("10.0.0.4",8888))
while True:
text = 'The time is {}'.format(datetime.now())
data = text.encode('ascii')
sock1.sendto(data, ('10.0.0.1', 4243))
data, address = sock1.recvfrom(MAX_BYTES)
sock2.sendto(data, ('10.0.0.1', 4243))
data, address = sock2.recvfrom(MAX_BYTES)
Server Code (Running on h1)
def server():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('10.0.0.1', 4243))
print('Listening at {}'.format(sock.getsockname()))
while True:
data, address = sock.recvfrom(MAX_BYTES)
text = data.decode('ascii')
print('The client at {} says {!r}'.format(address, text))
text = 'Your data was {} bytes long'.format(len(data))
data = text.encode('ascii')
sock.sendto(data, address)
One packet is sent over both interfaces of h2. Subsequent packets are all sent and received only via interface with IP 10.0.0.2. On wiresharking, half the packets have src/dest IPs set to 10.0.0.4

From your comments, here is what I think is happening:
You have conflicting default routes
You have ip_forwarding enabled allowing internal routing
The bind call is essentially being negated due to #1 and #2
Conflicting default routes
From your comments, both interfaces probably have a /8. This means for h2 the kernel adds the same route for both something like:
10.0.0.0/8 dev h2-eth0 proto kernel scope link src 10.0.0.2
10.0.0.0/8 dev h2-eth1 proto kernel scope link src 10.0.0.4
This means the kernel wants to pick only one of these to send all 10.0.0.0/8 traffic to/from. You are trying to get around this with a bind call, but I don't believe Bind will overwrite the routeing table behavior.
ip_forwarding
If this flag is set, it also complicates matters because it may be allowing eth1 traffic via eth0.
bind
So bringing this all together, you bind your sockets to the individual IPs which are associated with individual interfaces, but the kernel only wants to route traffic destined for 10.0.0.1 out one interface and it chooses eth0. I think normally this would prevent eth1 traffic, but due to the routes and the ip_forward flag, eth1 traffic is routed through eth0 (after an initial ARP packet, the one packet you see).
Solution
Give h1's interface two IP addresses 10.1.0.1/16 and 10.0.0.1/16. Then give h2-eth0 ip address 10.0.0.2/16 and h2-eth1 ip address 10.1.0.2/16. Then have your program send data to 10.1.0.1 from 10.1.0.2, and 10.0.0.1 from 10.0.0.2.
By isolating the subnets you will prevent the root cause of the confusion, which was the conflicting routes.

Related

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?
Summary
In a single Python process, I create three receive sockets (sockets S1, S2, and S3).
Each socket joins the same multicast group (same multicast address, same port) using 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.
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):
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. 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.
The expected behavior is that only socket S1 reports a received packet.
Here is some output from my script, along with some output from tcpdump, to illustrate the problem (<<< are manually added annotations):
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].
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:
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).
The network namespaces are connected to each other using veth interface pairs (veth-x-y).
Each Python process periodically sends a UDP multicast packet on each of its directly connected interfaces.
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).
Update:
I noticed the following comment in function ospf_rx_hook in file packet.c in 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);
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.
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?
Some options:
Use IP_PKTINFO (http://man7.org/linux/man-pages/man7/ip.7.html and comments in Python socket bind 0.0.0.0, inspect incoming message). It appears that this may not be supported on all operating systems and/or in Python.
Check whether the source address is on the same subnet as the interface. This only works if the packets are sent over one hop which is indeed the case in my scenario. But does it work for IPv6 (yes, I think so) and on unnumbered interfaces (clearly not, but I wont support those)?
Some interesting observations on 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 has a macro INIT_MREQ4 to initialize the request for 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)
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 (i.e. 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.
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.
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.
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
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,
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:
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.
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:
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

Why include host in python socket request as well?

I see examples where people create the socket object from a host and port but then also send a GET request, and include the Host header inside the http request.
import socket
HOST = 'daring.cwi.nl'
PORT = 80
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall("GET / HTTP/1.1\r\n\r\nHost: daring.cwi.nl")
data = s.recv(1024)
print('Received', repr(data))
Why do we need to provide the host once to create socket object and again to send the request?
First, if you are using just a normal tcp connection, you can send anything you want, you can s.sendall("HAHAHA").
Second, In your code, it is simulating a Get http request. To send parameters, you just need to concatenate them after url. /?parameter1=value1&parameter2=value2.
Finally, why we need to include Host again? Assume you are sending this request to a load-balance server, the application server cannot get real Host without you explicitly sending it.
The Host header is required by the HTTP standard (at least version 1.1), i.e. the standard which defines the protocol spoken with a web server. The reason for this is that you can have multiple domains share the same IP address and thus the web server somehow needs to find out which of the domains you want to access. Since the lower layer (i.e. TCP) does not contain this information but only the IP address you need to provide this information at the application layer (i.e. HTTP).
As for using the hostname inside the connect: This is actually not needed and you could also provide the IP address there. All what it does when providing the hostname and not the IP is to lookup the IP address for the host and connect to this.

UDP Hole Punching fails on non-symmetric NATs

For a little while now, I got a serious issue on my UDP hole punching system which may involve things that are out of my reach now. I'll try to explain as precisely as possible because this is really bothering me :
UDP Hole punching method involves a third host S, which basically is public (not behind any kind of NAT or persistent firewall), that will be used by two clients A and B to get to know each other. Client A sends a udp packet to S, such that S can know its public IP and Port (which were mapped by A's NAT). Client B does the same. Then S matches A and B by sending to A B's public address, and A's public address to B. Therefore, they each know each other's address and can proceed to communicate.
So far so good, but here is the catch : it works only on special cases.
I am aware of the problem caused by Symmetric NATs (NATs that map your private IP and Port to a specific different address for each destination address you want to reach, so let's say I send a packet to S1 and S2, S1 will see a public address with port 50263 whereas S2 will see 50264). I am also aware that some Non-Symmetric NAT require you to send a UDP packet to a specific address before allowing to receive one from the same address.
So what I've understood so far is that as long as your NAT is not Symmetric, when opening a new socket, and send whatever byte array, your NAT will allocate a public IP and port to it. The public server tells your peer this public address such that it can communicate with you. You then have to send a packet to that peer in order to allow him to reply.
But it actually doesn't work everytime. I've tried at my university appartment, and managed to connect to myself using my public IP address, even by being behind a NAT. I also managed to send a UDP packet to a friend's, but it worked only once in the hundred of tests I made. I am now trying at home and it doesn't work at all.
I've setup 2 public VPS on the internet, and sent one packet to each one of them to see if my NAT was Symmetric, but no, the port that gets displayed on both of them is the same. I just can reach myself only from those public servers. Using Wireshark, I observe the packets leaving my computer but it seems that they get dropped beofre going back.
I've written simple python scripts to try out sending simply to myself :
Server
import socket
import sys
from helper import *
# This script is used to allow a client to know which external address it has.
def main():
# Port used to receive client requests
port = 6666
# We create a UDP socket on this port
sockfd = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
sockfd.bind (("", port))
print ("Address discoverer started on port %d" % port)
# Server loop
while True:
# We wait for any client to request for his address.
data, addr = sockfd.recvfrom(1)
# Once a client has sent a request, we send him back his external address, which we obtained using the packet he sent us.
sockfd.sendto (addr2bytes(addr), addr)
print ("Client %s:%d requested his address" % addr)
if __name__ == "__main__":
main()
Client
import sys
import socket
from helper import *
def main():
master = ("xxx.xxx.xxx.xxx", 6666)
me = ("yyy.yyy.yyy.yyy", 6666)
sockfd = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
sockfd.bind(('',6666))
sockfd.sendto (bytearray([0]), master)
data, addr = sockfd.recvfrom (6)
a = bytes2addr(data)
print ('My address is %s:%d' % a)
for i in range (0, 10):
sockfd.sendto ('hi myself'.encode('utf-8'), me)
data, addr = sockfd.recvfrom (1024)
print ("Received : " + data.decode('utf-8'))
if __name__ == "__main__":
main()
Executed on one of my public VPS, it works. Executed at home, I don't receive anything. It seems that sending from a public host grants all accesses, but behind a NAT is completely different.
Am I missing something fundamental ? I think I did pretty much every possible test but no success.
Depending on what you want to achieve, you may want to use IGD (spec) to retrieve your public IP and/or configure port forwarding automatically. Most home routers support UPnP IGD, from what I can tell.
Here's an example script that uses miniupnpc, a minimal UPnP IGD client, and the ipaddress and socket modules from python's standard library:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
"""
IGDTestScript.py
================
Description: UPnP IGD example script
Author: shaggyrogers
Creation Date: 2018-03-28
License: None / Public domain
"""
import ipaddress
import socket
import sys
import miniupnpc
def main(args):
# Get IP address for default interface
allhosts = ipaddress.ip_address(socket.INADDR_ALLHOSTS_GROUP)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect((allhosts.compressed, 0))
localIP = ipaddress.ip_address(sock.getsockname()[0])
if localIP.is_global:
print(f'Public IP: {localIP} (no NAT)')
return 0
print(f'Local IP: {localIP}')
# Discover and connect to IGD device
igd = miniupnpc.UPnP()
assert igd.discover() > 0
assert igd.selectigd() != ''
# Get public IP
publicIP = ipaddress.ip_address(igd.externalipaddress())
assert publicIP.is_global
print(f'Public IP: {publicIP}')
# Dump port mappings
i, mapping = 0, igd.getgenericportmapping(0)
while mapping:
print(f'Port mapping {i}: {mapping}')
i += 1
mapping = igd.getgenericportmapping(i)
# Create/update port mapping
externalPort, internalPort, protocol = 1234, 4321, 'TCP'
assert igd.addportmapping(externalPort, protocol, localIP.compressed,
internalPort, 'test mapping', '')
print((f'Added port mapping: {protocol} {publicIP}:{externalPort}'
f' --> {localIP.compressed}:{internalPort}'))
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
Sample output:
Local IP: 192.168.1.123
Public IP: 203.0.113.1
Port mapping 0: (8908, 'TCP', ('192.168.1.123', 9100), 'tcp_map', '1', '', 0)
Port mapping 1: (22222, 'UDP', ('192.168.1.123', 22222), 'udp_map', '1', '', 0)
Added port mapping: TCP 203.0.113.1:1234 -> 192.168.1.123:4321

Can't perform TCP-handshake through a NAT between two NICs with SO_BINDTODEVICE

I'm trying to connect my computer to both sides of a NAT (run by OpenWRT) and to establish a TCP connection through the NAT:
I run a DHCP server on my first NIC (eth0, ip address 129.104.0.1) and connect it to the WAN port of the router (ip address 129.104.0.198)
I connect my wifi (wlan0, ip address 192.168.1.119) to the router's SSID behind the NAT
I'm using python and the SO_BINDTODEVICE option to send packet between a server (on eth0) and a client (on wlan0) through the NAT:
For the server:
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((str(self.local_ip_addr),self.handler.port))
self.server.setsockopt(socket.SOL_SOCKET,25,self.iface.name+"\0")
self.server.listen(10)
while self.stopped() is False:
connect = self.server.accept()[0]
connect.settimeout(1)
connect.close()
self.server.close()
For the client:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, 25, self.iface.name + "\0")
sock.settimeout(1)
try:
sock.connect((self.dest,self.handler.port))
sock.close()
expect socket.timeout, socket.error as e:
return -1
My problem is that the connection times out before. I wiresharked both my interfaces and it seems the problem resides on the client's side:
wlan0 sends a TCP SYN packet to 129.104.0.1
The packet is correctly NATed by the router and is received from 129.104.0.198 by eth0
eth0 replies with a SYN,ACK packet, which is correctly NATed back to wlan0
wlan0 does not understand this SYN,ACK and tries to retransmit the first SYN packet
I'm thinking it might have something to do with the linux-kernel refusing to receive a packet from an address that belongs to the machine but if anyone has a clue it would be of great help!
EDIT: I narrowed it down: it is indeed a kernel issue, the packets sent from eth0 are perceived as "martians" by the kernel because they have a local ip address as source. Setting net.ipv4.conf.all.accept_local=1 did not help, neither did deactivating net.ipv4.conf.all.rp_filter=0.
After browsing the kernel sources and adding a lot of KERNEL_WARNING we found where it came from: the linux kernel is configured on certain mainstream distributions (Ubuntu...) to act as a router and drop packets where the source address is suspect in order to prevent spoofing (search "rp_filter" on https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt and RFC3704).
To allow such traffic you have to set some variables on your machine (as root):
sysctl -w net.ipv4.conf.all.accept_local=1
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.your_nic.rp_filter=0
where your_nic is the network interface receiving the packet. Beware to change both net.ipv4.conf.all.rp_filter and net.ipv4.conf.your_nic.rp_filter, it will not work otherwise (the kernel defaults to the most restrictive setting).

Choosing multicast network interface in Python

I have a server with two separate Ethernet connections. When I bind a socket in python it defaults to one of the two networks. How do I pull a multicast stream from the second network in Python? I have tried calling bind using the server's IP address on the second network, but that hasn't worked.
I recommend you don't use INADDR_ANY. In production multicast environments you want to be very specific with your multicast sockets and don't want to be doing things like sending igmp joins out all interfaces. This leads to hack-job workarounds when things aren't working like "route add -host 239.1.1.1 dev eth3" to get multicast joins going correctly depending on the system in question. Use this instead:
def joinMcast(mcast_addr,port,if_ip):
"""
Returns a live multicast socket
mcast_addr is a dotted string format of the multicast group
port is an integer of the UDP port you want to receive
if_ip is a dotted string format of the interface you will use
"""
#create a UDP socket
mcastsock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#allow other sockets to bind this port too
mcastsock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
#explicitly join the multicast group on the interface specified
mcastsock.setsockopt(socket.SOL_IP,socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(mcast_addr)+socket.inet_aton(if_ip))
#finally bind the socket to start getting data into your socket
mcastsock.bind((mcast_addr,port))
return mcastsock
In the mcastsock.bind you can also use '' instead of the address string, but I advise against this. With '', if you have another socket using the same port, both sockets will get eachothers data.
When binding your socket, try the values mentioned here:
For IPv4 addresses, two special forms
are accepted instead of a host
address: the empty string represents
INADDR_ANY, and the string
'' represents
INADDR_BROADCAST.
INADDR_ANY is also known as the wildcard address:
Sockets with wildcarded local addresses may receive messages directed to the specified port number and addressed to any of the possible addresses assigned to a host`
More here.
I figured it out. It turns out that the piece I was missing was adding the interface to the mreq structure that is used in adding membership to a multicast group.
For IPv4, the index of the network interface is the IP address; for IPv6 the index of the network interface is returned by the method socket.getaddrinfo.
The code below shows how to listen to multicast on all network interfaces:
from socket import AF_INET6, AF_INET
import socket
import struct
# Bugfix for Python 3.6 for Windows ... missing IPPROTO_IPV6 constant
if not hasattr(socket, 'IPPROTO_IPV6'):
socket.IPPROTO_IPV6 = 41
multicast_address = {
AF_INET: ["224.0.1.187"],
AF_INET6: ["FF00::FD"]
}
multicast_port = 5683
addr_info = socket.getaddrinfo('', None) # get all ip
for addr in addr_info:
family = addr[0]
local_address = addr[4][0]
sock = socket.socket(family, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((local_address, multicast_port))
if family == AF_INET:
for multicast_group in multicast_address[family]:
sock.setsockopt(
socket.IPPROTO_IP,
socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(multicast_group) + socket.inet_aton(local_address)
)
elif family == AF_INET6:
for multicast_group in multicast_address[family]:
ipv6mr_interface = struct.pack('i', addr[4][3])
ipv6_mreq = socket.inet_pton(socket.AF_INET6, multicast_group) + ipv6mr_interface
sock.setsockopt(
socket.IPPROTO_IPV6,
socket.IPV6_JOIN_GROUP,
ipv6_mreq
)
# _transport, _protocol = await loop.create_datagram_endpoint(
# lambda: protocol_factory(), sock=sock)

Categories

Resources