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
73 lines
2.8 KiB
Go
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)
|
|
}
|