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>
144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
// Package dynupdate implements RFC 2136 dynamic updates for a file-backed
|
|
// authoritative zone.
|
|
//
|
|
// The seed file is never modified. An optional local database makes accepted
|
|
// updates durable. Queries and AXFR see the same atomically replaced snapshot.
|
|
package dynupdate
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/coredns/coredns/plugin"
|
|
"github.com/coredns/coredns/plugin/file"
|
|
"github.com/coredns/coredns/plugin/pkg/upstream"
|
|
"github.com/coredns/coredns/plugin/transfer"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
const pluginName = "dynupdate"
|
|
|
|
var (
|
|
_ plugin.Handler = (*DynUpdate)(nil)
|
|
_ transfer.Transferer = (*DynUpdate)(nil)
|
|
)
|
|
|
|
// DynUpdate serves one authoritative zone and accepts RFC 2136 UPDATE
|
|
// messages for it.
|
|
type DynUpdate struct {
|
|
Next plugin.Handler
|
|
|
|
// Zone is the canonical, fully-qualified origin served by this instance.
|
|
Zone string
|
|
|
|
// Xfer is populated when the transfer plugin is configured in the same
|
|
// server block. It is used for best-effort NOTIFY after a committed update.
|
|
Xfer *transfer.Transfer
|
|
|
|
permissions []permission
|
|
limits limits
|
|
seed string
|
|
database string
|
|
|
|
mu sync.RWMutex
|
|
records []dns.RR
|
|
view *file.File
|
|
store *zoneStore
|
|
closed bool
|
|
notifyPending bool
|
|
notifyRunning bool
|
|
}
|
|
|
|
// ServeDNS implements the plugin.Handler interface.
|
|
func (d *DynUpdate) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
|
if r.Opcode == dns.OpcodeUpdate {
|
|
return d.serveUpdate(ctx, w, r)
|
|
}
|
|
if len(r.Question) == 1 && !inZone(d.Zone, r.Question[0].Name) {
|
|
return plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
|
|
}
|
|
view, err := d.snapshot()
|
|
if err != nil {
|
|
return dns.RcodeServerFailure, err
|
|
}
|
|
|
|
return view.ServeDNS(ctx, w, r)
|
|
}
|
|
|
|
// Transfer implements transfer.Transferer. The current immutable view is
|
|
// used, so a transfer observes either the old or the new zone generation.
|
|
func (d *DynUpdate) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) {
|
|
zone = canonicalName(zone)
|
|
if zone != d.Zone {
|
|
return nil, transfer.ErrNotAuthoritative
|
|
}
|
|
|
|
view, err := d.snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return view.Transfer(zone, serial)
|
|
}
|
|
|
|
// Name implements the plugin.Handler interface.
|
|
func (d *DynUpdate) Name() string { return pluginName }
|
|
|
|
// CacheBypassZones prevents caching of mutable data without bypassing other
|
|
// middleware between cache and this authoritative backend.
|
|
func (d *DynUpdate) CacheBypassZones() []string { return []string{d.Zone} }
|
|
|
|
func (d *DynUpdate) snapshot() (*file.File, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if err := d.ensureStore(); err != nil {
|
|
return nil, err
|
|
}
|
|
view := d.view
|
|
if d.store != nil {
|
|
d.store.mu.RLock()
|
|
view = d.store.view
|
|
d.store.mu.RUnlock()
|
|
}
|
|
if view == nil {
|
|
return nil, fmt.Errorf("zone %q has no snapshot", d.Zone)
|
|
}
|
|
copyView := *view
|
|
copyView.Next = d.Next
|
|
return ©View, nil
|
|
}
|
|
|
|
// build creates the read and transfer view for a record snapshot. file.Zone
|
|
// already contains CoreDNS's authoritative lookup, wildcard, delegation, and
|
|
// DNSSEC response behavior, so this plugin does not duplicate those rules.
|
|
func (d *DynUpdate) build(records []dns.RR) (*file.File, error) {
|
|
if err := d.limits.check(records); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateRecords(records, d.Zone); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
z := file.NewZone(d.Zone, "")
|
|
z.Upstream = upstream.New()
|
|
for _, rr := range records {
|
|
if err := z.Insert(dns.Copy(rr)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &file.File{
|
|
Next: d.Next,
|
|
Zones: file.Zones{
|
|
Z: map[string]*file.Zone{d.Zone: z},
|
|
Names: []string{d.Zone},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// install swaps a fully built view. The caller must hold d.mu for writing.
|
|
func (d *DynUpdate) install(records []dns.RR, view *file.File) {
|
|
d.records = records
|
|
d.view = view
|
|
}
|