Die Nameserver von fon-aria.de haben AAAA-Records, der Container hat aber kein IPv6. dns.query.udp() ist deshalb mit OSError [Errno 99] "Cannot assign requested address" durchgeschlagen und hat den ganzen Lauf abgebrochen - nach dem Anlegen der Challenge-Records. Adressen, zu denen es keine Route gibt, werden jetzt vorher aussortiert (UDP-connect, verschickt nichts) und waehrend des Wartens zusaetzlich verworfen, falls doch eine ausfaellt. Bleibt keine uebrig, gibt es eine klare Meldung statt eines Tracebacks. Bei fon-aria.de bleiben so die beiden IPv4-Adressen derselben Nameserver uebrig. - OSError wird in main() sauber abgefangen - tests/test_dns.py deckt die Faelle ab; im Container gegen die echte Zone gegengeprueft Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87 lines
2.8 KiB
Python
87 lines
2.8 KiB
Python
"""DNS-Propagationslogik: nicht erreichbare Nameserver (z.B. IPv6 ohne IPv6) fliegen raus."""
|
|
import pathlib
|
|
import sys
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
|
|
|
from app import dnsutil
|
|
|
|
V4A, V4B, V6 = "37.157.254.21", "5.199.143.173", "2001:4ba0:cafe:918::1"
|
|
NAME = "_acme-challenge.endian.fon-aria.de"
|
|
WANTED = {"value-a", "value-b"}
|
|
|
|
dnsutil.time.sleep = lambda _s: None # keine echten Wartezeiten im Test
|
|
|
|
# ---- reachable() ----------------------------------------------------------
|
|
assert dnsutil.reachable("127.0.0.1") is True
|
|
assert dnsutil.reachable("not-an-ip") is False
|
|
print("reachable ok")
|
|
|
|
# ---- authoritative_servers() filtert unerreichbare Adressen ---------------
|
|
dnsutil.reachable = lambda ip: ":" not in ip # so tut ein Host ohne IPv6
|
|
resolved = {"ns1.fon-aria.de": [V4A, V6], "ns2.fon-aria.de": [V4B]}
|
|
|
|
|
|
class FakeAnswer(list):
|
|
pass
|
|
|
|
|
|
class FakeResolver:
|
|
def __init__(self, *a, **kw):
|
|
self.nameservers = []
|
|
self.lifetime = self.timeout = 5
|
|
|
|
def resolve(self, qname, rtype, raise_on_no_answer=False):
|
|
qname = str(qname).rstrip(".")
|
|
if rtype == "NS":
|
|
return FakeAnswer([type("R", (), {"target": n + "."})() for n in resolved])
|
|
if rtype == "A":
|
|
return FakeAnswer([ip for ip in resolved.get(qname, []) if ":" not in ip])
|
|
if rtype == "AAAA":
|
|
return FakeAnswer([ip for ip in resolved.get(qname, []) if ":" in ip])
|
|
return FakeAnswer()
|
|
|
|
|
|
dnsutil.dns.resolver.Resolver = FakeResolver
|
|
servers = dnsutil.authoritative_servers("fon-aria.de", ["1.1.1.1"])
|
|
assert servers == [V4A, V4B], servers
|
|
print("authoritative_servers ok:", servers)
|
|
|
|
# ---- wait_for_txt: der tote Server darf das Ergebnis nicht blockieren -----
|
|
calls = []
|
|
|
|
|
|
def fake_txt(name, server):
|
|
calls.append(server)
|
|
if server == V6:
|
|
return None # kein Weg dorthin
|
|
return set(WANTED) # beide Werte da
|
|
|
|
|
|
dnsutil.txt_values = fake_txt
|
|
assert dnsutil.wait_for_txt({NAME: WANTED}, [V4A, V6, V4B], timeout=30, interval=1) is True
|
|
assert V6 in calls and calls.count(V6) == 1, calls # genau einmal probiert, dann verworfen
|
|
print("wait_for_txt ignoriert toten Nameserver ok")
|
|
|
|
# alle tot -> sauberes False statt Absturz
|
|
dnsutil.txt_values = lambda name, server: None
|
|
assert dnsutil.wait_for_txt({NAME: WANTED}, [V6], timeout=30, interval=1) is False
|
|
|
|
# Wert fehlt auf einem Server -> Timeout, kein Haenger
|
|
state = {"n": 0}
|
|
|
|
|
|
def slowly(name, server):
|
|
state["n"] += 1
|
|
return set(WANTED) if state["n"] > 3 else {"value-a"}
|
|
|
|
|
|
dnsutil.txt_values = slowly
|
|
assert dnsutil.wait_for_txt({NAME: WANTED}, [V4A], timeout=30, interval=1) is True
|
|
|
|
dnsutil.txt_values = lambda name, server: {"value-a"}
|
|
assert dnsutil.wait_for_txt({NAME: WANTED}, [V4A], timeout=2, interval=1) is False
|
|
print("wait_for_txt Timeout-Pfad ok")
|
|
|
|
print("\nALLE DNS-TESTS BESTANDEN")
|