pythonpython-3.xnetwork-programmingudpudpclient

Receive foreign UDP Broadcast with Python


I have a device in the network at 192.168.123.204 which broadcasts UDP datagrams(Artnet) as 2.168.123.204 to 2.255.255.255:6454. (The network address is 192.168.123.204 but the datagram is sent with 2.168.123.204 as source.) The address 2.255.255.255 can't be changed (no setting for that).

My Python script runs on the device 192.168.123.148. I can receive the datagrams there with wireshark: but a Python socket bound to 0.0.0.0:6454 can't receive them. Binding it to 2.168.123.204 or 2.255.255.255 does not work. The script is working, since I can receive Packets from 127.0.0.1.

If it can't be solved with Python can I redirect the UDP broadcast with iptables (linux)?

Network:
        Router 192.168.123.1
          /   \
Broadcaster: 192.168.123.204Script: 192.168.123.148

basic script:

import socket
import asyncio


HOST, PORT = 'localhost', 6454


class SyslogProtocol(asyncio.DatagramProtocol):
    def __init__(self):
        super().__init__()

    def connection_made(self, transport) -> "Used by asyncio":
        self.transport = transport

    def datagram_received(self, data, addr) -> "Main entrypoint for processing message":
        # Here is where you would push message to whatever methods/classes you want.
      
        print(data)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    t = loop.create_datagram_endpoint(SyslogProtocol, local_addr=('0.0.0.0', PORT))
    loop.run_until_complete(t) # Server starts listening
    loop.run_forever()

full script:

#import pygame
from ctypes import *
import socket
import asyncio
import os, random

class ArtNetPackage(LittleEndianStructure):
    PORT = 0x1936
    _fields_ = [("id", c_char * 8),
                ("opcode", c_ushort),
                ("protverh", c_ubyte),
                ("protver", c_ubyte),
                ("sequence", c_ubyte),
                ("physical", c_ubyte),         
                ("universe", c_ushort),
                ("lengthhi", c_ubyte),
                ("length", c_ubyte),
                ("payload", c_ubyte * 512)]
    
    def get_length(self):
        return self.lengthhi*256+self.length
        
    def __init__(self,data=b''):
        if len(data) == 0:
            self.id = b"Art-Net"
            self.opcode = 0x5000
            self.protver = 14
            self.universe = 0
            self.lengthhi = 2
        else:
            self.id = data[:8]
            self.opcode = data[8]+data[9]*256
            if self.opcode == 0x5000:
                self.protverh = data[10]
                self.protver = data[11]
                self.sequence = data[12]
                self.physical = data[13]
                self.universe = data[14]+data[15]*256
                self.lengthhi = data[16]
                self.length = data[17]
                self.payload = (c_ubyte * 512).from_buffer_copy(
                        data[18:530])#.ljust(512,b'\x00'))

#pygame.init()

HOST, PORT = 'localhost', 6454


class SyslogProtocol(asyncio.DatagramProtocol):
    def __init__(self):
        super().__init__()
        
    def connection_made(self, transport) -> "Used by asyncio":
        self.transport = transport

    def datagram_received(self, data, addr) -> "Main entrypoint for processing message":
        # Here is where you would push message to whatever methods/classes you want.
        try:
            dmx = ArtNetPackage(data)
            if not dmx.opcode == 0x5000:
                return
            print(dmx.payload[0])
        except:
            print("error")


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    t = loop.create_datagram_endpoint(SyslogProtocol, local_addr=('0.0.0.0', PORT))
    loop.run_until_complete(t) # Server starts listening
    loop.run_forever()

interfaces:

$ ip a
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
    inet 192.168.123.148/24 brd 192.168.123.255 scope global dynamic wlan0
       valid_lft 86393sec preferred_lft 86393sec
    inet6 XXXXXXXX/64 scope link 
       valid_lft forever preferred_lft forever

Wireshark Protocol


Solution

  • I misread your question, you are dealing with broadcast not multicast addresses. Your problem is that you do not have the SO_BROADCAST flag sent

    IPv4 addresses are divided into unicast, broadcast and multicast addresses. Unicast addresses specify a single interface of a host, broadcast addresses specify all hosts on a network and multicast addresses address all hosts in a multicast group. Datagrams to broadcast addresses can be only sent or received when the SO_BROADCAST socket flag is set. In the current implementation, connection-oriented sockets are only allowed to use unicast addresses.

    There is an explicit example here. The most important part being:

        def connection_made(self, transport):
            print('started')
            self.transport = transport
            sock = transport.get_extra_info("socket")
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.broadcast()
    
    

    You will also need to add an address on that subnet to the interface. This is very simple just enter:

    ip addr add 2.255.255.254/8 dev eth1
    

    This assumes that the broadcast is on 2.0.0.0/8, the interface name is eth1 and that 2.255.255.254 is not taken by another host.