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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
611593b5c0
commit
efdaefc39e
+1
-1
@@ -1,3 +1,3 @@
|
||||
"""Wildcard Let's Encrypt certificate creator for Plesk-managed DNS zones."""
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.1.1"
|
||||
|
||||
+53
-14
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
|
||||
import dns.exception
|
||||
@@ -36,7 +37,7 @@ def authoritative_servers(zone: str, fallback_resolvers: list[str]) -> list[str]
|
||||
names = sorted({str(rdata.target).rstrip(".") for rdata in answer})
|
||||
if names:
|
||||
break
|
||||
except dns.exception.DNSException as exc:
|
||||
except (dns.exception.DNSException, OSError) as exc:
|
||||
log.debug("NS lookup for %s failed: %s", zone, exc)
|
||||
|
||||
lookup = _resolver(fallback_resolvers)
|
||||
@@ -45,19 +46,41 @@ def authoritative_servers(zone: str, fallback_resolvers: list[str]) -> list[str]
|
||||
try:
|
||||
for rdata in lookup.resolve(name, rtype, raise_on_no_answer=False):
|
||||
ips.append(str(rdata))
|
||||
except dns.exception.DNSException:
|
||||
except (dns.exception.DNSException, OSError):
|
||||
continue
|
||||
|
||||
unique = list(dict.fromkeys(ips))
|
||||
if unique:
|
||||
log.info("Authoritative nameservers for %s: %s (%s)", zone, ", ".join(names), ", ".join(unique))
|
||||
usable = [ip for ip in unique if reachable(ip)]
|
||||
dropped = [ip for ip in unique if ip not in usable]
|
||||
if dropped:
|
||||
# Typical inside a container without IPv6: the NS have AAAA records we
|
||||
# cannot talk to. Their IPv4 addresses answer the same zone data.
|
||||
log.info("Skipping unreachable nameserver address(es): %s", ", ".join(dropped))
|
||||
if usable:
|
||||
log.info("Authoritative nameservers for %s: %s (%s)", zone, ", ".join(names), ", ".join(usable))
|
||||
else:
|
||||
log.warning("Could not determine authoritative nameservers for %s, using public resolvers.", zone)
|
||||
return unique
|
||||
log.warning("Could not reach any authoritative nameserver for %s, using public resolvers.", zone)
|
||||
return usable
|
||||
|
||||
|
||||
def txt_values(name: str, server: str) -> set[str]:
|
||||
"""TXT values for *name* as seen by the DNS server at *server*."""
|
||||
def reachable(server: str) -> bool:
|
||||
"""Can this machine send a UDP packet to *server* at all?
|
||||
|
||||
``connect()`` on a UDP socket sends nothing, it only resolves the route -
|
||||
exactly what we need to weed out IPv6 addresses on an IPv4-only host.
|
||||
"""
|
||||
family = socket.AF_INET6 if ":" in server else socket.AF_INET
|
||||
try:
|
||||
with socket.socket(family, socket.SOCK_DGRAM) as sock:
|
||||
sock.connect((server, 53))
|
||||
return True
|
||||
except OSError as exc:
|
||||
log.debug("Nameserver %s is not reachable from here: %s", server, exc)
|
||||
return False
|
||||
|
||||
|
||||
def txt_values(name: str, server: str) -> set[str] | None:
|
||||
"""TXT values for *name* as seen by *server*. None if the server is unusable."""
|
||||
query = dns.message.make_query(name, dns.rdatatype.TXT)
|
||||
values: set[str] = set()
|
||||
try:
|
||||
@@ -67,6 +90,10 @@ def txt_values(name: str, server: str) -> set[str]:
|
||||
except dns.exception.DNSException as exc:
|
||||
log.debug("TXT query %s @%s failed: %s", name, server, exc)
|
||||
return values
|
||||
except OSError as exc:
|
||||
# No route to that address (e.g. IPv6 without IPv6 connectivity).
|
||||
log.debug("TXT query %s @%s not possible: %s", name, server, exc)
|
||||
return None
|
||||
|
||||
for rrset in response.answer:
|
||||
if rrset.rdtype != dns.rdatatype.TXT:
|
||||
@@ -89,19 +116,31 @@ def wait_for_txt(
|
||||
return False
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
alive = list(servers)
|
||||
attempt = 0
|
||||
while True:
|
||||
attempt += 1
|
||||
missing: list[str] = []
|
||||
unusable: list[str] = []
|
||||
for name, wanted in expected.items():
|
||||
for server in servers:
|
||||
for server in alive:
|
||||
seen = txt_values(name, server)
|
||||
for value in wanted:
|
||||
if value not in seen:
|
||||
missing.append(f"{name} @{server}")
|
||||
break
|
||||
if seen is None:
|
||||
unusable.append(server)
|
||||
elif not wanted <= seen:
|
||||
missing.append(f"{name} @{server}")
|
||||
|
||||
if unusable:
|
||||
for server in dict.fromkeys(unusable):
|
||||
alive.remove(server)
|
||||
log.warning("Nameserver %s cannot be queried from here - ignoring it.", server)
|
||||
if not alive:
|
||||
log.error("None of the nameservers could be queried at all.")
|
||||
return False
|
||||
continue # judge the round again, now without the dead servers
|
||||
|
||||
if not missing:
|
||||
log.info("DNS propagation confirmed on all %d nameserver(s).", len(servers))
|
||||
log.info("DNS propagation confirmed on all %d nameserver(s).", len(alive))
|
||||
return True
|
||||
|
||||
remaining = deadline - time.monotonic()
|
||||
|
||||
@@ -225,6 +225,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
except AcmeFailure as exc:
|
||||
log.error("%s", exc)
|
||||
return 1
|
||||
except OSError as exc:
|
||||
log.error("Network error: %s", exc)
|
||||
return 1
|
||||
except KeyboardInterrupt: # pragma: no cover
|
||||
log.error("Aborted.")
|
||||
return 130
|
||||
|
||||
Reference in New Issue
Block a user