176 lines
4.5 KiB
Go
176 lines
4.5 KiB
Go
|
|
package rfc2136
|
||
|
|
|
||
|
|
import (
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"sync/atomic"
|
||
|
|
|
||
|
|
"github.com/miekg/dns"
|
||
|
|
)
|
||
|
|
|
||
|
|
// recordStore is the in-memory store of dynamic records. It is keyed
|
||
|
|
// by canonical (lowercased, trailing-dot) owner name, then by RR type,
|
||
|
|
// to a slice of RRs forming the RRset.
|
||
|
|
//
|
||
|
|
// Concurrency: a single RWMutex covers all mutations. Read paths
|
||
|
|
// (queries) are far more frequent than writes (UPDATEs), so the
|
||
|
|
// RW split is the right choice. The lock is held only across the
|
||
|
|
// O(1) map operations; nothing slow happens under the lock.
|
||
|
|
//
|
||
|
|
// Serial: every successful mutation bumps the atomic generation
|
||
|
|
// counter. The synthetic SOA uses this counter so resolvers see a
|
||
|
|
// fresh serial after every UPDATE — even though we don't run AXFR
|
||
|
|
// against secondaries, future use-cases (DNS-NOTIFY, debugging,
|
||
|
|
// cache invalidation) benefit from a monotonic serial.
|
||
|
|
type recordStore struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
// rrs[name][rrtype] = RRset (slice). The map-of-maps shape means
|
||
|
|
// per-type lookups are O(1), and "name exists at all" is O(1) on
|
||
|
|
// the outer map.
|
||
|
|
rrs map[string]map[uint16][]dns.RR
|
||
|
|
|
||
|
|
gen atomic.Uint64
|
||
|
|
}
|
||
|
|
|
||
|
|
func newStore() *recordStore {
|
||
|
|
return &recordStore{rrs: make(map[string]map[uint16][]dns.RR)}
|
||
|
|
}
|
||
|
|
|
||
|
|
// generation returns the current monotonic counter. Used for SOA serial.
|
||
|
|
func (s *recordStore) generation() uint64 {
|
||
|
|
return s.gen.Load()
|
||
|
|
}
|
||
|
|
|
||
|
|
// canon normalises a name to the store's internal form: lowercase +
|
||
|
|
// trailing dot. miekg/dns sometimes returns names without the dot;
|
||
|
|
// always passing through this keeps everything consistent.
|
||
|
|
func canon(name string) string {
|
||
|
|
return strings.ToLower(dns.Fqdn(name))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add inserts a single RR into the store. Duplicate RRs (same owner,
|
||
|
|
// type, AND rdata) are silently de-duplicated — matches RFC 2136 §3.4.2.2
|
||
|
|
// behavior for `add` semantics.
|
||
|
|
func (s *recordStore) Add(rr dns.RR) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
name := canon(rr.Header().Name)
|
||
|
|
rtype := rr.Header().Rrtype
|
||
|
|
|
||
|
|
byType, ok := s.rrs[name]
|
||
|
|
if !ok {
|
||
|
|
byType = make(map[uint16][]dns.RR)
|
||
|
|
s.rrs[name] = byType
|
||
|
|
}
|
||
|
|
|
||
|
|
// De-duplicate by string comparison of the rdata-bearing form.
|
||
|
|
rrStr := rr.String()
|
||
|
|
for _, existing := range byType[rtype] {
|
||
|
|
if existing.String() == rrStr {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
byType[rtype] = append(byType[rtype], rr)
|
||
|
|
s.gen.Add(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
// RemoveRRset deletes ALL records of the given (name, type) — i.e.
|
||
|
|
// "delete the RRset" semantics from RFC 2136 §3.4.2.3. If the name
|
||
|
|
// has no records left after this, the name entry is reaped so
|
||
|
|
// NameExists returns false.
|
||
|
|
func (s *recordStore) RemoveRRset(name string, rtype uint16) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
name = canon(name)
|
||
|
|
byType, ok := s.rrs[name]
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if _, hadType := byType[rtype]; !hadType {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
delete(byType, rtype)
|
||
|
|
if len(byType) == 0 {
|
||
|
|
delete(s.rrs, name)
|
||
|
|
}
|
||
|
|
s.gen.Add(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
// RemoveRR deletes one specific RR (matching owner, type, and rdata).
|
||
|
|
// This implements RFC 2136 §3.4.2.4 "delete an RR from an RRset".
|
||
|
|
func (s *recordStore) RemoveRR(rr dns.RR) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
name := canon(rr.Header().Name)
|
||
|
|
rtype := rr.Header().Rrtype
|
||
|
|
byType, ok := s.rrs[name]
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
rrs := byType[rtype]
|
||
|
|
target := rr.String()
|
||
|
|
for i, existing := range rrs {
|
||
|
|
if existing.String() == target {
|
||
|
|
byType[rtype] = append(rrs[:i], rrs[i+1:]...)
|
||
|
|
if len(byType[rtype]) == 0 {
|
||
|
|
delete(byType, rtype)
|
||
|
|
}
|
||
|
|
if len(byType) == 0 {
|
||
|
|
delete(s.rrs, name)
|
||
|
|
}
|
||
|
|
s.gen.Add(1)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// RemoveName deletes all records for an owner name (§3.4.2.3 "delete
|
||
|
|
// all RRsets from a name").
|
||
|
|
func (s *recordStore) RemoveName(name string) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
name = canon(name)
|
||
|
|
if _, ok := s.rrs[name]; !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
delete(s.rrs, name)
|
||
|
|
s.gen.Add(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lookup returns the RRset for (name, rtype). Returns nil for both
|
||
|
|
// "name doesn't exist" and "name exists with other types but not this
|
||
|
|
// one" — use NameExists to distinguish NODATA from NXDOMAIN.
|
||
|
|
//
|
||
|
|
// The returned slice is a copy so callers can freely mutate it without
|
||
|
|
// affecting store state.
|
||
|
|
func (s *recordStore) Lookup(name string, rtype uint16) []dns.RR {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
|
||
|
|
byType, ok := s.rrs[canon(name)]
|
||
|
|
if !ok {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
rrs := byType[rtype]
|
||
|
|
if len(rrs) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
out := make([]dns.RR, len(rrs))
|
||
|
|
copy(out, rrs)
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// NameExists reports whether ANY records exist for the given name.
|
||
|
|
// Used to distinguish NODATA (name exists, no records of asked type)
|
||
|
|
// from NXDOMAIN (name doesn't exist at all).
|
||
|
|
func (s *recordStore) NameExists(name string) bool {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
_, ok := s.rrs[canon(name)]
|
||
|
|
return ok
|
||
|
|
}
|