Files
archy/scripts/c01_network_mdns.py

96 lines
4.8 KiB
Python

"""Bounded packet-only mDNS transport for the appointed host LAN and manager.
No DNS records are generated or rewritten. Private injection uses multicast
hop-limit zero: local DNS-SD consumers receive the real packet, but pasta cannot
send the injected copy back out. Host loopback copies are bounded/deduplicated.
"""
import hashlib
import ipaddress
import socket
import struct
import time
PORT=5353
GROUP4='224.0.0.251'
GROUP6='ff02::fb'
MAX_PACKET=9000
def validate(value,uplink):
if type(value) is not dict or set(value)!={'interface','ifindex','ipv4'}:raise ValueError('mDNS scope fields')
if value['interface']!=uplink['wan'][0] or value['ipv4']!=uplink['wan'][2] or uplink.get('routing')!='host-dual-stack':raise ValueError('mDNS must use the appointed physical uplink')
if type(value['ifindex']) is not int or not 0<value['ifindex']<2**31:raise ValueError('mDNS interface identity')
if str(ipaddress.IPv4Address(value['ipv4']))!=value['ipv4']:raise ValueError('mDNS IPv4 source')
def make_socket(family,interface,ipv4,private=False):
index=socket.if_nametoindex(interface)
result=socket.socket(family,socket.SOCK_DGRAM|socket.SOCK_CLOEXEC)
try:
result.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
if family==socket.AF_INET:
result.setsockopt(socket.IPPROTO_IP,8,1) # IP_PKTINFO
result.setsockopt(socket.IPPROTO_IP,12,1) # IP_RECVTTL
result.bind(('0.0.0.0',PORT))
result.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(GROUP4)+socket.inet_aton(ipv4)+struct.pack('=i',index))
result.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_IF,socket.inet_aton(ipv4))
result.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,0 if private else 255)
result.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_LOOP,1)
elif family==socket.AF_INET6:
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1)
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_RECVPKTINFO,1)
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_RECVHOPLIMIT,1)
result.bind(('::',PORT))
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_JOIN_GROUP,
socket.inet_pton(socket.AF_INET6,GROUP6)+struct.pack('=I',index))
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_MULTICAST_IF,index)
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_MULTICAST_HOPS,0 if private else 255)
result.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_MULTICAST_LOOP,1)
else:raise ValueError('mDNS family')
result.setblocking(False)
return result
except BaseException:result.close();raise
def destination(channel):
if channel.family==socket.AF_INET:return (GROUP4,PORT)
return (GROUP6,PORT,0,channel.getsockopt(socket.IPPROTO_IPV6,socket.IPV6_MULTICAST_IF))
class Link:
def __init__(self,host,managed,host_index):
self.host,self.managed,self.host_index=host,managed,host_index
self.recent={}
self.observed=set()
def receive(self,realm,enabled):
source=self.host if realm=='host' else self.managed
target=self.managed if realm=='host' else self.host
packet,anc,flags,_=source.recvmsg(MAX_PACKET+1,256)
if flags or not 12<=len(packet)<=MAX_PACKET or not enabled:return
index=None;group=None;hop=None
for level,kind,raw in anc:
if (level,kind)==(socket.IPPROTO_IP,8) and len(raw)==12:
index=struct.unpack('=I',raw[:4])[0];group=socket.inet_ntop(socket.AF_INET,raw[8:12])
elif (level,kind)==(socket.IPPROTO_IPV6,socket.IPV6_PKTINFO) and len(raw)==20:
index=struct.unpack('=I',raw[16:])[0];group=socket.inet_ntop(socket.AF_INET6,raw[:16])
elif (level,kind) in ((socket.IPPROTO_IP,socket.IP_TTL),(socket.IPPROTO_IPV6,socket.IPV6_HOPLIMIT)) and len(raw)==4:
hop=struct.unpack('=i',raw)[0]
if group!=(GROUP4 if source.family==socket.AF_INET else GROUP6) or hop!=255:return
if realm=='host' and index!=self.host_index:return
now=time.monotonic()
self.recent={key:expiry for key,expiry in self.recent.items() if expiry>now}
key=hashlib.sha256(packet).digest()
if realm=='host' and key in self.recent:return
if realm=='managed':
if len(self.recent)>=256:return
self.recent[key]=now+1
target.sendto(packet,destination(target))
if realm not in self.observed:
self.observed.add(realm)
print('C01_MDNS_FORWARD family='+('4' if source.family==socket.AF_INET else '6')+' source='+realm,flush=True)
def close(self):
self.managed.close() # host socket belongs to the ordinary listener lease