coredns-rfc2136/update.go

282 lines
8.5 KiB
Go
Raw Normal View History

Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
package rfc2136
import (
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).
2026-05-21 11:26:50 -06:00
"fmt"
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
"strings"
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).
2026-05-21 11:26:50 -06:00
"time"
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
"github.com/miekg/dns"
)
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).
2026-05-21 11:26:50 -06:00
// handleUpdate implements the RFC 2136 UPDATE opcode against the
// on-disk zone file.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
//
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).
2026-05-21 11:26:50 -06:00
// Sequence per UPDATE message:
// 1. Validate the Zone section (RFC 2136 §2.3): must be exactly one
// SOA-typed record whose name is a zone we manage.
// 2. Acquire the zone file's mutex.
// 3. Load the file's RRs into memory.
// 4. Check each prerequisite (§3.2) against the loaded RRs. First
// failure short-circuits with the spec's rcode.
// 5. Apply each update RR (§3.4.2) to the in-memory slice.
// 6. Bump the SOA serial (CalVer YYYYMMDDNN).
// 7. Atomic write to disk (temp file + rename).
// 8. Optionally `git add && git commit` for audit trail.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
//
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).
2026-05-21 11:26:50 -06:00
// Steps 3-7 happen under the zone-file mutex. If 8 fails we log but
// don't roll back (the on-disk state is authoritative; lost commits
// can be re-staged via `git add` later).
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
func (p *RFC2136) handleUpdate(w dns.ResponseWriter, r *dns.Msg) (int, error) {
resp := new(dns.Msg)
resp.SetReply(r)
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).
2026-05-21 11:26:50 -06:00
// 1. Validate the Zone section.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
if len(r.Question) != 1 {
log.Debugf("UPDATE rejected: expected 1 Zone record, got %d", len(r.Question))
return p.updateResp(w, resp, dns.RcodeFormatError)
}
zoneQ := r.Question[0]
if zoneQ.Qtype != dns.TypeSOA {
log.Debugf("UPDATE rejected: Zone section type=%d, want SOA", zoneQ.Qtype)
return p.updateResp(w, resp, dns.RcodeFormatError)
}
zone := p.findZone(zoneQ.Name)
if zone == "" {
log.Debugf("UPDATE rejected: zone %q not authoritative", zoneQ.Name)
return p.updateResp(w, resp, dns.RcodeNotAuth)
}
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).
2026-05-21 11:26:50 -06:00
zf, ok := p.zones[zone]
if !ok {
log.Errorf("UPDATE rejected: no zone file handle for %q (setup bug?)", zone)
return p.updateResp(w, resp, dns.RcodeServerFailure)
}
zf.mu.Lock()
defer zf.mu.Unlock()
// 3. Load the current zone contents.
rrs, err := zf.loadRRs()
if err != nil {
log.Errorf("UPDATE failed: %v", err)
return p.updateResp(w, resp, dns.RcodeServerFailure)
}
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
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).
2026-05-21 11:26:50 -06:00
// 4. Check prerequisites.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
for _, rr := range r.Answer {
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).
2026-05-21 11:26:50 -06:00
rcode := checkPrereq(zone, rrs, rr)
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
if rcode != dns.RcodeSuccess {
log.Debugf("UPDATE prereq failed: %s → rcode=%d", rr.String(), rcode)
return p.updateResp(w, resp, rcode)
}
}
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).
2026-05-21 11:26:50 -06:00
// 5. Apply updates. Build a fresh RR slice rather than mutating in
// place — that way a partial application can't leave the slice in
// a half-modified state if an early update fails.
updated := rrs
changed := false
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
for _, rr := range r.Ns {
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).
2026-05-21 11:26:50 -06:00
next, rcode, modified := applyUpdate(zone, p.TTL, updated, rr)
if rcode != dns.RcodeSuccess {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return p.updateResp(w, resp, rcode)
}
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).
2026-05-21 11:26:50 -06:00
updated = next
if modified {
changed = true
}
}
if !changed {
// UPDATE was a valid no-op (e.g. only contained adds for RRs
// that were already present, deduped away). Return NOERROR
// without rewriting the file.
return p.updateResp(w, resp, dns.RcodeSuccess)
}
// 6. Bump SOA serial.
now := time.Now()
if err := bumpSerial(updated, now); err != nil {
log.Errorf("UPDATE failed: %v", err)
return p.updateResp(w, resp, dns.RcodeServerFailure)
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
// 7. Atomic write.
if err := zf.writeAtomic(updated, now); err != nil {
log.Errorf("UPDATE write failed: %v", err)
return p.updateResp(w, resp, dns.RcodeServerFailure)
}
// 8. Auto-commit. Failure to commit isn't fatal to the UPDATE —
// the on-disk state is authoritative — but we log loudly.
msg := summarizeUpdate(zone, r.Ns)
if err := zf.commit(msg); err != nil {
log.Warningf("git auto-commit failed: %v", err)
}
log.Infof("UPDATE applied: zone=%s prereqs=%d updates=%d msg=%q",
zone, len(r.Answer), len(r.Ns), msg)
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return p.updateResp(w, resp, dns.RcodeSuccess)
}
// updateResp writes the response and returns the rcode/err pair for ServeDNS.
func (p *RFC2136) updateResp(w dns.ResponseWriter, resp *dns.Msg, rcode int) (int, error) {
resp.Rcode = rcode
_ = w.WriteMsg(resp)
return rcode, nil
}
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).
2026-05-21 11:26:50 -06:00
// findZone returns the longest matching configured zone for qname, or
// "" if qname is outside all configured zones.
func (p *RFC2136) findZone(qname string) string {
qname = canon(qname)
var best string
for _, z := range p.Zones {
if qname == z || strings.HasSuffix(qname, "."+z) {
if len(z) > len(best) {
best = z
}
}
}
return best
}
// checkPrereq evaluates one record from the Prerequisite section
// against the loaded RR slice. Returns dns.RcodeSuccess if satisfied,
// or the spec rcode otherwise (§3.2).
func checkPrereq(zone string, rrs []dns.RR, rr dns.RR) int {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
hdr := rr.Header()
name := canon(hdr.Name)
if !inZone(name, zone) {
return dns.RcodeNotZone
}
switch hdr.Class {
case dns.ClassANY:
if hdr.Rrtype == dns.TypeANY {
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).
2026-05-21 11:26:50 -06:00
if !nameExistsIn(rrs, name) {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return dns.RcodeNameError
}
return dns.RcodeSuccess
}
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).
2026-05-21 11:26:50 -06:00
if len(lookupIn(rrs, name, hdr.Rrtype)) == 0 {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return dns.RcodeNXRrset
}
return dns.RcodeSuccess
case dns.ClassNONE:
if hdr.Rrtype == dns.TypeANY {
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).
2026-05-21 11:26:50 -06:00
if nameExistsIn(rrs, name) {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return dns.RcodeYXDomain
}
return dns.RcodeSuccess
}
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).
2026-05-21 11:26:50 -06:00
if len(lookupIn(rrs, name, hdr.Rrtype)) > 0 {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return dns.RcodeYXRrset
}
return dns.RcodeSuccess
default:
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).
2026-05-21 11:26:50 -06:00
// CLASS = zone class with rdata. Exact value-match prereqs
// (§3.2.5). Not used by Caddy/caddy-dns/rfc2136; treating as
// satisfied for now. v2 can implement value-prereq if a real
// caller needs it.
log.Debugf("prereq with rdata-match semantics not implemented; treating as satisfied")
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
return dns.RcodeSuccess
}
}
// applyUpdate handles one record in the Update section per §3.4.2.
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).
2026-05-21 11:26:50 -06:00
// Returns the (possibly mutated) RR slice, an rcode (Success unless
// the update was rejected), and a flag indicating whether the slice
// was actually modified (to avoid no-op file rewrites).
func applyUpdate(zone string, defaultTTL uint32, rrs []dns.RR, rr dns.RR) ([]dns.RR, int, bool) {
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
hdr := rr.Header()
name := canon(hdr.Name)
if !inZone(name, zone) {
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).
2026-05-21 11:26:50 -06:00
return rrs, dns.RcodeNotZone, false
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
switch hdr.Class {
case dns.ClassANY:
if hdr.Rrtype == dns.TypeANY {
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).
2026-05-21 11:26:50 -06:00
// Wipe the whole name. Refuse apex wipes — that would
// destroy SOA + NS bedrock.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
if isApex(name, zone) {
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).
2026-05-21 11:26:50 -06:00
log.Debugf("apex wipe refused: %s", name)
return rrs, dns.RcodeRefused, false
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
before := len(rrs)
rrs = removeNameFrom(rrs, name)
return rrs, dns.RcodeSuccess, len(rrs) != before
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
// Apex SOA/NS removal refused for the same reason.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
if isApex(name, zone) && (hdr.Rrtype == dns.TypeSOA || hdr.Rrtype == dns.TypeNS) {
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).
2026-05-21 11:26:50 -06:00
log.Debugf("apex %s removal refused", dns.TypeToString[hdr.Rrtype])
return rrs, dns.RcodeRefused, false
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
before := len(rrs)
rrs = removeRRsetFrom(rrs, name, hdr.Rrtype)
return rrs, dns.RcodeSuccess, len(rrs) != before
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
case dns.ClassNONE:
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).
2026-05-21 11:26:50 -06:00
// Refuse to delete apex SOA/NS by exact-RR match.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
if isApex(name, zone) && (hdr.Rrtype == dns.TypeSOA || hdr.Rrtype == dns.TypeNS) {
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).
2026-05-21 11:26:50 -06:00
return rrs, dns.RcodeRefused, false
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
before := len(rrs)
rrs = removeRRFrom(rrs, rr)
return rrs, dns.RcodeSuccess, len(rrs) != before
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
default:
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).
2026-05-21 11:26:50 -06:00
// Apex SOA/NS adds refused — those are managed by the zone-file
// owner, not by dynamic updates.
if isApex(name, zone) && (hdr.Rrtype == dns.TypeSOA || hdr.Rrtype == dns.TypeNS) {
log.Debugf("apex %s add refused", dns.TypeToString[hdr.Rrtype])
return rrs, dns.RcodeRefused, false
}
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
if hdr.Ttl == 0 {
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).
2026-05-21 11:26:50 -06:00
hdr.Ttl = defaultTTL
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
before := len(rrs)
rrs = addRRTo(rrs, rr)
return rrs, dns.RcodeSuccess, len(rrs) != before
}
}
// summarizeUpdate produces a one-line commit message describing the
// UPDATE for git history.
func summarizeUpdate(zone string, updates []dns.RR) string {
if len(updates) == 1 {
return fmt.Sprintf("rfc2136 %s: %s", zone, oneLineOp(updates[0]))
}
return fmt.Sprintf("rfc2136 %s: %d operations", zone, len(updates))
}
// oneLineOp returns a short human-readable description of a single
// update RR for inclusion in commit messages.
func oneLineOp(rr dns.RR) string {
hdr := rr.Header()
name := strings.TrimSuffix(canon(hdr.Name), ".")
ttype := dns.TypeToString[hdr.Rrtype]
switch hdr.Class {
case dns.ClassANY:
if hdr.Rrtype == dns.TypeANY {
return fmt.Sprintf("delete all %s", name)
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
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).
2026-05-21 11:26:50 -06:00
return fmt.Sprintf("delete %s %s", ttype, name)
case dns.ClassNONE:
return fmt.Sprintf("delete-rr %s %s", ttype, name)
default:
return fmt.Sprintf("add %s %s", ttype, name)
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
}
}
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).
2026-05-21 11:26:50 -06:00
// inZone reports whether name is within zone.
Phase 1.4: UPDATE opcode handler + TSIG verification Replaces the Phase-1.3 refuseUpdate() stub with a real RFC 2136 handler. Caddy via caddy-dns/rfc2136 can now inject and remove records. UPDATE message handling (update.go): - Zone section validation: must be exactly one SOA-typed record naming a zone we're authoritative for. Returns FORMERR/NOTAUTH otherwise. - Prerequisites (§3.2): name-exists, RRset-exists, name-NOT-exists, RRset-NOT-exists semantics implemented. First failure short-circuits with the spec's rcode (NXDOMAIN/NXRRSET/YXDOMAIN/YXRRSET). - Updates (§3.4.2): add RR, delete RRset (CLASS=ANY+RDLEN=0), delete all RRsets at name (CLASS=ANY+TYPE=ANY), delete specific RR (CLASS= NONE). - Apex SOA/NS protected: synthetic and cannot be added or removed via UPDATE. Apex wipe (TYPE=ANY at apex) also refused. - Default TTL applied to incoming records with TTL=0. TSIG (tsig.go + setup.go): - setup() now populates dnsserver.Config.TsigSecret so the underlying dns.Server auto-verifies signatures via miekg/dns. - checkTSIG() in ServeDNS gates UPDATEs: rejects if no TSIG, unknown key name, algorithm-downgrade attempt, or w.TsigStatus() != nil. - No TSIG keys configured → all UPDATEs refused (safety default). - Algorithm pinning prevents downgrade attacks (e.g. forced HMAC-MD5). Tests (update_test.go): 11 new cases covering happy paths and every error rcode. Total: 35 top-level test passes, 0 failures. ServeDNS dispatch now calls handleUpdate after auth gate. The refuseUpdate() stub is gone. UPDATE end-to-end via nsupdate requires the custom CoreDNS image (Phase 2) to verify TSIG plumbing on the dns.Server side.
2026-05-21 10:51:18 -06:00
func inZone(name, zone string) bool {
return name == zone || strings.HasSuffix(name, "."+zone)
}
// isApex reports whether name IS the zone's apex.
func isApex(name, zone string) bool {
return name == zone
}