#!/usr/bin/bash # # sync-zones.sh — rsync-based zone replication from primary to secondary # # Runs on the SECONDARY (netinfra-02). Syncs the zones/ directory from the # primary (netinfra-01) every 60 seconds. When rsync changes any zone file, # Technitium is reloaded (container restart) so it serves the fresh zones — # Technitium does NOT reliably auto-detect externally modified zone files, # which left the secondary serving stale records (seen 2026-09-01 [#344]). # # This is used instead of AXFR-based zone transfer because Technitium's zone # transfer mechanism uses port 53 (standard DNS), but on the netinfra hosts # port 53 is Pi-hole and Technitium is on port 5300. rsync-based replication # avoids the port conflict entirely. Native clustering tracked in [#469]. # # Install as a systemd service/timer or run via cron: # * * * * * /home/localuser/services/technitium/sync-zones.sh # set -uo pipefail PRIMARY_HOST="${PRIMARY_HOST:-pfv-netinfra-01.knel.net}" CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}" ZONE_DIR="$CONFIG_DIR/zones" LOCK_FILE="/tmp/technitium-zone-sync.lock" LOG_FILE="${LOG_FILE:-/home/localuser/services/technitium/sync.log}" # Reload command; array form so tests can substitute a fake. read -r -a RELOAD_CMD <<< "${RELOAD_CMD:-docker restart tsys-dns}" export RELOAD_CMD log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$LOG_FILE"; } # Sync zones from the primary and reload Technitium when anything changed. # Returns non-zero when rsync fails (reload failures are logged, not fatal). sync_and_reload() { local changed rc zone_count changed_count # --temp-dir avoids partial writes; --delete removes zones deleted on # primary; --itemize-changes reports exactly what changed. changed=$(rsync -az --delete --itemize-changes --out-format='%n' --temp-dir=/tmp \ "${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" 2>> "$LOG_FILE") rc=$? if [ "$rc" -ne 0 ]; then return "$rc" fi zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l) if [ -n "$changed" ]; then changed_count=$(printf '%s\n' "$changed" | wc -l) log "Sync complete: $zone_count zones, $changed_count changed — reloading Technitium" if "${RELOAD_CMD[@]}" >> "$LOG_FILE" 2>&1; then log "Technitium reload triggered" else log "WARN: reload command failed (rc=$?) — records may be stale until next change" fi else log "Sync complete: $zone_count zones (no changes)" fi } main() { # Prevent overlapping runs exec 9>"$LOCK_FILE" || exit 0 flock -n 9 || { log "another sync is running; skipping"; exit 0; } mkdir -p "$ZONE_DIR" log "Syncing zones from $PRIMARY_HOST..." sync_and_reload || { log "ERROR: rsync failed (rc=$?)"; exit 1; } } # Allow sourcing (unit tests) without executing the sync if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main fi