mirror of
https://github.com/coredns/coredns.git
synced 2026-10-16 07:20:43 -04:00
plugin/dynupdate: add durable authenticated RFC 2136 updates (#8520)
* 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>
This commit is contained in:
323
plugin/dynupdate/zone.go
Normal file
323
plugin/dynupdate/zone.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package dynupdate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
allNames = "*"
|
||||
allTypes = "*"
|
||||
)
|
||||
|
||||
var errMissingSOA = errors.New("zone has no SOA")
|
||||
|
||||
func validateRecords(records []dns.RR, origin string) error {
|
||||
origin = canonicalName(origin)
|
||||
soaCount := 0
|
||||
nameTypes := make(map[string]map[uint16]struct{})
|
||||
for _, rr := range records {
|
||||
if rr == nil {
|
||||
return errors.New("zone contains a nil record")
|
||||
}
|
||||
h := rr.Header()
|
||||
if h.Class != dns.ClassINET {
|
||||
return errors.New("zone contains a non-IN record")
|
||||
}
|
||||
if !inZone(origin, h.Name) {
|
||||
return errors.New("zone contains an out-of-zone record")
|
||||
}
|
||||
if unsupportedRRType(h.Rrtype) {
|
||||
return fmt.Errorf("zone contains unsupported RR type %s", dns.TypeToString[h.Rrtype])
|
||||
}
|
||||
name := canonicalName(h.Name)
|
||||
types := nameTypes[name]
|
||||
if types == nil {
|
||||
types = make(map[uint16]struct{})
|
||||
nameTypes[name] = types
|
||||
}
|
||||
if h.Rrtype == dns.TypeCNAME && len(types) != 0 {
|
||||
return fmt.Errorf("zone contains CNAME data conflict at %s", h.Name)
|
||||
}
|
||||
if h.Rrtype != dns.TypeCNAME {
|
||||
if _, exists := types[dns.TypeCNAME]; exists {
|
||||
return fmt.Errorf("zone contains CNAME data conflict at %s", h.Name)
|
||||
}
|
||||
}
|
||||
types[h.Rrtype] = struct{}{}
|
||||
if h.Rrtype == dns.TypeSOA {
|
||||
soa, ok := rr.(*dns.SOA)
|
||||
if !ok {
|
||||
return errors.New("zone contains an invalid SOA record")
|
||||
}
|
||||
if canonicalName(h.Name) != origin {
|
||||
return errors.New("zone contains a non-apex SOA record")
|
||||
}
|
||||
// RFC 2136 sections 4.2 and 7.11 prohibit zero for
|
||||
// interoperability with older DNS implementations.
|
||||
if soa.Serial == 0 {
|
||||
return errors.New("zone contains an SOA with serial zero")
|
||||
}
|
||||
soaCount++
|
||||
}
|
||||
}
|
||||
if soaCount == 0 {
|
||||
return errMissingSOA
|
||||
}
|
||||
if soaCount != 1 {
|
||||
return errors.New("zone contains more than one SOA record")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type permission struct {
|
||||
key string
|
||||
name string
|
||||
types map[uint16]struct{}
|
||||
allTypes bool
|
||||
}
|
||||
|
||||
func canonicalName(name string) string {
|
||||
return strings.ToLower(dns.CanonicalName(name))
|
||||
}
|
||||
|
||||
func inZone(origin, name string) bool {
|
||||
name = canonicalName(name)
|
||||
return name == origin || dns.IsSubDomain(origin, name)
|
||||
}
|
||||
|
||||
func (d *DynUpdate) configuredKey(key string) bool {
|
||||
key = canonicalName(key)
|
||||
for _, p := range d.permissions {
|
||||
if p.key == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DynUpdate) allows(key, name string, rrType uint16) bool {
|
||||
key = canonicalName(key)
|
||||
name = canonicalName(name)
|
||||
for _, p := range d.permissions {
|
||||
if p.key != key || (p.name != allNames && p.name != name) {
|
||||
continue
|
||||
}
|
||||
if p.allTypes {
|
||||
return true
|
||||
}
|
||||
if _, ok := p.types[rrType]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DynUpdate) nameInUse(name string) bool {
|
||||
name = canonicalName(name)
|
||||
for _, rr := range d.records {
|
||||
if canonicalName(rr.Header().Name) == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DynUpdate) rrset(name string, rrType uint16) []dns.RR {
|
||||
return rrsetOf(d.records, name, rrType)
|
||||
}
|
||||
|
||||
func rrsetOf(records []dns.RR, name string, rrType uint16) []dns.RR {
|
||||
name = canonicalName(name)
|
||||
var set []dns.RR
|
||||
for _, rr := range records {
|
||||
h := rr.Header()
|
||||
if canonicalName(h.Name) == name && h.Rrtype == rrType {
|
||||
set = append(set, rr)
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (d *DynUpdate) rrsetExists(name string, rrType uint16) bool {
|
||||
return len(d.rrset(name, rrType)) != 0
|
||||
}
|
||||
|
||||
func soaOf(records []dns.RR) *dns.SOA {
|
||||
for _, rr := range records {
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
return soa
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func soaAt(records []dns.RR, name string) *dns.SOA {
|
||||
name = canonicalName(name)
|
||||
for _, rr := range records {
|
||||
soa, ok := rr.(*dns.SOA)
|
||||
if ok && canonicalName(soa.Header().Name) == name {
|
||||
return soa
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneRecords(records []dns.RR) []dns.RR {
|
||||
cloned := make([]dns.RR, len(records))
|
||||
for i, rr := range records {
|
||||
cloned[i] = dns.Copy(rr)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func sameRR(a, b dns.RR) bool {
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// dns.IsDuplicate implements miekg/dns's generated, type-aware RDATA
|
||||
// comparison and deliberately ignores TTL. UPDATE delete records use
|
||||
// CLASS NONE on the wire, while the corresponding zone RR has the zone
|
||||
// class, so normalize both classes before comparing.
|
||||
left, right := dns.Copy(a), dns.Copy(b)
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
left.Header().Class = dns.ClassINET
|
||||
right.Header().Class = dns.ClassINET
|
||||
return dns.IsDuplicate(left, right)
|
||||
}
|
||||
|
||||
func sameRRset(have, want []dns.RR) bool {
|
||||
// Treat each section as a set. Zone files and UPDATE messages should not
|
||||
// contain duplicate RRs, but ignoring duplicates here follows the RFC's
|
||||
// RRset semantics and avoids making comparison depend on wire ordering.
|
||||
have = uniqueRecords(have)
|
||||
want = uniqueRecords(want)
|
||||
if len(have) != len(want) {
|
||||
return false
|
||||
}
|
||||
used := make([]bool, len(have))
|
||||
for _, wanted := range want {
|
||||
found := false
|
||||
for i, actual := range have {
|
||||
if !used[i] && sameRR(actual, wanted) {
|
||||
used[i] = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func uniqueRecords(records []dns.RR) []dns.RR {
|
||||
unique := make([]dns.RR, 0, len(records))
|
||||
for _, rr := range records {
|
||||
duplicate := false
|
||||
for _, existing := range unique {
|
||||
if sameRR(existing, rr) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicate {
|
||||
unique = append(unique, rr)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
func findRR(records []dns.RR, want dns.RR) int {
|
||||
for i, rr := range records {
|
||||
if sameRR(rr, want) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func removeRecords(records []dns.RR, match func(dns.RR) bool) ([]dns.RR, bool) {
|
||||
result := make([]dns.RR, 0, len(records))
|
||||
changed := false
|
||||
for _, rr := range records {
|
||||
if match(rr) {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
result = append(result, rr)
|
||||
}
|
||||
return result, changed
|
||||
}
|
||||
|
||||
func countRRset(records []dns.RR, name string, rrType uint16) int {
|
||||
name = canonicalName(name)
|
||||
count := 0
|
||||
for _, rr := range records {
|
||||
if canonicalName(rr.Header().Name) == name && rr.Header().Rrtype == rrType {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func hasCNAME(records []dns.RR, name string) bool {
|
||||
return countRRset(records, name, dns.TypeCNAME) != 0
|
||||
}
|
||||
|
||||
func hasOtherData(records []dns.RR, name string) bool {
|
||||
name = canonicalName(name)
|
||||
for _, rr := range records {
|
||||
if canonicalName(rr.Header().Name) != name {
|
||||
continue
|
||||
}
|
||||
if !cnameCompatibleType(rr.Header().Rrtype) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cnameCompatibleType(rrType uint16) bool {
|
||||
return rrType == dns.TypeCNAME
|
||||
}
|
||||
|
||||
// serialGreater implements RFC 1982 serial arithmetic. The half-space value
|
||||
// is deliberately not considered greater because RFC 1982 leaves it
|
||||
// undefined.
|
||||
func serialGreater(a, b uint32) bool {
|
||||
return a != b && a-b < 1<<31
|
||||
}
|
||||
|
||||
func bumpSerial(records []dns.RR) {
|
||||
if soa := soaOf(records); soa != nil {
|
||||
soa.Serial++
|
||||
// RFC 2136 section 7.11 recommends that an automatically incremented
|
||||
// serial never become zero after wrapping at 2^32.
|
||||
if soa.Serial == 0 {
|
||||
soa.Serial = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func knownRRType(rrType uint16) bool {
|
||||
_, ok := dns.TypeToRR[rrType]
|
||||
return ok
|
||||
}
|
||||
|
||||
func isQueryMetaType(rrType uint16) bool {
|
||||
switch rrType {
|
||||
case dns.TypeANY, dns.TypeAXFR, dns.TypeIXFR, dns.TypeMAILA, dns.TypeMAILB,
|
||||
dns.TypeOPT, dns.TypeTKEY, dns.TypeTSIG:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user