// Package rfc2136 implements a CoreDNS plugin that accepts dynamic DNS // updates per RFC 2136 (UPDATE opcode), authenticated via TSIG. The // primary use case is self-hosted ACME DNS-01 cert automation: an ACME // client (e.g. Caddy via caddy-dns/rfc2136) injects _acme-challenge TXT // records into a delegated sub-zone that this plugin serves. // // Scope: // - Handles UPDATE messages (OPCODE=5) for configured zones. // - Verifies TSIG signatures (HMAC-SHA family; algorithm-pluggable). // - Stores records in memory; optional periodic snapshot to disk. // - Serves queries (SOA, NS, A, AAAA, TXT) for the configured zone // from the in-memory store plus a synthetic SOA/NS apex. // // Non-goals: // - General-purpose authoritative DNS (use `auto`/`file` for that). // - DNSSEC signing (add later via the `dnssec` plugin in front). // // Phase 1.2 status: parser fully wires Corefile into typed config. // ServeDNS still passes through to the next plugin — UPDATE handling // and zone-serving land in Phase 1.3/1.4. See plan at // // ~/.claude/plans/dood-does-coredns-offer-enumerated-piglet.md package rfc2136 import ( "context" "github.com/coredns/coredns/plugin" "github.com/miekg/dns" ) // DefaultTTL is the TTL applied to dynamically-added records when the // Corefile doesn't specify one. 60s matches the short-lived nature of // ACME challenge TXT records and keeps stale answers from lingering in // resolver caches. const DefaultTTL uint32 = 60 // RFC2136 is the plugin handler. One instance per Corefile server block. type RFC2136 struct { // Next is the downstream plugin in the chain. Next plugin.Handler // Zones is the set of canonical (dot-terminated, lowercase) zone // names this instance is authoritative for. Queries outside these // zones pass through to Next. Zones []string // TSIGKeys is keyed by canonical key name (lowercased, trailing // dot). Empty means TSIG is disabled — UPDATEs without TSIG would // be rejected unconditionally in Phase 1.4. TSIGKeys map[string]tsigKey // TTL is applied to dynamically-injected records that don't carry // an explicit TTL in the UPDATE message. TTL uint32 // PersistPath, when non-empty, names a file the plugin writes a // JSON snapshot of its in-memory store to on a periodic schedule. // Empty means in-memory only (acceptable for ACME challenges, // which are seconds-to-minutes lived and re-issued on restart). PersistPath string } // Name implements plugin.Handler. func (p *RFC2136) Name() string { return "rfc2136" } // ServeDNS implements plugin.Handler. Phase 1.x is a pass-through so // the plugin can register, parse config, and live in the chain without // changing behavior. Phase 1.3 wires UPDATE handling + query serving. func (p *RFC2136) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r) }