Add IPv6 support

Enable dual-stack mDNS lookups (Zeroconf ip_version was set to a value
that matched no IPVersion member), add AAAA query handling, and include
AAAA glue records for SRV targets. Link-local IPv6/IPv4 addresses are
filtered out since they aren't usable by clients off the local link.
This commit is contained in:
2026-08-05 10:22:32 +02:00
parent f031fde1a5
commit 0fa23ae378
+30 -17
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo, DNSQuestion, DNSOutgoing, RecordUpdateListener from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo, DNSQuestion, DNSOutgoing, RecordUpdateListener, IPVersion
from zeroconf.const import _TYPE_A, _CLASS_IN, _FLAGS_QR_QUERY from zeroconf.const import _TYPE_A, _TYPE_AAAA, _CLASS_IN, _FLAGS_QR_QUERY
import sys import sys
import time import time
from twisted.internet import reactor, defer, threads from twisted.internet import reactor, defer, threads
from twisted.names import client, dns, error, server from twisted.names import client, dns, error, server
from twisted.python import log from twisted.python import log
import socket import socket
import ipaddress
domain = sys.argv[1] domain = sys.argv[1]
port = int(sys.argv[2]) port = int(sys.argv[2])
@@ -16,7 +17,7 @@ timeout = 2
class DynamicResolver(object): class DynamicResolver(object):
def __init__(self): def __init__(self):
self.zeroconf = Zeroconf(ip_version=4) self.zeroconf = Zeroconf(ip_version=IPVersion.All)
def _dynamicResponseRequired(self, query): def _dynamicResponseRequired(self, query):
if str(query.name).endswith(domain): if str(query.name).endswith(domain):
@@ -84,22 +85,33 @@ class DynamicResolver(object):
answers = [dns.RRHeader(name=localname[:-6] + domain, ttl=ttl, type=dns.SRV, payload=dns.Record_SRV( answers = [dns.RRHeader(name=localname[:-6] + domain, ttl=ttl, type=dns.SRV, payload=dns.Record_SRV(
info.priority, info.weight, info.port, info.server[:-6] + domain info.priority, info.weight, info.port, info.server[:-6] + domain
))] ))]
additional = [dns.RRHeader(name=info.server[:-6] + domain, ttl=ttl, type=dns.A, payload=dns.Record_A( target = info.server[:-6] + domain
socket.inet_ntop(socket.AF_INET, addr) additional = [dns.RRHeader(name=target, ttl=ttl, type=dns.A, payload=dns.Record_A(
)) for addr in info.addresses] str(addr)
)) for addr in info.ip_addresses_by_version(IPVersion.V4Only)]
additional += [dns.RRHeader(name=target, ttl=ttl, type=dns.AAAA, payload=dns.Record_AAAA(
str(addr)
)) for addr in info.ip_addresses_by_version(IPVersion.V6Only) if not addr.is_link_local]
return answers, [], additional return answers, [], additional
def host(localname): def host(localname, qtype):
if qtype == dns.AAAA:
mdns_type, addr_len, parse_addr = _TYPE_AAAA, 16, ipaddress.IPv6Address
else:
mdns_type, addr_len, parse_addr = _TYPE_A, 4, ipaddress.IPv4Address
class listener(RecordUpdateListener): class listener(RecordUpdateListener):
def __init__(self): def __init__(self):
self.addrs = [] self.addrs = []
self.time = time.time() self.time = time.time()
def update_record(self, zc, now, record): def update_record(self, zc, now, record):
if record.type == _TYPE_A and len(record.address) == 4: if record.type == mdns_type and len(record.address) == addr_len:
self.addrs.append(socket.inet_ntop(socket.AF_INET, record.address)) addr = parse_addr(record.address)
if not addr.is_link_local:
self.addrs.append(str(addr))
l = listener() l = listener()
q = DNSQuestion(localname, _TYPE_A, _CLASS_IN) q = DNSQuestion(localname, mdns_type, _CLASS_IN)
self.zeroconf.add_listener(l, q) self.zeroconf.add_listener(l, q)
out = DNSOutgoing(_FLAGS_QR_QUERY) out = DNSOutgoing(_FLAGS_QR_QUERY)
out.add_question(q) out.add_question(q)
@@ -107,11 +119,12 @@ class DynamicResolver(object):
while len(l.addrs) == 0 and time.time() - l.time < timeout: while len(l.addrs) == 0 and time.time() - l.time < timeout:
time.sleep(0.1) time.sleep(0.1)
self.zeroconf.remove_listener(l) self.zeroconf.remove_listener(l)
answers = [dns.RRHeader(name=query.name.name, ttl=ttl, type=dns.A, payload=dns.Record_A( record_cls = dns.Record_AAAA if qtype == dns.AAAA else dns.Record_A
answers = [dns.RRHeader(name=query.name.name, ttl=ttl, type=qtype, payload=record_cls(
addr addr
)) for addr in l.addrs] )) for addr in l.addrs]
return answers, [], [] return answers, [], []
d = defer.Deferred() d = defer.Deferred()
@@ -125,10 +138,10 @@ class DynamicResolver(object):
elif query.type == dns.SRV: elif query.type == dns.SRV:
d = threads.deferToThread(srv, localname) d = threads.deferToThread(srv, localname)
return d return d
elif query.type == dns.A: elif query.type in (dns.A, dns.AAAA):
d = threads.deferToThread(host, localname) d = threads.deferToThread(host, localname, query.type)
return d return d
elif query.type != dns.AAAA: else:
print("Unsupported request", query) print("Unsupported request", query)
d.callback(([], [], [])) d.callback(([], [], []))
return d return d