mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 03:55:21 -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>
361 lines
8.6 KiB
Go
361 lines
8.6 KiB
Go
package dynupdate
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/coredns/caddy"
|
|
"github.com/coredns/coredns/core/dnsserver"
|
|
"github.com/coredns/coredns/plugin"
|
|
"github.com/coredns/coredns/plugin/file"
|
|
"github.com/coredns/coredns/plugin/transfer"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
func init() { plugin.Register(pluginName, setup) }
|
|
|
|
func setup(c *caddy.Controller) error {
|
|
d, err := parse(c)
|
|
if err != nil {
|
|
return plugin.Error(pluginName, err)
|
|
}
|
|
|
|
cfg := dnsserver.GetConfig(c)
|
|
cfg.AllowOpcode(dns.OpcodeUpdate)
|
|
cfg.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
|
d.mu.Lock()
|
|
d.Next = next
|
|
if d.view != nil {
|
|
d.view.Next = next
|
|
}
|
|
d.mu.Unlock()
|
|
return d
|
|
})
|
|
|
|
c.OnStartup(func() error {
|
|
if h := dnsserver.GetConfig(c).Handler("transfer"); h != nil {
|
|
if t, ok := h.(*transfer.Transfer); ok {
|
|
d.mu.Lock()
|
|
d.Xfer = t
|
|
d.mu.Unlock()
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
c.OnShutdown(d.close)
|
|
|
|
return nil
|
|
}
|
|
|
|
func parse(c *caddy.Controller) (*DynUpdate, error) {
|
|
if !c.Next() {
|
|
return nil, c.ArgErr()
|
|
}
|
|
|
|
origin, err := parseOrigin(c.RemainingArgs(), c.ServerBlockKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
seed string
|
|
seedDefined bool
|
|
permissions []permission
|
|
database string
|
|
bound limits
|
|
)
|
|
seen := make(map[string]bool)
|
|
for c.NextBlock() {
|
|
property := c.Val()
|
|
if property != "allow" && seen[property] {
|
|
return nil, c.Errf("%s specified more than once", property)
|
|
}
|
|
seen[property] = true
|
|
switch c.Val() {
|
|
case "database":
|
|
args := c.RemainingArgs()
|
|
if len(args) != 1 || args[0] == "" {
|
|
return nil, c.ArgErr()
|
|
}
|
|
database = args[0]
|
|
|
|
case "max_records", "max_bytes", "max_update_records":
|
|
args := c.RemainingArgs()
|
|
if len(args) != 1 {
|
|
return nil, c.ArgErr()
|
|
}
|
|
n, err := strconv.Atoi(args[0])
|
|
if err != nil || n <= 0 {
|
|
return nil, c.Errf("%s requires a positive integer", property)
|
|
}
|
|
switch property {
|
|
case "max_records":
|
|
bound.records = n
|
|
case "max_bytes":
|
|
bound.bytes = n
|
|
case "max_update_records":
|
|
bound.updateRecords = n
|
|
}
|
|
case "file":
|
|
args := c.RemainingArgs()
|
|
if len(args) != 1 {
|
|
return nil, c.ArgErr()
|
|
}
|
|
if seedDefined {
|
|
return nil, c.Err("file specified more than once")
|
|
}
|
|
seed, seedDefined = args[0], true
|
|
|
|
case "allow":
|
|
p, err := parsePermission(c.RemainingArgs(), origin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
permissions = append(permissions, p)
|
|
|
|
default:
|
|
return nil, c.Errf("unknown property %q", c.Val())
|
|
}
|
|
}
|
|
|
|
if c.Next() {
|
|
return nil, plugin.ErrOnce
|
|
}
|
|
if !seedDefined {
|
|
return nil, c.Err("file is required")
|
|
}
|
|
if len(permissions) == 0 {
|
|
return nil, c.Err("at least one allow rule is required")
|
|
}
|
|
|
|
if !filepath.IsAbs(seed) && dnsserver.GetConfig(c).Root != "" {
|
|
seed = filepath.Join(dnsserver.GetConfig(c).Root, seed)
|
|
}
|
|
d := &DynUpdate{
|
|
Zone: origin,
|
|
permissions: permissions,
|
|
limits: bound.defaults(),
|
|
seed: seed,
|
|
}
|
|
if database != "" {
|
|
d.database, err = databasePath(database, dnsserver.GetConfig(c).Root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s, err := d.acquireStore(true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.mu.RLock()
|
|
d.records = s.records
|
|
copyView := *s.view
|
|
d.view = ©View
|
|
s.mu.RUnlock()
|
|
if err := releaseStore(s); err != nil {
|
|
return nil, err
|
|
}
|
|
return d, nil
|
|
}
|
|
d.records, err = readZoneLimited(seed, origin, d.limits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.view, err = d.build(d.records)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("building zone %q: %w", origin, err)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func parseOrigin(args, serverBlockKeys []string) (string, error) {
|
|
var token string
|
|
switch {
|
|
case len(args) == 0:
|
|
serverZones, err := normalizeServerBlockZones(serverBlockKeys)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(serverZones) != 1 {
|
|
return "", fmt.Errorf("zone is required when the server block does not define exactly one zone")
|
|
}
|
|
token = serverZones[0]
|
|
case len(args) == 1:
|
|
token = args[0]
|
|
default:
|
|
return "", fmt.Errorf("exactly one zone is allowed")
|
|
}
|
|
|
|
hosts := plugin.Host(token).NormalizeExact()
|
|
if len(hosts) != 1 || hosts[0] == "" {
|
|
return "", fmt.Errorf("invalid zone %q", token)
|
|
}
|
|
origin := canonicalName(hosts[0])
|
|
if _, ok := dns.IsDomainName(origin); !ok {
|
|
return "", fmt.Errorf("invalid zone %q", token)
|
|
}
|
|
return origin, nil
|
|
}
|
|
|
|
func normalizeServerBlockZones(keys []string) ([]string, error) {
|
|
seen := make(map[string]struct{}, len(keys))
|
|
for _, key := range keys {
|
|
hosts := plugin.Host(key).NormalizeExact()
|
|
if len(hosts) != 1 || hosts[0] == "" {
|
|
return nil, fmt.Errorf("invalid server block zone %q", key)
|
|
}
|
|
zone := canonicalName(hosts[0])
|
|
if _, ok := dns.IsDomainName(zone); !ok {
|
|
return nil, fmt.Errorf("invalid server block zone %q", key)
|
|
}
|
|
seen[zone] = struct{}{}
|
|
}
|
|
|
|
zones := make([]string, 0, len(seen))
|
|
for zone := range seen {
|
|
zones = append(zones, zone)
|
|
}
|
|
return zones, nil
|
|
}
|
|
|
|
func parsePermission(args []string, origin string) (permission, error) {
|
|
if len(args) < 3 {
|
|
return permission{}, fmt.Errorf("allow requires KEY NAME and at least one RR type")
|
|
}
|
|
|
|
if strings.TrimSpace(args[0]) == "" || args[0] == "*" || args[0] == "@" {
|
|
return permission{}, fmt.Errorf("invalid TSIG key %q", args[0])
|
|
}
|
|
key := plugin.Name(args[0]).Normalize()
|
|
if _, ok := dns.IsDomainName(key); !ok {
|
|
return permission{}, fmt.Errorf("invalid TSIG key %q", args[0])
|
|
}
|
|
|
|
p := permission{key: key, name: allNames, types: make(map[uint16]struct{})}
|
|
if args[1] != allNames {
|
|
name := args[1]
|
|
if name == "@" {
|
|
name = origin
|
|
} else {
|
|
name = canonicalName(name)
|
|
}
|
|
if _, ok := dns.IsDomainName(name); !ok || !inZone(origin, name) {
|
|
return permission{}, fmt.Errorf("allow name %q is outside zone %q", args[1], origin)
|
|
}
|
|
p.name = name
|
|
}
|
|
|
|
for _, raw := range args[2:] {
|
|
if raw == allTypes {
|
|
if len(args) != 3 {
|
|
return permission{}, fmt.Errorf("wildcard RR type must be the only type in an allow rule")
|
|
}
|
|
p.allTypes = true
|
|
continue
|
|
}
|
|
rrType, ok := parseRRType(raw)
|
|
if !ok || !validPolicyType(rrType) {
|
|
return permission{}, fmt.Errorf("invalid RR type %q in allow rule", raw)
|
|
}
|
|
p.types[rrType] = struct{}{}
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func parseRRType(raw string) (uint16, bool) {
|
|
name := strings.ToUpper(raw)
|
|
if rrType, ok := dns.StringToType[name]; ok {
|
|
return rrType, true
|
|
}
|
|
if !strings.HasPrefix(name, "TYPE") {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseUint(strings.TrimPrefix(name, "TYPE"), 10, 16)
|
|
if err != nil || n == 0 || n == uint64(dns.TypeReserved) {
|
|
return 0, false
|
|
}
|
|
return uint16(n), true
|
|
}
|
|
|
|
func validPolicyType(rrType uint16) bool {
|
|
if !knownRRType(rrType) || rrType == dns.TypeNone || unsupportedRRType(rrType) {
|
|
return false
|
|
}
|
|
switch rrType {
|
|
case dns.TypeANY:
|
|
// ANY is an UPDATE metatype for deleting all RRsets at one name,
|
|
// and is therefore a valid explicit authorization target.
|
|
return true
|
|
case dns.TypeAXFR, dns.TypeIXFR, dns.TypeMAILA, dns.TypeMAILB,
|
|
dns.TypeOPT, dns.TypeTKEY, dns.TypeTSIG:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func readZone(path, origin string) ([]dns.RR, error) {
|
|
return readZoneLimited(path, origin, limits{})
|
|
}
|
|
|
|
func readZoneLimited(path, origin string, bound limits) ([]dns.RR, error) {
|
|
f, err := os.Open(filepath.Clean(path))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening zone file %q: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
zp := dns.NewZoneParser(f, dns.Fqdn(origin), path)
|
|
zp.SetIncludeAllowed(true)
|
|
z := file.NewZone(origin, path)
|
|
soaCount := 0
|
|
bound = bound.defaults()
|
|
count, remaining := 0, bound.bytes
|
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
|
count++
|
|
size := dns.Len(rr)
|
|
if count > bound.records || size > remaining {
|
|
return nil, fmt.Errorf("zone file %q exceeds configured limits", path)
|
|
}
|
|
remaining -= size
|
|
if _, ok := rr.(*dns.SOA); ok {
|
|
soaCount++
|
|
}
|
|
if err := z.Insert(rr); err != nil {
|
|
return nil, fmt.Errorf("parsing zone file %q: %w", path, err)
|
|
}
|
|
}
|
|
if err := zp.Err(); err != nil {
|
|
return nil, fmt.Errorf("parsing zone file %q: %w", path, err)
|
|
}
|
|
if soaCount == 0 {
|
|
return nil, fmt.Errorf("zone %q has no SOA", origin)
|
|
}
|
|
if soaCount != 1 {
|
|
return nil, fmt.Errorf("zone %q contains more than one SOA", origin)
|
|
}
|
|
|
|
apex, err := z.ApexIfDefined()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zone %q has no SOA: %w", origin, err)
|
|
}
|
|
records := make([]dns.RR, 0, len(apex))
|
|
for _, rr := range apex {
|
|
records = append(records, dns.Copy(rr))
|
|
}
|
|
for _, elem := range z.All() {
|
|
for _, rr := range elem.All() {
|
|
records = append(records, dns.Copy(rr))
|
|
}
|
|
}
|
|
|
|
if err := validateRecords(records, origin); err != nil {
|
|
return nil, fmt.Errorf("invalid zone %q: %w", origin, err)
|
|
}
|
|
return records, nil
|
|
}
|