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.
162 lines
4.6 KiB
Go
162 lines
4.6 KiB
Go
package rfc2136
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strconv"
|
|
|
|
"github.com/coredns/caddy"
|
|
"github.com/coredns/coredns/core/dnsserver"
|
|
"github.com/coredns/coredns/plugin"
|
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// log is the package logger, scoped so messages are prefixed `[rfc2136]`.
|
|
var log = clog.NewWithPlugin("rfc2136")
|
|
|
|
func init() {
|
|
plugin.Register("rfc2136", setup)
|
|
}
|
|
|
|
// setup is invoked by the CoreDNS plugin registry once per Corefile
|
|
// `rfc2136` directive. It parses the directive's arguments and block,
|
|
// constructs an RFC2136 handler, and links it into the plugin chain.
|
|
func setup(c *caddy.Controller) error {
|
|
p, err := parse(c)
|
|
if err != nil {
|
|
return plugin.Error("rfc2136", err)
|
|
}
|
|
|
|
cfg := dnsserver.GetConfig(c)
|
|
|
|
// Register our TSIG keys with the underlying dns.Server so miekg/dns
|
|
// auto-verifies incoming signatures. We then just inspect the
|
|
// verification result via dns.ResponseWriter.TsigStatus() in our
|
|
// UPDATE handler — no need to do MAC arithmetic ourselves.
|
|
//
|
|
// dns.Server.TsigSecret expects base64-encoded secrets, so we
|
|
// re-encode (the parser decoded them at Corefile-load time, and
|
|
// keeping the raw bytes lets future code do other things with
|
|
// them).
|
|
if len(p.TSIGKeys) > 0 {
|
|
if cfg.TsigSecret == nil {
|
|
cfg.TsigSecret = make(map[string]string)
|
|
}
|
|
for name, key := range p.TSIGKeys {
|
|
cfg.TsigSecret[name] = base64.StdEncoding.EncodeToString(key.Secret)
|
|
}
|
|
}
|
|
|
|
cfg.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
|
p.Next = next
|
|
return p
|
|
})
|
|
|
|
log.Infof("registered for zones=%v keys=%d ttl=%d persist=%q",
|
|
p.Zones, len(p.TSIGKeys), p.TTL, p.PersistPath)
|
|
return nil
|
|
}
|
|
|
|
// parse reads a single `rfc2136 <zone> [<zone>...] { ... }` block from
|
|
// the Corefile and returns a fully-populated RFC2136 handler with all
|
|
// values validated at parse time (so configuration errors fail fast at
|
|
// CoreDNS startup, not later mid-request).
|
|
//
|
|
// Grammar:
|
|
//
|
|
// rfc2136 <zone> [<zone>...] {
|
|
// tsig-key <name> <algorithm> <base64-secret> ; may repeat
|
|
// ttl <seconds> ; default 60
|
|
// persist <path> ; default off (in-memory only)
|
|
// }
|
|
func parse(c *caddy.Controller) (*RFC2136, error) {
|
|
p := &RFC2136{
|
|
TSIGKeys: make(map[string]tsigKey),
|
|
TTL: DefaultTTL,
|
|
store: newStore(),
|
|
}
|
|
|
|
for c.Next() {
|
|
args := c.RemainingArgs()
|
|
if len(args) < 1 {
|
|
return nil, c.ArgErr()
|
|
}
|
|
// Normalize each declared zone to lowercase + trailing dot
|
|
// (CoreDNS canonical form). This makes later zone-membership
|
|
// checks an exact match against r.Question[0].Name.
|
|
for _, z := range args {
|
|
p.Zones = append(p.Zones, plugin.Host(z).NormalizeExact()...)
|
|
}
|
|
|
|
for c.NextBlock() {
|
|
switch c.Val() {
|
|
|
|
case "nameserver":
|
|
nArgs := c.RemainingArgs()
|
|
if len(nArgs) != 1 {
|
|
return nil, c.ArgErr()
|
|
}
|
|
p.Nameserver = dns.Fqdn(nArgs[0])
|
|
|
|
case "tsig-key":
|
|
// tsig-key <name> <algorithm> <base64-secret>
|
|
kArgs := c.RemainingArgs()
|
|
if len(kArgs) != 3 {
|
|
return nil, c.Errf("tsig-key requires 3 args (name algorithm secret), got %d", len(kArgs))
|
|
}
|
|
keyName := canonicalKeyName(kArgs[0])
|
|
algo, err := parseTSIGAlgorithm(kArgs[1])
|
|
if err != nil {
|
|
return nil, c.Err(err.Error())
|
|
}
|
|
secret, err := decodeTSIGSecret(kArgs[2])
|
|
if err != nil {
|
|
return nil, c.Errf("tsig-key %q: %v", keyName, err)
|
|
}
|
|
if _, exists := p.TSIGKeys[keyName]; exists {
|
|
return nil, c.Errf("duplicate tsig-key %q", keyName)
|
|
}
|
|
p.TSIGKeys[keyName] = tsigKey{Algorithm: algo, Secret: secret}
|
|
|
|
case "ttl":
|
|
tArgs := c.RemainingArgs()
|
|
if len(tArgs) != 1 {
|
|
return nil, c.ArgErr()
|
|
}
|
|
ttl, err := strconv.ParseUint(tArgs[0], 10, 32)
|
|
if err != nil {
|
|
return nil, c.Errf("ttl must be a non-negative integer: %v", err)
|
|
}
|
|
// Anything over a week is almost certainly a mistake
|
|
// for ACME challenge records, but allow up to the
|
|
// uint32 max so we don't ship an arbitrary cap.
|
|
p.TTL = uint32(ttl)
|
|
|
|
case "persist":
|
|
pArgs := c.RemainingArgs()
|
|
if len(pArgs) != 1 {
|
|
return nil, c.ArgErr()
|
|
}
|
|
p.PersistPath = pArgs[0]
|
|
|
|
default:
|
|
return nil, c.Errf("unknown directive: %s", c.Val())
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(p.Zones) == 0 {
|
|
return nil, c.Err("at least one zone must be specified")
|
|
}
|
|
|
|
// Default nameserver to the first zone apex. The user can override
|
|
// via the `nameserver` directive — e.g. when the delegating parent
|
|
// zone publishes `auth NS dns.supported.systems`, this should be
|
|
// set to `dns.supported.systems.` to match.
|
|
if p.Nameserver == "" {
|
|
p.Nameserver = p.Zones[0]
|
|
}
|
|
|
|
return p, nil
|
|
}
|