Files
dns-updater/README.md
T
unkinben 194080511c
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
records: add domain/IP-range allow+deny filtering
Adds a Filter (allow/deny CIDRs + allow/deny domain suffixes) applied to the
desired set before reconcile. Range rules match A/AAAA by value and PTR by the
address encoded in the reverse-DNS owner, so both the junk A record and its
reverse PTR are dropped together. Configured via -deny-ranges/-allow-ranges/
-deny-domains/-allow-domains (comma-separated) or DNS_UPDATER_* env.

Purpose: keep k8s pod/service CIDRs, LB VIP ranges and other internal addresses
out of the authoritative zones, and stop NOTAUTH updates for reverse zones the
server does not host (10.42.x, 198.18.200.x, etc.).
2026-07-18 01:59:29 +10:00

4.2 KiB

dns-updater

A small Go daemon that keeps a host's DNS records current on a BIND server via TSIG-signed RFC2136 dynamic updates. It replaces the puppet profiles::dns::updater shell script + dns-update.path/dns-update.service systemd pair with a single long-running service.

What it does

  • Reads a desired-records file (zone|name|type|ttl|value, the same format profiles::dns::record already emits).
  • Pushes the delta to the server — one TSIG-signed UPDATE message per zone, so one bad zone (e.g. a NOTZONE) cannot abort the others.
  • Re-reconciles when:
    • the records file changes (inotify on the directory, so atomic replace is caught),
    • a new interface address appears (DHCP assign/renew) — address removals (interface down) are ignored, so a transient link drop never disturbs records; loopback/link-local are ignored too,
    • a periodic safety-net timer fires,
    • it receives SIGHUP.
  • Exposes a local status API (unix socket) for facter and health checks.

Names are qualified correctly: a record name already ending in . is used verbatim, so there is no .. empty-label bug — and a malformed record is rejected with a clear error instead of being sent as broken wire data.

Why native RFC2136 (not nsupdate)

The daemon talks the update protocol directly (github.com/miekg/dns), so every zone update carries a structured server RCODE and error. That is the observability the shell version lacked — a bad name or a missing zone shows up immediately in the logs and the status API instead of an opaque nsupdate ... failed.

Configuration

Flags or env (see packaging/env.sample); env wins via the systemd EnvironmentFile:

flag env default
-server DNS_UPDATER_SERVER (required)
-key-file DNS_UPDATER_KEY_FILE /etc/dns-updater/key
-records-file DNS_UPDATER_RECORDS_FILE /var/lib/dns-updater/records
-state-file DNS_UPDATER_STATE_FILE /var/lib/dns-updater/applied
-watch-interfaces DNS_UPDATER_WATCH_INTERFACES true
-resync DNS_UPDATER_RESYNC 10m
-api DNS_UPDATER_API /run/dns-updater/api.sock
-log-level DNS_UPDATER_LOG_LEVEL info
-oneshot DNS_UPDATER_ONESHOT false
-deny-ranges DNS_UPDATER_DENY_RANGES (none)
-allow-ranges DNS_UPDATER_ALLOW_RANGES (none)
-deny-domains DNS_UPDATER_DENY_DOMAINS (none)
-allow-domains DNS_UPDATER_ALLOW_DOMAINS (none)

The TSIG key file is BIND format (key "name" { algorithm ...; secret "..."; };).

Filtering

*-ranges are comma-separated CIDRs; *-domains are comma-separated FQDN suffixes. Range rules apply to records that carry an address — A/AAAA by value, PTR by the address encoded in the reverse-DNS owner — so both prodnxsr01-kube-lb0 A 198.18.200.2 and the matching …200.18.198.in-addr.arpa PTR are dropped by -deny-ranges=198.18.200.0/24. Domain rules apply to every record by owner name. Deny wins; a non-empty allow list means "only these". This keeps k8s/LB/internal addresses (pod/service CIDRs, LB VIP ranges) out of the authoritative zones and stops NOTAUTH updates for zones the server does not host.

Status API

  • GET /status → JSON: health, managed-record count, last reconcile/change time, and per-zone {adds, deletes, rcode, error}.
  • GET /healthz → 200 when healthy, 503 otherwise.

The packaged facter fact (/opt/puppetlabs/facter/facts.d/dns_updater.sh) queries this and emits dns_updater_healthy, dns_updater_zones_failed, dns_updater_failed_zones, etc. Puppet already knows the desired records (it writes the file); these facts report what actually landed on the server.

Logging

INFO on real changes (applied) and failures (reconcile partial, zone update failed with zone + rcode); the steady-state "nothing to do" path stays at DEBUG, so periodic resyncs and interface flaps do not spam the journal. slog key=value output parses cleanly in VictoriaLogs.

Build / release

make build      # binary
make test       # unit + in-process TSIG server integration tests
make rpm        # RPM via nfpm (needs the binary)
make patch      # tag vX.Y.(Z+1) and push -> Woodpecker release