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:
143
plugin/dynupdate/dynupdate.go
Normal file
143
plugin/dynupdate/dynupdate.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user