4 Commits

Author SHA1 Message Date
7367401734 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
2026-05-23 00:54:45 -06:00
89993ca207 L1/L2/L4: cleanup + README operational guide
L1 — Replace hand-rolled atoi/parseUint with strconv.ParseUint wrapped
in mustParseUint. Hamilton's reasoning: the comment "strconv adds
overhead we don't need" is the Lauren-Bug shape — we already validated
the input. Until we hadn't, on a path we couldn't predict. Stdlib's
edge-case coverage is the safer default; the wrapper panics on
malformed input so any future regression surfaces in CI, not as a
silent 0 serial.

L2 — applyUpdate no longer mutates the caller's RR header TTL. miekg/
dns parses the UPDATE message into RRs the caller still owns; silently
rewriting hdr.Ttl was a hygiene smell with no current functional
consequence but a clear documentation issue. Now we dns.Copy() the RR
before any header mutation.

L4 — README expanded with an "Operational constraints" section
documenting the contracts and limits operators should understand
before relying on this in production:
  - Single-process atomicity only (with rsync-race mitigation)
  - Process-global MsgAcceptFunc override
  - No-op UPDATE doesn't bump SOA (with touch-UPDATE workaround)
  - SOA invariants enforced strictly (zero, multi, non-apex SOA all
    refused)
  - Serial counter NNNN=9999 rollover semantics
  - TSIG replay window dependency on miekg/dns default
  - Git commit failure logged at ERROR, not rolled back
  - Per-key rate limit knobs

Every constraint maps to a Hamilton review finding; documenting the
contract in operator-facing prose closes the gap between code and
expectation that the review identified.
2026-05-22 21:33:37 -06:00
eba6313ec0 Phase 1.2: wire parser → typed config + 13 unit tests
The Corefile parser now fully populates typed fields on RFC2136 instead
of just recognising directives. Validation happens at parse-time so
configuration errors fail loud at CoreDNS startup rather than silent at
request time.

Added:
- config.go: tsigKey type, TSIG algorithm allowlist (rejects HMAC-MD5
  deliberately), base64 secret decoder with 8-byte minimum length check,
  canonical-key-name normalisation (lowercase + trailing dot).
- plugin.go: RFC2136 struct now carries TSIGKeys map, TTL uint32,
  PersistPath string. DefaultTTL=60.
- setup.go: parse() validates and stores tsig-key/ttl/persist directives.
  Duplicate key names rejected. Multiple TSIG keys allowed (for rotation).
  At-least-one-zone is enforced.
- setup_test.go: 13 table-driven cases (5 happy + 8 error paths) using
  caddy.NewTestController. All pass.

ServeDNS still passes through — UPDATE handling lands in Phase 1.4.

Module path: git.supported.systems/rsp2k/coredns-rfc2136
2026-05-21 10:31:22 -06:00
e9d37f483c Initial commit: plugin skeleton, compiles against CoreDNS 1.14.3
Sets up the package layout for a CoreDNS plugin that will accept RFC 2136
dynamic updates with TSIG authentication, primarily targeting self-hosted
ACME DNS-01 cert automation.

What this commit gives us:
- go.mod against coredns/caddy v1.1.4, coredns/coredns v1.14.3, miekg/dns v1.1.72
- plugin.go: RFC2136 struct + Handler interface (ServeDNS is pass-through)
- setup.go: init() registration + Corefile parser (skeleton — recognizes
  tsig-key, ttl, persist directives but doesn't yet wire them)
- README.md, .gitignore

go build ./... clean. No tests yet — those come with Phase 1.2 alongside
the actual UPDATE handler and in-memory store.

Plan: ~/.claude/plans/dood-does-coredns-offer-enumerated-piglet.md
2026-05-20 18:25:36 -06:00