1+ """Knot DNS backend helpers for catalog updates, DNSSEC, and transfers."""
2+
13from functools import lru_cache
24from hashlib import sha1
35import logging
46import os
57import socket
68import select
7- import threading
89import time
910
1011import dns .dnssec
4344
4445
4546def _tsig_algorithm (name ):
47+ """Return a dnspython TSIG algorithm constant for the configured name."""
4648 if not name :
4749 return None
4850 algorithm = _TSIG_ALGORITHM_MAP .get (name .lower ())
@@ -53,6 +55,7 @@ def _tsig_algorithm(name):
5355
5456@lru_cache (maxsize = 1 )
5557def _knot_host_ip ():
58+ """Resolve NSLORD_KNOT_HOST to a concrete IP address (IPv4/IPv6)."""
5659 host = settings .NSLORD_KNOT_HOST
5760 try :
5861 dns .inet .af_for_address (host )
@@ -75,6 +78,7 @@ def _knot_host_ip():
7578
7679
7780def _update_keyring ():
81+ """Return TSIG keyring/name/algorithm tuple for dynamic updates."""
7882 key_name = settings .NSLORD_KNOT_UPDATE_KEY_NAME
7983 key_secret = settings .NSLORD_KNOT_UPDATE_KEY_SECRET
8084 if not key_name or not key_secret :
@@ -88,6 +92,7 @@ def _update_keyring():
8892
8993
9094def _transfer_keyring ():
95+ """Return TSIG keyring/name/algorithm tuple for AXFR/IXFR transfers."""
9196 key_name = settings .NSLORD_KNOT_TRANSFER_KEY_NAME
9297 key_secret = settings .NSLORD_KNOT_TRANSFER_KEY_SECRET
9398 if not key_name or not key_secret :
@@ -132,15 +137,18 @@ def _send_update(update: dns.update.Update) -> None:
132137
133138
134139def _catalog_member_subname (zone ):
140+ """Return catalog member label for a zone name (stable hash)."""
135141 zone = zone .rstrip ("." ) + "."
136142 return f"{ sha1 (zone .encode ()).hexdigest ()} .zones"
137143
138144
139145def _catalog_record_name (zone ):
146+ """Return the FQDN of the catalog member PTR record for a zone."""
140147 return f"{ _catalog_member_subname (zone )} .{ settings .CATALOG_ZONE } " .strip ("." ) + "."
141148
142149
143150def _new_update (zone ):
151+ """Create a dnspython Update with configured TSIG for a zone."""
144152 keyring , keyname , keyalgorithm = _update_keyring ()
145153 return dns .update .Update (
146154 zone ,
@@ -178,6 +186,7 @@ def ensure_default_ns(name):
178186
179187
180188def _write_bind_keypair (name , dnskey , private_key ):
189+ """Write BIND-style DNSKEY + private key files for Knot import."""
181190 import_dir = settings .NSLORD_KNOT_IMPORT_DIR
182191 if not import_dir or not private_key :
183192 return None
@@ -201,6 +210,7 @@ def _write_bind_keypair(name, dnskey, private_key):
201210
202211
203212def _key_ready_path (name ):
213+ """Return the path of the Knot CSK import readiness marker file."""
204214 import_dir = settings .NSLORD_KNOT_IMPORT_DIR
205215 if not import_dir :
206216 return None
@@ -209,12 +219,14 @@ def _key_ready_path(name):
209219
210220
211221def prepare_csk_key (name , * , dnskey , private_key = None ):
222+ """Prepare a CSK keypair for Knot import without triggering any update."""
212223 if not private_key :
213224 return
214225 _write_bind_keypair (name , dnskey , private_key )
215226
216227
217228def wait_for_csk_key_ready (name ):
229+ """Wait for Knot to signal completion of CSK import via .ready file."""
218230 ready_path = _key_ready_path (name )
219231 if not ready_path :
220232 return
@@ -228,6 +240,7 @@ def wait_for_csk_key_ready(name):
228240
229241
230242def _dnskey_present (name , dnskey ):
243+ """Check whether a specific DNSKEY RR appears in the zone."""
231244 query = dns .message .make_query (name , dns .rdatatype .DNSKEY , want_dnssec = True )
232245 response = dns .query .tcp (
233246 query ,
@@ -245,6 +258,7 @@ def _dnskey_present(name, dnskey):
245258
246259
247260def _wait_for_dnskey (name , dnskey , * , attempts : int = 20 , delay_seconds : float = 0.2 ):
261+ """Poll for the presence of a DNSKEY RR, with bounded retries."""
248262 for _ in range (attempts ):
249263 if _dnskey_present (name , dnskey ):
250264 return True
@@ -254,6 +268,7 @@ def _wait_for_dnskey(name, dnskey, *, attempts: int = 20, delay_seconds: float =
254268
255269
256270def _dnskey_set (name ):
271+ """Return the set of DNSKEY RR text values in the zone."""
257272 query = dns .message .make_query (name , dns .rdatatype .DNSKEY , want_dnssec = True )
258273 response = dns .query .tcp (
259274 query ,
@@ -273,6 +288,7 @@ def _dnskey_set(name):
273288def _wait_for_dnskey_set (
274289 name , expected , * , attempts : int = 60 , delay_seconds : float = 0.5
275290):
291+ """Poll until the DNSKEY RRset equals the expected set."""
276292 for _ in range (attempts ):
277293 if _dnskey_set (name ) == expected :
278294 return True
@@ -282,6 +298,7 @@ def _wait_for_dnskey_set(
282298
283299
284300def import_csk_key (name , * , dnskey , private_key = None ):
301+ """Import a CSK into Knot and optionally verify visibility."""
285302 if not wait_for_zone (name , attempts = 60 , interval_seconds = 0.5 ):
286303 raise KnotException (f"Knot zone { name } not ready for updates" )
287304 if private_key :
@@ -324,6 +341,7 @@ def import_csk_key(name, *, dnskey, private_key=None):
324341
325342
326343def wait_for_zone (name , * , attempts = 20 , interval_seconds = 0.5 ) -> bool :
344+ """Poll for zone availability by querying SOA from Knot."""
327345 query = dns .message .make_query (name , dns .rdatatype .SOA )
328346 query_timeout = min (settings .NSLORD_KNOT_TIMEOUT , 1.0 )
329347
@@ -349,12 +367,14 @@ def wait_for_zone(name, *, attempts=20, interval_seconds=0.5) -> bool:
349367
350368
351369def _sleep (seconds : float ) -> None :
370+ """Sleep without blocking signals in some environments."""
352371 if seconds <= 0 :
353372 return
354373 select .select ([], [], [], seconds )
355374
356375
357376def delete_zone (name ):
377+ """Delete a zone from the catalog."""
358378 catalog_update = _new_update (settings .CATALOG_ZONE )
359379 catalog_update .delete (_catalog_record_name (name ), "PTR" )
360380 _send_update (catalog_update )
@@ -413,23 +433,7 @@ def update_rrsets(
413433 has_changes = True
414434
415435 if has_changes :
416- update_done = {"error" : None }
417-
418- def _apply_update ():
419- try :
420- _send_update (update )
421- except Exception as exc :
422- update_done ["error" ] = exc
423-
424- thread = threading .Thread (target = _apply_update , daemon = True )
425- thread .start ()
426- thread .join (timeout = settings .NSLORD_KNOT_TIMEOUT * 2 )
427- if thread .is_alive ():
428- if settings .DEBUG :
429- return
430- raise KnotException ("Knot update timed out" )
431- if update_done ["error" ] is not None :
432- raise update_done ["error" ]
436+ _send_update (update )
433437
434438
435439def import_zonefile_rrsets (name , rrsets ):
@@ -463,6 +467,7 @@ def import_zonefile_rrsets(name, rrsets):
463467def ensure_soa_serial_min (
464468 name , serial : int , * , attempts : int = 5 , delay_seconds : float = 0.2
465469):
470+ """Ensure SOA serial is at least the given value by issuing updates."""
466471 query = dns .message .make_query (name , dns .rdatatype .SOA )
467472 for attempt in range (1 , attempts + 1 ):
468473 response = dns .query .tcp (
@@ -513,6 +518,7 @@ def ensure_soa_serial_min(
513518
514519
515520def get_zonefile (domain ) -> bytes :
521+ """Fetch an AXFR and render a filtered zonefile payload."""
516522 keyring , keyname , keyalgorithm = _transfer_keyring ()
517523 zone_name = domain .name .rstrip ("." ) + "."
518524 xfr = dns .query .xfr (
@@ -549,6 +555,7 @@ def get_zonefile(domain) -> bytes:
549555
550556
551557def get_keys (domain ):
558+ """Return DNSKEYs for a domain, including DS records where applicable."""
552559 query = dns .message .make_query (domain .name , dns .rdatatype .DNSKEY , want_dnssec = True )
553560 response = dns .query .tcp (
554561 query ,
0 commit comments