mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 12:05:22 -04:00
* plugin/dynupdate: add authenticated RFC 2136 updates Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: fix README test fixtures Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * test: format README fixture map Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: persist updates and bound writable zones Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: preserve middleware and fix interoperability fixtures Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * test(dynupdate): validate Kea lifecycle and bounded zone costs Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: reject duplicate directives and harden client fixtures Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: fix update routing and startup validation Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> --------- Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
400 lines
11 KiB
Go
400 lines
11 KiB
Go
package dynupdate
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
|
"github.com/coredns/coredns/plugin/tsig"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
var log = clog.NewWithPlugin(pluginName)
|
|
|
|
func (d *DynUpdate) serveUpdate(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
|
if len(r.Question) != 1 || r.Question[0].Qtype != dns.TypeSOA || r.Question[0].Qclass != dns.ClassINET {
|
|
return d.reply(w, r, dns.RcodeFormatError)
|
|
}
|
|
|
|
zone := canonicalName(r.Question[0].Name)
|
|
if zone != d.Zone {
|
|
// Query-only backends can acknowledge an UPDATE without applying it.
|
|
// Writable zones must use separate server blocks, not fallthrough.
|
|
return d.reply(w, r, dns.RcodeNotAuth)
|
|
}
|
|
|
|
var key string
|
|
var ok bool
|
|
if ctx != nil {
|
|
key, ok = tsig.ValidatedKeyName(ctx)
|
|
}
|
|
if !ok {
|
|
log.Debugf("refusing UPDATE for %s without a validated TSIG identity", zone)
|
|
return d.reply(w, r, dns.RcodeRefused)
|
|
}
|
|
|
|
rcode, err := d.applyUpdate(key, r.Answer, r.Ns)
|
|
if err != nil {
|
|
log.Errorf("UPDATE for %s failed: %v", zone, err)
|
|
}
|
|
return d.reply(w, r, rcode)
|
|
}
|
|
|
|
// applyUpdate performs one complete RFC 2136 transaction. It returns only
|
|
// after either the old snapshot is unchanged or a fully built new snapshot is
|
|
// installed. The caller must not hold d.mu.
|
|
func (d *DynUpdate) applyUpdate(key string, prerequisites, updates []dns.RR) (int, error) {
|
|
d.mu.Lock()
|
|
changed, rcode, err := d.updateLocked(key, prerequisites, updates)
|
|
if changed && d.Xfer != nil {
|
|
d.notifyPending = true
|
|
if !d.notifyRunning {
|
|
d.notifyRunning = true
|
|
go d.notify()
|
|
}
|
|
}
|
|
d.mu.Unlock()
|
|
return rcode, err
|
|
}
|
|
|
|
// Coalesce bursts and keep at most one NOTIFY operation in flight per instance.
|
|
func (d *DynUpdate) notify() {
|
|
for {
|
|
d.mu.Lock()
|
|
if d.closed || !d.notifyPending {
|
|
d.notifyRunning = false
|
|
d.mu.Unlock()
|
|
return
|
|
}
|
|
d.notifyPending = false
|
|
xfer, zone := d.Xfer, d.Zone
|
|
d.mu.Unlock()
|
|
if err := xfer.Notify(zone); err != nil {
|
|
log.Warningf("NOTIFY for %s after UPDATE failed: %v", zone, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *DynUpdate) updateLocked(key string, prerequisites, updates []dns.RR) (bool, int, error) {
|
|
// Authenticate and identify the key before doing semantic work on the
|
|
// request. An unknown valid TSIG must not be able to probe zone state.
|
|
if !d.configuredKey(key) {
|
|
return false, dns.RcodeRefused, nil
|
|
}
|
|
bound := d.limits.defaults()
|
|
if len(prerequisites) > bound.updateRecords || len(updates) > bound.updateRecords-len(prerequisites) {
|
|
return false, dns.RcodeRefused, nil
|
|
}
|
|
if err := d.ensureStore(); err != nil {
|
|
return false, dns.RcodeServerFailure, err
|
|
}
|
|
if d.store != nil {
|
|
d.store.mu.Lock()
|
|
defer d.store.mu.Unlock()
|
|
d.records = d.store.records
|
|
}
|
|
// RFC 2136 evaluates prerequisites against the current snapshot before
|
|
// checking permissions and prescanning the Update section. Keeping this
|
|
// order matters when a request contains both a failed prerequisite and an
|
|
// invalid update record.
|
|
if rcode := d.checkPrerequisites(prerequisites); rcode != dns.RcodeSuccess {
|
|
return false, rcode, nil
|
|
}
|
|
if rcode := d.authorize(key, updates); rcode != dns.RcodeSuccess {
|
|
return false, rcode, nil
|
|
}
|
|
if rcode := d.validateUpdates(updates); rcode != dns.RcodeSuccess {
|
|
return false, rcode, nil
|
|
}
|
|
|
|
candidate, changed, explicitSOA := d.apply(updates)
|
|
if !changed {
|
|
return false, dns.RcodeSuccess, nil
|
|
}
|
|
if err := d.limits.check(candidate); err != nil {
|
|
return false, dns.RcodeRefused, err
|
|
}
|
|
if !explicitSOA {
|
|
bumpSerial(candidate)
|
|
}
|
|
view, err := d.build(candidate)
|
|
if err != nil {
|
|
return false, dns.RcodeServerFailure, fmt.Errorf("building candidate zone: %w", err)
|
|
}
|
|
if d.store != nil {
|
|
if err := d.store.commit(candidate, view); err != nil {
|
|
return false, dns.RcodeServerFailure, fmt.Errorf("committing zone: %w", err)
|
|
}
|
|
}
|
|
d.install(candidate, view)
|
|
return true, dns.RcodeSuccess, nil
|
|
}
|
|
|
|
func (d *DynUpdate) checkPrerequisites(prerequisites []dns.RR) int {
|
|
valueDependent := make(map[rrsetKey][]dns.RR)
|
|
for _, rr := range prerequisites {
|
|
if rr == nil {
|
|
return dns.RcodeFormatError
|
|
}
|
|
h := rr.Header()
|
|
if h.Ttl != 0 {
|
|
return dns.RcodeFormatError
|
|
}
|
|
if !inZone(d.Zone, h.Name) {
|
|
return dns.RcodeNotZone
|
|
}
|
|
if unsupportedRRType(h.Rrtype) {
|
|
return dns.RcodeNotImplemented
|
|
}
|
|
|
|
switch h.Class {
|
|
case dns.ClassANY:
|
|
if h.Rdlength != 0 || !validPrerequisiteType(h.Rrtype, true) {
|
|
return dns.RcodeFormatError
|
|
}
|
|
if h.Rrtype == dns.TypeANY {
|
|
if !d.nameInUse(h.Name) {
|
|
return dns.RcodeNameError
|
|
}
|
|
} else if !d.rrsetExists(h.Name, h.Rrtype) {
|
|
return dns.RcodeNXRrset
|
|
}
|
|
|
|
case dns.ClassNONE:
|
|
if h.Rdlength != 0 || !validPrerequisiteType(h.Rrtype, true) {
|
|
return dns.RcodeFormatError
|
|
}
|
|
if h.Rrtype == dns.TypeANY {
|
|
if d.nameInUse(h.Name) {
|
|
return dns.RcodeYXDomain
|
|
}
|
|
} else if d.rrsetExists(h.Name, h.Rrtype) {
|
|
return dns.RcodeYXRrset
|
|
}
|
|
|
|
case dns.ClassINET:
|
|
if !validPrerequisiteType(h.Rrtype, false) {
|
|
return dns.RcodeFormatError
|
|
}
|
|
key := rrsetKey{name: canonicalName(h.Name), rrType: h.Rrtype}
|
|
valueDependent[key] = append(valueDependent[key], rr)
|
|
|
|
default:
|
|
return dns.RcodeFormatError
|
|
}
|
|
}
|
|
|
|
for key, want := range valueDependent {
|
|
if !sameRRset(d.rrset(key.name, key.rrType), want) {
|
|
return dns.RcodeNXRrset
|
|
}
|
|
}
|
|
return dns.RcodeSuccess
|
|
}
|
|
|
|
func validPrerequisiteType(rrType uint16, allowAny bool) bool {
|
|
if !knownRRType(rrType) || rrType == dns.TypeNone {
|
|
return false
|
|
}
|
|
if rrType == dns.TypeANY {
|
|
return allowAny
|
|
}
|
|
return !isQueryMetaType(rrType)
|
|
}
|
|
|
|
type rrsetKey struct {
|
|
name string
|
|
rrType uint16
|
|
}
|
|
|
|
func (d *DynUpdate) authorize(key string, updates []dns.RR) int {
|
|
for _, rr := range updates {
|
|
if rr == nil {
|
|
return dns.RcodeFormatError
|
|
}
|
|
if !d.allows(key, rr.Header().Name, rr.Header().Rrtype) {
|
|
return dns.RcodeRefused
|
|
}
|
|
}
|
|
return dns.RcodeSuccess
|
|
}
|
|
|
|
func (d *DynUpdate) validateUpdates(updates []dns.RR) int {
|
|
for _, rr := range updates {
|
|
if rr == nil {
|
|
return dns.RcodeFormatError
|
|
}
|
|
h := rr.Header()
|
|
if !inZone(d.Zone, h.Name) {
|
|
return dns.RcodeNotZone
|
|
}
|
|
if !knownRRType(h.Rrtype) || h.Rrtype == dns.TypeNone {
|
|
return dns.RcodeFormatError
|
|
}
|
|
|
|
switch h.Class {
|
|
case dns.ClassINET:
|
|
if isQueryMetaType(h.Rrtype) {
|
|
return dns.RcodeFormatError
|
|
}
|
|
if h.Rrtype == dns.TypeSOA {
|
|
if canonicalName(h.Name) != d.Zone {
|
|
return dns.RcodeFormatError
|
|
}
|
|
soa, ok := rr.(*dns.SOA)
|
|
// RFC 2136 sections 4.2 and 7.11 prohibit zero for
|
|
// interoperability with older DNS implementations.
|
|
if !ok || soa.Serial == 0 {
|
|
return dns.RcodeFormatError
|
|
}
|
|
}
|
|
case dns.ClassANY:
|
|
if h.Ttl != 0 || h.Rdlength != 0 || isQueryMetaType(h.Rrtype) && h.Rrtype != dns.TypeANY {
|
|
return dns.RcodeFormatError
|
|
}
|
|
case dns.ClassNONE:
|
|
if h.Ttl != 0 || h.Rrtype == dns.TypeANY || isQueryMetaType(h.Rrtype) {
|
|
return dns.RcodeFormatError
|
|
}
|
|
default:
|
|
return dns.RcodeFormatError
|
|
}
|
|
|
|
if unsupportedRRType(h.Rrtype) {
|
|
return dns.RcodeNotImplemented
|
|
}
|
|
}
|
|
|
|
return dns.RcodeSuccess
|
|
}
|
|
|
|
// unsupportedRRType identifies records whose contents become invalid when a
|
|
// different RRset is updated without regenerating its associated metadata.
|
|
// This stage deliberately fails closed instead of serving stale DNSSEC or
|
|
// zone-digest data. The list includes obsolete DNSSEC types because they are
|
|
// still representable by miekg/dns and can otherwise enter a seed zone.
|
|
func unsupportedRRType(rrType uint16) bool {
|
|
switch rrType {
|
|
case dns.TypeSIG, dns.TypeKEY, dns.TypeNXT,
|
|
dns.TypeDS, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY,
|
|
dns.TypeNSEC3, dns.TypeNSEC3PARAM,
|
|
dns.TypeTALINK, dns.TypeCDS, dns.TypeCDNSKEY,
|
|
dns.TypeZONEMD, dns.TypeTA, dns.TypeDLV:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// apply follows RFC 2136 section 3.4.2 against a private copy. The boolean
|
|
// explicitSOA reports whether an accepted SOA update supplied the new serial;
|
|
// otherwise the server increments the serial after any real change.
|
|
func (d *DynUpdate) apply(updates []dns.RR) ([]dns.RR, bool, bool) {
|
|
records := cloneRecords(d.records)
|
|
changed := false
|
|
explicitSOA := false
|
|
|
|
for _, rr := range updates {
|
|
h := rr.Header()
|
|
name := canonicalName(h.Name)
|
|
apex := name == d.Zone
|
|
|
|
switch h.Class {
|
|
case dns.ClassINET:
|
|
switch h.Rrtype {
|
|
case dns.TypeSOA:
|
|
current := soaAt(records, d.Zone)
|
|
incoming, ok := rr.(*dns.SOA)
|
|
if !ok || current == nil || !serialGreater(incoming.Serial, current.Serial) {
|
|
continue
|
|
}
|
|
records, _ = removeRecords(records, func(existing dns.RR) bool {
|
|
return canonicalName(existing.Header().Name) == name && existing.Header().Rrtype == dns.TypeSOA
|
|
})
|
|
records = append(records, dns.Copy(rr))
|
|
changed = true
|
|
explicitSOA = true
|
|
|
|
case dns.TypeCNAME:
|
|
if hasOtherData(records, name) {
|
|
continue
|
|
}
|
|
existing := rrsetOf(records, name, dns.TypeCNAME)
|
|
if len(existing) == 1 && sameRR(existing[0], rr) && existing[0].Header().Ttl == h.Ttl {
|
|
continue
|
|
}
|
|
records, _ = removeRecords(records, func(existing dns.RR) bool {
|
|
return canonicalName(existing.Header().Name) == name && existing.Header().Rrtype == dns.TypeCNAME
|
|
})
|
|
records = append(records, dns.Copy(rr))
|
|
changed = true
|
|
|
|
default:
|
|
if hasCNAME(records, name) && !cnameCompatibleType(h.Rrtype) {
|
|
continue
|
|
}
|
|
if index := findRR(records, rr); index >= 0 {
|
|
if records[index].Header().Ttl != h.Ttl {
|
|
records[index] = dns.Copy(rr)
|
|
changed = true
|
|
}
|
|
continue
|
|
}
|
|
records = append(records, dns.Copy(rr))
|
|
changed = true
|
|
}
|
|
|
|
case dns.ClassANY:
|
|
if h.Rrtype == dns.TypeANY {
|
|
var removed bool
|
|
records, removed = removeRecords(records, func(existing dns.RR) bool {
|
|
if canonicalName(existing.Header().Name) != name {
|
|
return false
|
|
}
|
|
if apex && (existing.Header().Rrtype == dns.TypeSOA || existing.Header().Rrtype == dns.TypeNS) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
changed = changed || removed
|
|
continue
|
|
}
|
|
if apex && (h.Rrtype == dns.TypeSOA || h.Rrtype == dns.TypeNS) {
|
|
continue
|
|
}
|
|
var removed bool
|
|
records, removed = removeRecords(records, func(existing dns.RR) bool {
|
|
return canonicalName(existing.Header().Name) == name && existing.Header().Rrtype == h.Rrtype
|
|
})
|
|
changed = changed || removed
|
|
|
|
case dns.ClassNONE:
|
|
if apex && h.Rrtype == dns.TypeSOA {
|
|
continue
|
|
}
|
|
if apex && h.Rrtype == dns.TypeNS && countRRset(records, name, dns.TypeNS) <= 1 {
|
|
continue
|
|
}
|
|
if index := findRR(records, rr); index >= 0 {
|
|
records = append(records[:index], records[index+1:]...)
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return records, changed, explicitSOA
|
|
}
|
|
|
|
func (d *DynUpdate) reply(w dns.ResponseWriter, r *dns.Msg, rcode int) (int, error) {
|
|
m := new(dns.Msg)
|
|
m.SetReply(r)
|
|
m.Opcode = dns.OpcodeUpdate
|
|
m.Rcode = rcode
|
|
m.Authoritative = true
|
|
if err := w.WriteMsg(m); err != nil {
|
|
return dns.RcodeServerFailure, err
|
|
}
|
|
return dns.RcodeSuccess, nil
|
|
}
|