coredns-rfc2136/setup.go
Ryan Malloy 1cca9a5aa7 Phase 1.3: in-memory store + ServeDNS query dispatch
ServeDNS now answers authoritatively for the configured zone(s):
- Apex SOA → synthetic SOA (serial = store generation counter)
- Apex NS  → synthetic NS pointing at p.Nameserver
- In-store lookups for any qtype
- NODATA vs NXDOMAIN correctly distinguished (SOA in authority section)
- UPDATE opcode → REFUSED (Phase 1.4 implements properly)
- Queries outside our zones pass through to Next

Added:
- store.go: recordStore with sync.RWMutex + atomic generation counter.
  Operations: Add (de-dupes), RemoveRRset, RemoveRR, RemoveName, Lookup
  (returns a copy so callers can't corrupt internal state), NameExists.
  All keyed on canonical lowercase + trailing-dot names.
- plugin.go: ServeDNS dispatch, findZone (longest-suffix match),
  syntheticSOA, syntheticNS. New Nameserver field.
- setup.go: nameserver directive. Default Nameserver = first zone apex.
  Store initialised at parse time.
- store_test.go: 12 unit tests covering add/dedupe/remove/lookup/
  generation/case-insensitivity/copy-safety.
- plugin_test.go: 10 dispatch tests covering pass-through, apex
  synthetics, in-store lookups, NXDOMAIN/NODATA semantics, UPDATE
  refusal, findZone longest-suffix-wins and case behavior.
- setup_test.go: 3 new cases for the nameserver directive + store init.

Total: 38 tests passing.

Module: git.supported.systems/rsp2k/coredns-rfc2136
2026-05-21 10:37:48 -06:00

141 lines
3.9 KiB
Go

package rfc2136
import (
"strconv"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/miekg/dns"
)
// log is the package logger, scoped so messages are prefixed `[rfc2136]`.
var log = clog.NewWithPlugin("rfc2136")
func init() {
plugin.Register("rfc2136", setup)
}
// setup is invoked by the CoreDNS plugin registry once per Corefile
// `rfc2136` directive. It parses the directive's arguments and block,
// constructs an RFC2136 handler, and links it into the plugin chain.
func setup(c *caddy.Controller) error {
p, err := parse(c)
if err != nil {
return plugin.Error("rfc2136", err)
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
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> [<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).
//
// Grammar:
//
// 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{
TSIGKeys: make(map[string]tsigKey),
TTL: DefaultTTL,
store: newStore(),
}
for c.Next() {
args := c.RemainingArgs()
if len(args) < 1 {
return nil, c.ArgErr()
}
// Normalize each declared zone to lowercase + trailing dot
// (CoreDNS canonical form). This makes later zone-membership
// checks an exact match against r.Question[0].Name.
for _, z := range args {
p.Zones = append(p.Zones, plugin.Host(z).NormalizeExact()...)
}
for c.NextBlock() {
switch c.Val() {
case "nameserver":
nArgs := c.RemainingArgs()
if len(nArgs) != 1 {
return nil, c.ArgErr()
}
p.Nameserver = dns.Fqdn(nArgs[0])
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))
}
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()
}
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()
}
p.PersistPath = pArgs[0]
default:
return nil, c.Errf("unknown directive: %s", c.Val())
}
}
}
if len(p.Zones) == 0 {
return nil, c.Err("at least one zone must be specified")
}
// Default nameserver to the first zone apex. The user can override
// via the `nameserver` directive — e.g. when the delegating parent
// zone publishes `auth NS dns.supported.systems`, this should be
// set to `dns.supported.systems.` to match.
if p.Nameserver == "" {
p.Nameserver = p.Zones[0]
}
return p, nil
}