96 lines
4.8 KiB
Python
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
|