Files
duffyduckandClaude Opus 5 efdaefc39e Nameserver ohne Route ueberspringen statt abzustuerzen
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>
2026-08-13 10:21:30 +02:00

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")