Send DNS NOTIFY to secondaries after every UPDATE

Per RFC 1996, a master that mutates a zone SHOULD notify its
secondaries so they can immediately AXFR rather than wait for their
next SOA-refresh poll. Without this, propagation lag from UPDATE to
public DNS is bounded by the secondary's refresh interval (300s for
us) — which is borderline for ACME validation timing.

New Corefile directive:
    notify <host[:port]> [<host[:port]>...]

Targets accept bare hostnames (port 53 default), host:port, or
[ipv6]:port. The same list applies to every zone in the rfc2136
block.

Implementation: fire-and-forget UDP per target, each in its own
goroutine, capped by a 2s timeout. The UPDATE response to the client
is never held pending NOTIFY acks (RFC 1996 §4 explicitly decouples
them). Failures log at DEBUG only — a briefly-unreachable secondary
is normal and would otherwise spam logs.

Retires the external scripts/notify-secondaries.py workflow for any
deployment that wires the directive: secondaries now hear about
changes within seconds of the UPDATE landing, no cron or manual
invocation needed.

New tests:
- TestSendNotify_DeliversToTarget — packet arrives, opcode + zone correct
- TestSendNotify_NoTargets_NoCrash — empty list short-circuits
- TestSendNotify_BadTarget_LogsButDoesNotBlock — fire-and-forget timing
- TestNotifyOne_AppendsDefaultPort — host vs host:port normalization
This commit is contained in:
Ryan Malloy 2026-05-23 00:54:45 -06:00
parent 89993ca207
commit 7367401734
6 changed files with 241 additions and 0 deletions

View file

@ -69,6 +69,13 @@ type RFC2136 struct {
// disables rate limiting (test mode, or insecure deployments).
// Populated in setup() once TSIG keys are known.
rateLimit *rateLimiter
// NotifyTargets is the list of secondary servers (IP[:port]) to
// send DNS NOTIFY messages to after every successful UPDATE.
// Default port 53. Empty list = no NOTIFY (secondaries rely on
// their own SOA-refresh polling). Configured via the `notify`
// directive in the Corefile.
NotifyTargets []string
}
// Name implements plugin.Handler.