From 4e375b130ce9ab0fe610b07f14ccff57f499def5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20D=C3=BCsterhaupt?= Date: Sat, 5 Sep 2026 13:55:34 +0200 Subject: [PATCH] feat(dyndns): add optional rndc journal synchronization Add configurable post-update journal synchronization via rndc sync -clean. Separate RFC 2136 update settings from local RNDC control settings. Run synchronization only after a successful nsupdate request and only when DYNDNS_RNDC_SYNC_CLEAN is enabled. Treat RNDC synchronization failures as warnings because the DNS update has already been accepted and named retains the authoritative state in its journal. --- dyndns.sh | 86 +++++++++++++++++++++++++++++++++------------------- vars.example | 40 ++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 33 deletions(-) diff --git a/dyndns.sh b/dyndns.sh index 38ac2ab..0116808 100644 --- a/dyndns.sh +++ b/dyndns.sh @@ -241,6 +241,19 @@ _vars_source_check() { [ -n "$DYNDNS_BIND_ZONE_DIR" ] || _die "DYNDNS_BIND_ZONE_DIR env-var undefined" 5 [ -n "$DYNDNS_BIND_ZONE_FILE" ] || _die "DYNDNS_BIND_ZONE_FILE env-var undefined" 5 + case "$DYNDNS_RNDC_SYNC_CLEAN" in + 0|1) + ;; + *) + _die "DYNDNS_RNDC_SYNC_CLEAN must be either 0 or 1" 5 + ;; + esac + + if [ "$DYNDNS_RNDC_SYNC_CLEAN" = "1" ]; then + [ -n "$DYNDNS_RNDC_SERVER" ] || _die "DYNDNS_RNDC_SERVER env-var undefined" 5 + [ -n "$DYNDNS_RNDC_KEY" ] || _die "DYNDNS_RNDC_KEY env-var undefined" 5 + fi + _log "Leave the function '_vars_source_check()'..." 1 return 0 @@ -265,12 +278,15 @@ _verify_runtime_init() { || command -v "$DYNDNS_DB_PROGRAM" >/dev/null 2>&1 \ || _die "Database client '$DYNDNS_DB_PROGRAM' not available. $help_note" 5 - command -v rndc >/dev/null 2>&1 || _die "rndc not available. $help_note" 5 command -v sed >/dev/null 2>&1 || _die "sed not available. $help_note" 5 command -v grep >/dev/null 2>&1 || _die "grep not available. $help_note" 5 command -v awk >/dev/null 2>&1 || _die "awk not available. $help_note" 5 command -v nsupdate >/dev/null 2>&1 || _die "nsupdate not available. $help_note" 5 + if [ "$DYNDNS_RNDC_SYNC_CLEAN" = "1" ]; then + command -v rndc >/dev/null 2>&1 || _die "rndc not available. $help_note" 5 + fi + _log "Leave the function '_verify_runtime_init()'..." 1 return 0 } #=> _verify_runtime_init() @@ -371,6 +387,16 @@ _vars_setup() { set_var DYNDNS_BIND_ZONE_FILE_SIGNED "$DYNDNS_BIND_ZONE_DIR/$DYNDNS_BIND_ZONE.zone.signed" set_var DYNDNS_BIND_ZONE_TTL 300 + # RNDC post-update zone synchronization defaults + # + # The RNDC control interface is separate from the authoritative DNS + # listener used by nsupdate. Keep the default disabled so existing + # installations do not gain a new administrative post-update action + # unless it is explicitly enabled in their vars file. + set_var DYNDNS_RNDC_SERVER "127.0.0.1" + set_var DYNDNS_RNDC_KEY "" + set_var DYNDNS_RNDC_SYNC_CLEAN 0 + # Create a per-run temp directory name. The directory itself may be created # lazily later by dyndns_mktemp(). if [ -z "$DYNDNS_TEMP_DIR_session" ]; then @@ -1297,26 +1323,11 @@ _zone_updates_apply() { return 1 } - _log "Check the zone '$DYNDNS_BIND_ZONE' for DNSSEC capabilities..." 1 - if [ -f "$DYNDNS_BIND_ZONE_FILE_SIGNED" ]; then - _log "The zone '$DYNDNS_BIND_ZONE' uses DNSSEC..." 1 - zone_uses_dnssec=1 - else - _log "The zone '$DYNDNS_BIND_ZONE' does not use DNSSEC..." 1 - zone_uses_dnssec=0 - fi - { printf 'debug\n' printf 'server %s\n' "$DYNDNS_BIND_SERVER" printf 'zone %s.\n' "$DYNDNS_BIND_ZONE" - if [ "$zone_uses_dnssec" -eq 1 ]; then - printf '; Move %s from secure to insecure temporarily\n' "$DYNDNS_BIND_ZONE_FILE" - printf 'update delete %s. DNSKEY\n' "$DYNDNS_BIND_ZONE" - printf 'send\n' - fi - awk -F '\t' -v zone_file="$DYNDNS_BIND_ZONE_FILE" ' $1 == "del" { del_owner[++del_count] = $2 @@ -1375,9 +1386,20 @@ _zone_updates_apply() { cp "$nsupdate_file" "$DYNDNS_LOG_UPDATE_FILE" 2>/dev/null || true - if ! nsupdate -k "$DYNDNS_BIND_ZONE_KEY" "$nsupdate_file" >/dev/null 2>&1; then + nsupdate_output=$(nsupdate -d -k "$DYNDNS_BIND_ZONE_KEY" "$nsupdate_file" 2>&1) + nsupdate_rc=$? + + if [ "$nsupdate_rc" -ne 0 ]; then + { + printf '%s\n' "nsupdate output (exit code: $nsupdate_rc):" + printf '%s\n' "$nsupdate_output" + } >> "$DYNDNS_LOG_FILE" + + cp "$nsupdate_file" \ + "$DYNDNS_LOG_UPDATE_FILE.failed" 2>/dev/null || true rm -f "$nsupdate_file" - _log "Zone updates could not be applied." 4 + + _log "Zone updates could not be applied (nsupdate exit code: $nsupdate_rc)." 4 _log "Leave the function '_zone_updates_apply()'..." 1 return 1 fi @@ -1397,20 +1419,22 @@ _zone_updates_apply() { rm -f "$nsupdate_file" _log "Zone updates were applied successfully." 2 - if command -v rndc >/dev/null 2>&1; then - if rndc -s "$DYNDNS_BIND_SERVER" -k "$DYNDNS_BIND_ZONE_KEY" freeze "$DYNDNS_BIND_ZONE" >/dev/null 2>&1; then - _log "The zone '$DYNDNS_BIND_ZONE' was frozen." 2 - if rndc -s "$DYNDNS_BIND_SERVER" -k "$DYNDNS_BIND_ZONE_KEY" thaw "$DYNDNS_BIND_ZONE" >/dev/null 2>&1; then - _log "The zone '$DYNDNS_BIND_ZONE' was reloaded." 2 - else - _log "Couldn't reload the zone '$DYNDNS_BIND_ZONE'." 4 - _log "Leave the function '_zone_updates_apply()'..." 1 - return 1 - fi + # Synchronize dynamic zone journal changes into the master zone file. + # + # BIND keeps successful RFC 2136 updates in the zone journal until a zone dump + # occurs. sync -clean writes the active journal state to the master zone file + # and removes the journal after a successful synchronization. + # + # A synchronization failure is intentionally non-fatal: nsupdate has already + # succeeded and named keeps the authoritative runtime state in the journal. + if [ "$DYNDNS_RNDC_SYNC_CLEAN" = "1" ]; then + if rndc \ + -s "$DYNDNS_RNDC_SERVER" \ + -k "$DYNDNS_RNDC_KEY" \ + sync -clean "$DYNDNS_BIND_ZONE" >/dev/null 2>&1; then + _log "The zone '$DYNDNS_BIND_ZONE' was synchronized and journals were cleaned." 2 else - _log "Couldn't freeze the zone '$DYNDNS_BIND_ZONE'." 4 - _log "Leave the function '_zone_updates_apply()'..." 1 - return 1 + _log "Zone update succeeded, but rndc sync -clean failed for '$DYNDNS_BIND_ZONE'." 3 fi fi diff --git a/vars.example b/vars.example index 44e1a1e..5a08845 100644 --- a/vars.example +++ b/vars.example @@ -81,7 +81,12 @@ # DNS / BIND # ------------------------------------------------------------------ -# Authoritative DNS server used for update operations +# Authoritative DNS server used for RFC 2136 dynamic update operations. +# +# This address is used by nsupdate on DNS port 53. It must point to the +# authoritative BIND listener and matching view serving DYNDNS_BIND_ZONE. +# +# Do not use a local recursive-only listener or a non-authoritative view. #set_var DYNDNS_BIND_SERVER "127.0.0.1" # Service name used when reloading/reconfiguring the name server @@ -90,7 +95,10 @@ # DNS zone handled by dynDNS #set_var DYNDNS_BIND_ZONE "example24.com" -# TSIG key file or key identifier used for nsupdate access +# TSIG key file or key identifier used for nsupdate access. +# +# This key should be restricted in BIND with update-policy and must not be +# confused with the RNDC control key configured below. #set_var DYNDNS_BIND_ZONE_KEY "keyfile" # Directory containing the primary zone files @@ -105,6 +113,34 @@ # Default TTL for generated/managed records #set_var DYNDNS_BIND_ZONE_TTL 300 +# ------------------------------------------------------------------ +# RNDC POST-UPDATE ZONE SYNCHRONIZATION +# ------------------------------------------------------------------ + +# Local BIND control endpoint used for optional post-update synchronization. +# +# rndc connects to named's control interface, normally TCP port 953. +# This is independent from DYNDNS_BIND_SERVER, which is used by nsupdate +# against DNS port 53. +#set_var DYNDNS_RNDC_SERVER "127.0.0.1" + +# RNDC control key authorized by the named controls {} configuration. +# +# This key is intentionally separate from DYNDNS_BIND_ZONE_KEY. The latter is +# used by nsupdate for RFC 2136 DNS updates, while this key authorizes BIND +# administrative operations such as "rndc sync -clean". +#set_var DYNDNS_RNDC_KEY "/etc/named/rndc.key" + +# Synchronize dynamic zone journals after a successful nsupdate request. +# +# 0 = disabled; named handles zone dumps using its normal maintenance schedule. +# 1 = execute "rndc sync -clean " after a successful DNS update. +# +# A failed synchronization must be logged as a warning only. The dynamic update +# remains successful because named retains the authoritative state in its +# journal until the next successful zone dump. +#set_var DYNDNS_RNDC_SYNC_CLEAN 0 + # ------------------------------------------------------------------ # MEMBER DEFAULTS # ------------------------------------------------------------------