使用 Python 接收外部 UDP 广播
Receive foreign UDP Broadcast with Python
我在 192.168.123.204
的网络中有一个设备,它将 UDP 数据报 (Artnet) 作为 2.168.123.204
广播到 2.255.255.255:6454
。 (网络地址是 192.168.123.204
但数据报以 2.168.123.204
作为源发送。)地址 2.255.255.255
无法更改(没有设置)。
我的 Python 脚本在设备 192.168.123.148
上运行。我可以使用 wireshark 在那里接收数据报:但是绑定到 0.0.0.0:6454
的 Python 套接字无法接收它们。将它绑定到 2.168.123.204
或 2.255.255.255
不起作用。该脚本正在运行,因为我可以从 127.0.0.1
.
接收数据包
如果用Python解决不了,我可以用iptables(linux)重定向UDP广播吗?
网络:
路由器192.168.123.1
/ \
主播:192.168.123.204 脚本:192.168.123.148
基本脚本:
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()
完整脚本:
#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()
接口:
$ 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
使用原始套接字。
import struct
SRC_IPv4 = slice(26, 30)
DST_IPv4 = slice(30, 34)
SRC_PORTv4 = slice(34, 36)
DST_PORTv4 = slice(36, 38)
sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(3))
while 1:
raw, (nic, ptype, *x) = sock.recvfrom(2048)
if ptype == 2048 and raw[23] == 17:
src_host = socket.inet_ntoa(raw[SRC_IPv4])
src_port, = struct.Struct("!H").unpack(raw[SRC_PORTv4])
dst_host = socket.inet_ntoa(raw[DST_IPv4])
dst_port, = struct.Struct("!H").unpack(raw[DST_PORTv4])
print(f'{src_host}:{src_port} => {dst_host}:{dst_port}')```
我误读了你的问题,你处理的是广播地址而不是多播地址。你的问题是你没有发送 SO_BROADCAST 标志
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.
有一个明确的例子here。最重要的部分是:
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()
您还需要将该子网上的地址添加到接口。这很简单,只需输入:
ip addr add 2.255.255.254/8 dev eth1
假设广播在 2.0.0.0/8 上,接口名称是 eth1
并且 2.255.255.254 没有被其他主机占用。
我在 192.168.123.204
的网络中有一个设备,它将 UDP 数据报 (Artnet) 作为 2.168.123.204
广播到 2.255.255.255:6454
。 (网络地址是 192.168.123.204
但数据报以 2.168.123.204
作为源发送。)地址 2.255.255.255
无法更改(没有设置)。
我的 Python 脚本在设备 192.168.123.148
上运行。我可以使用 wireshark 在那里接收数据报:但是绑定到 0.0.0.0:6454
的 Python 套接字无法接收它们。将它绑定到 2.168.123.204
或 2.255.255.255
不起作用。该脚本正在运行,因为我可以从 127.0.0.1
.
如果用Python解决不了,我可以用iptables(linux)重定向UDP广播吗?
网络:
路由器192.168.123.1
/ \
主播:192.168.123.204 脚本:192.168.123.148
基本脚本:
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()
完整脚本:
#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()
接口:
$ 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
使用原始套接字。
import struct
SRC_IPv4 = slice(26, 30)
DST_IPv4 = slice(30, 34)
SRC_PORTv4 = slice(34, 36)
DST_PORTv4 = slice(36, 38)
sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(3))
while 1:
raw, (nic, ptype, *x) = sock.recvfrom(2048)
if ptype == 2048 and raw[23] == 17:
src_host = socket.inet_ntoa(raw[SRC_IPv4])
src_port, = struct.Struct("!H").unpack(raw[SRC_PORTv4])
dst_host = socket.inet_ntoa(raw[DST_IPv4])
dst_port, = struct.Struct("!H").unpack(raw[DST_PORTv4])
print(f'{src_host}:{src_port} => {dst_host}:{dst_port}')```
我误读了你的问题,你处理的是广播地址而不是多播地址。你的问题是你没有发送 SO_BROADCAST 标志
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.
有一个明确的例子here。最重要的部分是:
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()
您还需要将该子网上的地址添加到接口。这很简单,只需输入:
ip addr add 2.255.255.254/8 dev eth1
假设广播在 2.0.0.0/8 上,接口名称是 eth1
并且 2.255.255.254 没有被其他主机占用。