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
This commit is contained in:
Ryan Malloy 2026-05-21 10:31:22 -06:00
parent e9d37f483c
commit eba6313ec0
6 changed files with 364 additions and 29 deletions

View file

@ -1,6 +1,8 @@
package rfc2136
import (
"strconv"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
@ -28,22 +30,28 @@ func setup(c *caddy.Controller) error {
return p
})
log.Infof("registered for zones: %v", p.Zones)
log.Infof("registered for zones=%v keys=%d ttl=%d persist=%q",
p.Zones, len(p.TSIGKeys), p.TTL, p.PersistPath)
return nil
}
// parse reads a single `rfc2136 <zone> { ... }` block from the Corefile.
// parse reads a single `rfc2136 <zone> [<zone>...] { ... }` block from
// the Corefile and returns a fully-populated RFC2136 handler with all
// values validated at parse time (so configuration errors fail fast at
// CoreDNS startup, not later mid-request).
//
// Phase 1 grammar (only the surface is parsed; sub-directives are
// accepted but ignored — Phase 2 wires them):
// Grammar:
//
// rfc2136 <zone> {
// tsig-key <name> <algorithm> <secret>
// ttl <seconds>
// persist <path>
// rfc2136 <zone> [<zone>...] {
// tsig-key <name> <algorithm> <base64-secret> ; may repeat
// ttl <seconds> ; default 60
// persist <path> ; default off (in-memory only)
// }
func parse(c *caddy.Controller) (*RFC2136, error) {
p := &RFC2136{}
p := &RFC2136{
TSIGKeys: make(map[string]tsigKey),
TTL: DefaultTTL,
}
for c.Next() {
args := c.RemainingArgs()
@ -59,28 +67,47 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
for c.NextBlock() {
switch c.Val() {
case "tsig-key":
// tsig-key <name> <algorithm> <base64-secret>
kArgs := c.RemainingArgs()
if len(kArgs) != 3 {
return nil, c.Errf("tsig-key requires 3 args (name algorithm secret), got %d", len(kArgs))
}
// Phase 2: store in p.tsigKeys[name] = tsigKey{algo, secret}
log.Debugf("tsig-key parsed (storage NYI): name=%s alg=%s", kArgs[0], kArgs[1])
keyName := canonicalKeyName(kArgs[0])
algo, err := parseTSIGAlgorithm(kArgs[1])
if err != nil {
return nil, c.Err(err.Error())
}
secret, err := decodeTSIGSecret(kArgs[2])
if err != nil {
return nil, c.Errf("tsig-key %q: %v", keyName, err)
}
if _, exists := p.TSIGKeys[keyName]; exists {
return nil, c.Errf("duplicate tsig-key %q", keyName)
}
p.TSIGKeys[keyName] = tsigKey{Algorithm: algo, Secret: secret}
case "ttl":
tArgs := c.RemainingArgs()
if len(tArgs) != 1 {
return nil, c.ArgErr()
}
// Phase 2: parse uint32, validate range, store in p.ttl
log.Debugf("ttl parsed (storage NYI): %s", tArgs[0])
ttl, err := strconv.ParseUint(tArgs[0], 10, 32)
if err != nil {
return nil, c.Errf("ttl must be a non-negative integer: %v", err)
}
// Anything over a week is almost certainly a mistake
// for ACME challenge records, but allow up to the
// uint32 max so we don't ship an arbitrary cap.
p.TTL = uint32(ttl)
case "persist":
pArgs := c.RemainingArgs()
if len(pArgs) != 1 {
return nil, c.ArgErr()
}
log.Debugf("persist parsed (storage NYI): %s", pArgs[0])
p.PersistPath = pArgs[0]
default:
return nil, c.Errf("unknown directive: %s", c.Val())
@ -88,5 +115,9 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
}
}
if len(p.Zones) == 0 {
return nil, c.Err("at least one zone must be specified")
}
return p, nil
}