coredns-rfc2136/plugin.go
Ryan Malloy 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

73 lines
2.8 KiB
Go

// 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)
}