Phase 2b: refactor to file-backed storage; UPDATE writes zones/*.zone
Major architectural pivot per the user's "RFC 2136 mechanism for the existing zonefiles, not a new in-memory thing" framing. The plugin no longer maintains its own in-memory state OR serves any queries -- both of those are now the auto plugin's job, reading the same zone files. The plugin's sole responsibility is now: receive TSIG-authed UPDATE messages, edit the matching zones/<zone>.zone file, bump the SOA serial in CalVer (YYYYMMDDNN) form, and optionally auto-commit to git. What changed: - DELETED: store.go (in-memory recordStore), store_test.go (12 tests), plugin_test.go (10 ServeDNS query tests), old update_test.go. - NEW: zonefile.go -- file-backed authority for one zone. loadRRs via miekg/dns zone parser; mutation helpers (lookupIn/nameExistsIn/ removeRRsetFrom/removeRRFrom/removeNameFrom/addRRTo) on []dns.RR slices; bumpSerial with CalVer semantics + NN exhaustion handling; writeAtomic via temp-file rename; commit shells to `git add && git commit` with configurable author. - NEW: zonefile_test.go -- 17 tests covering load/lookup/mutate/bump/ write paths. - REWRITTEN: plugin.go -- ServeDNS is now thin: UPDATE → TSIG → handler; everything else → Next. No synthetic SOA/NS, no query serving. - REWRITTEN: update.go -- handleUpdate now opens the zoneFile, loads, applies (with prereq checks against the loaded RRs), bumps serial, writes, commits. Detects no-op updates to avoid spurious file writes. - REWRITTEN: setup.go -- new directives: `zones-dir` (required), `auto-commit` (default true), `git-author <name> <email>`. Dropped `nameserver` and `persist`. Validates each declared zone has a file on disk via os.Stat before CoreDNS finishes starting. - REWRITTEN: setup_test.go -- 17 cases for the new grammar. - REWRITTEN: update_test.go -- 11 cases using real temp zone files via t.TempDir(). Total: 30 tests passing, 0 failures. Next: Phase 2c (custom CoreDNS image, deploy, smoke test with nsupdate).
This commit is contained in:
parent
1d2d919728
commit
0f28127284
10 changed files with 1223 additions and 1109 deletions
126
setup.go
126
setup.go
|
|
@ -2,13 +2,15 @@ package rfc2136
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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]`.
|
||||
|
|
@ -19,25 +21,24 @@ func init() {
|
|||
}
|
||||
|
||||
// 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.
|
||||
// `rfc2136` directive. It parses the directive, validates that each
|
||||
// declared zone has a corresponding file in zones-dir, registers
|
||||
// TSIG keys with the underlying dns.Server, and links the handler
|
||||
// into the plugin chain.
|
||||
func setup(c *caddy.Controller) error {
|
||||
p, err := parse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("rfc2136", err)
|
||||
}
|
||||
if err := p.validateZoneFiles(); err != nil {
|
||||
return plugin.Error("rfc2136", err)
|
||||
}
|
||||
|
||||
cfg := dnsserver.GetConfig(c)
|
||||
|
||||
// Register our TSIG keys with the underlying dns.Server so miekg/dns
|
||||
// Register TSIG keys with the underlying dns.Server so miekg/dns
|
||||
// auto-verifies incoming signatures. We then just inspect the
|
||||
// verification result via dns.ResponseWriter.TsigStatus() in our
|
||||
// UPDATE handler — no need to do MAC arithmetic ourselves.
|
||||
//
|
||||
// dns.Server.TsigSecret expects base64-encoded secrets, so we
|
||||
// re-encode (the parser decoded them at Corefile-load time, and
|
||||
// keeping the raw bytes lets future code do other things with
|
||||
// them).
|
||||
// result via dns.ResponseWriter.TsigStatus() in our UPDATE handler.
|
||||
if len(p.TSIGKeys) > 0 {
|
||||
if cfg.TsigSecret == nil {
|
||||
cfg.TsigSecret = make(map[string]string)
|
||||
|
|
@ -52,38 +53,37 @@ func setup(c *caddy.Controller) error {
|
|||
return p
|
||||
})
|
||||
|
||||
log.Infof("registered for zones=%v keys=%d ttl=%d persist=%q",
|
||||
p.Zones, len(p.TSIGKeys), p.TTL, p.PersistPath)
|
||||
log.Infof("ready: zones=%v keys=%d ttl=%d dir=%q auto-commit=%t",
|
||||
p.Zones, len(p.TSIGKeys), p.TTL, p.ZonesDir, p.AutoCommit)
|
||||
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).
|
||||
// parse reads a single `rfc2136 <zone> [<zone>...] { ... }` block.
|
||||
//
|
||||
// Grammar:
|
||||
//
|
||||
// rfc2136 <zone> [<zone>...] {
|
||||
// zones-dir <path> ; required
|
||||
// tsig-key <name> <algorithm> <base64-secret> ; may repeat
|
||||
// ttl <seconds> ; default 60
|
||||
// persist <path> ; default off (in-memory only)
|
||||
// auto-commit <true|false> ; default true
|
||||
// git-author <name> <email> ; optional
|
||||
// }
|
||||
func parse(c *caddy.Controller) (*RFC2136, error) {
|
||||
p := &RFC2136{
|
||||
TSIGKeys: make(map[string]tsigKey),
|
||||
TTL: DefaultTTL,
|
||||
store: newStore(),
|
||||
TSIGKeys: make(map[string]tsigKey),
|
||||
TTL: DefaultTTL,
|
||||
AutoCommit: true,
|
||||
}
|
||||
|
||||
// Per-zone git author overrides. Defaults are applied later.
|
||||
var gitAuthorName, gitAuthorEmail string
|
||||
|
||||
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()...)
|
||||
}
|
||||
|
|
@ -91,15 +91,14 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
|
|||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
|
||||
case "nameserver":
|
||||
nArgs := c.RemainingArgs()
|
||||
if len(nArgs) != 1 {
|
||||
case "zones-dir":
|
||||
dArgs := c.RemainingArgs()
|
||||
if len(dArgs) != 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.Nameserver = dns.Fqdn(nArgs[0])
|
||||
p.ZonesDir = dArgs[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))
|
||||
|
|
@ -127,17 +126,29 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
|
|||
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 {
|
||||
case "auto-commit":
|
||||
aArgs := c.RemainingArgs()
|
||||
if len(aArgs) != 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.PersistPath = pArgs[0]
|
||||
switch aArgs[0] {
|
||||
case "true", "yes", "on":
|
||||
p.AutoCommit = true
|
||||
case "false", "no", "off":
|
||||
p.AutoCommit = false
|
||||
default:
|
||||
return nil, c.Errf("auto-commit must be true|false, got %q", aArgs[0])
|
||||
}
|
||||
|
||||
case "git-author":
|
||||
gArgs := c.RemainingArgs()
|
||||
if len(gArgs) != 2 {
|
||||
return nil, c.Errf("git-author requires 2 args (name email), got %d", len(gArgs))
|
||||
}
|
||||
gitAuthorName = gArgs[0]
|
||||
gitAuthorEmail = gArgs[1]
|
||||
|
||||
default:
|
||||
return nil, c.Errf("unknown directive: %s", c.Val())
|
||||
|
|
@ -148,14 +159,45 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
|
|||
if len(p.Zones) == 0 {
|
||||
return nil, c.Err("at least one zone must be specified")
|
||||
}
|
||||
if p.ZonesDir == "" {
|
||||
return nil, c.Err("zones-dir is required")
|
||||
}
|
||||
|
||||
// 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]
|
||||
// Build zoneFile handles for each declared zone.
|
||||
p.zones = make(map[string]*zoneFile, len(p.Zones))
|
||||
for _, z := range p.Zones {
|
||||
// Trailing dot → filename. supported.systems. → supported.systems.zone
|
||||
stem := z
|
||||
if l := len(stem); l > 0 && stem[l-1] == '.' {
|
||||
stem = stem[:l-1]
|
||||
}
|
||||
path := filepath.Join(p.ZonesDir, stem+".zone")
|
||||
zf := openZoneFile(path, z)
|
||||
zf.AutoCommit = p.AutoCommit
|
||||
if gitAuthorName != "" {
|
||||
zf.GitAuthorName = gitAuthorName
|
||||
}
|
||||
if gitAuthorEmail != "" {
|
||||
zf.GitAuthorEmail = gitAuthorEmail
|
||||
}
|
||||
p.zones[z] = zf
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// validateZoneFiles ensures every configured zone has an accessible
|
||||
// file on disk at the expected path. Catches typos at CoreDNS startup
|
||||
// rather than the first UPDATE.
|
||||
func (p *RFC2136) validateZoneFiles() error {
|
||||
for zone, zf := range p.zones {
|
||||
st, err := os.Stat(zf.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("zone %q: file not accessible at %s: %w", zone, zf.Path, err)
|
||||
}
|
||||
if st.IsDir() {
|
||||
return fmt.Errorf("zone %q: %s is a directory, expected a regular file", zone, zf.Path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue