plugin/secondary: parse catalog zones after transfer (#8209)

This commit is contained in:
houyuwushang
2026-07-05 08:06:05 +08:00
committed by GitHub
parent 9226f8a3aa
commit 97e3a71afb
10 changed files with 662 additions and 15 deletions

View File

@@ -24,6 +24,7 @@ type (
Next plugin.Handler Next plugin.Handler
Zones Zones
Xfer *transfer.Transfer Xfer *transfer.Transfer
TransferInFunc
Fall fall.F Fall fall.F
} }
@@ -71,7 +72,9 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
log.Infof("Notify from %s for %s: checking transfer", state.IP(), zone) log.Infof("Notify from %s for %s: checking transfer", state.IP(), zone)
ok, err := z.shouldTransfer() ok, err := z.shouldTransfer()
if ok { if ok {
z.TransferIn(f.Xfer) if err := f.transferIn(z, f.Xfer); err != nil {
log.Warningf("Notify from %s for %s: transfer failed: %s", state.IP(), zone, err)
}
} else { } else {
log.Infof("Notify from %s for %s: no SOA serial increase seen", state.IP(), zone) log.Infof("Notify from %s for %s: no SOA serial increase seen", state.IP(), zone)
} }
@@ -131,6 +134,13 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
// Name implements the Handler interface. // Name implements the Handler interface.
func (f File) Name() string { return "file" } func (f File) Name() string { return "file" }
func (f File) transferIn(z *Zone, t *transfer.Transfer) error {
if f.TransferInFunc != nil {
return f.TransferInFunc(z, t)
}
return z.TransferIn(t)
}
type serialErr struct { type serialErr struct {
err string err string
zone string zone string

View File

@@ -9,8 +9,17 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// TransferInFunc transfers zone data into z.
type TransferInFunc func(z *Zone, t *transfer.Transfer) error
// TransferIn retrieves the zone from the masters, parses it and sets it live. // TransferIn retrieves the zone from the masters, parses it and sets it live.
func (z *Zone) TransferIn(t *transfer.Transfer) error { func (z *Zone) TransferIn(t *transfer.Transfer) error {
return z.TransferInWithRecords(t, nil)
}
// TransferInWithRecords retrieves the zone from the masters, calls validate
// with the transferred records, and sets the zone live if validation succeeds.
func (z *Zone) TransferInWithRecords(t *transfer.Transfer, validate func([]dns.RR) error) error {
if len(z.TransferFrom) == 0 { if len(z.TransferFrom) == 0 {
return nil return nil
} }
@@ -22,6 +31,7 @@ func (z *Zone) TransferIn(t *transfer.Transfer) error {
Err error Err error
tr string tr string
) )
var transferred []dns.RR
Transfer: Transfer:
for _, tr = range z.TransferFrom { for _, tr = range z.TransferFrom {
@@ -32,6 +42,7 @@ Transfer:
Err = err Err = err
continue Transfer continue Transfer
} }
var records []dns.RR
for env := range c { for env := range c {
if env.Error != nil { if env.Error != nil {
log.Errorf("Failed to transfer `%s' from %q: %v", z.origin, tr, env.Error) log.Errorf("Failed to transfer `%s' from %q: %v", z.origin, tr, env.Error)
@@ -44,14 +55,23 @@ Transfer:
Err = err Err = err
continue Transfer continue Transfer
} }
if validate != nil {
records = append(records, rr)
}
} }
} }
transferred = records
Err = nil Err = nil
break break
} }
if Err != nil { if Err != nil {
return Err return Err
} }
if validate != nil {
if err := validate(transferred); err != nil {
return err
}
}
z.setData(z1.Apex, z1.Tree) z.setData(z1.Apex, z1.Tree)
log.Infof("Transferred: %s from %s", z.origin, tr) log.Infof("Transferred: %s from %s", z.origin, tr)
@@ -114,6 +134,11 @@ func less(a, b uint32) bool {
// server) it will retry every retry interval. If the zone failed to transfer before the expire, the zone // server) it will retry every retry interval. If the zone failed to transfer before the expire, the zone
// will be marked expired. // will be marked expired.
func (z *Zone) Update(updateShutdown chan bool, t *transfer.Transfer) error { func (z *Zone) Update(updateShutdown chan bool, t *transfer.Transfer) error {
return z.UpdateWithTransfer(updateShutdown, t, (*Zone).TransferIn)
}
// UpdateWithTransfer updates the secondary zone using transferIn for zone transfers.
func (z *Zone) UpdateWithTransfer(updateShutdown chan bool, t *transfer.Transfer, transferIn TransferInFunc) error {
// If we don't have a SOA, we don't have a zone, wait for it to appear. // If we don't have a SOA, we don't have a zone, wait for it to appear.
for z.getSOA() == nil { for z.getSOA() == nil {
if waitOrShutdown(updateShutdown, time.Second) { if waitOrShutdown(updateShutdown, time.Second) {
@@ -162,7 +187,7 @@ Restart:
} }
if ok { if ok {
if err := z.TransferIn(t); err != nil { if err := transferIn(z, t); err != nil {
// transfer failed, leave retryActive true // transfer failed, leave retryActive true
break break
} }
@@ -188,7 +213,7 @@ Restart:
} }
if ok { if ok {
if err := z.TransferIn(t); err != nil { if err := transferIn(z, t); err != nil {
// transfer failed // transfer failed
retryActive = true retryActive = true
break break

View File

@@ -0,0 +1,157 @@
// Package catalog parses DNS catalog zones as defined by RFC 9432.
package catalog
import (
"fmt"
"sort"
"strings"
"github.com/miekg/dns"
)
// Version is the RFC 9432 catalog zone schema version supported by this package.
const Version = "2"
// Catalog is the parsed catalog zone state.
type Catalog struct {
Origin string
Members []Member
}
// Member is a member zone entry from a catalog zone.
type Member struct {
ID string
Zone string
Groups []string
ChangeOfOwnership string
}
// Parse builds a Catalog from resource records belonging to origin.
func Parse(origin string, rrs []dns.RR) (*Catalog, error) {
origin = normalizeName(origin)
originLabels := dns.SplitDomainName(origin)
zonesLabels := append([]string{"zones"}, originLabels...)
versionOwner := "version." + origin
var (
hasSOA bool
hasNS bool
versionText []string
)
memberPTR := make(map[string][]string)
groups := make(map[string][]string)
coo := make(map[string][]string)
for _, rr := range rrs {
h := rr.Header()
if h.Class != dns.ClassINET {
return nil, fmt.Errorf("catalog zone %s contains non-IN record %s", origin, rr.String())
}
owner := normalizeName(h.Name)
switch x := rr.(type) {
case *dns.SOA:
if owner == origin {
hasSOA = true
}
case *dns.NS:
if owner == origin {
hasNS = true
}
case *dns.TXT:
if owner == versionOwner {
versionText = append(versionText, txtValue(x))
} else {
if prop, id, ok := propertyOwner(owner, zonesLabels); ok && prop == "group" {
groups[id] = append(groups[id], txtValue(x))
}
}
case *dns.PTR:
switch {
case isMemberOwner(owner, zonesLabels):
id := dns.SplitDomainName(owner)[0]
memberPTR[id] = append(memberPTR[id], normalizeName(x.Ptr))
default:
if prop, id, ok := propertyOwner(owner, zonesLabels); ok && prop == "coo" {
coo[id] = append(coo[id], normalizeName(x.Ptr))
}
}
}
}
if !hasSOA {
return nil, fmt.Errorf("catalog zone %s has no SOA record", origin)
}
if !hasNS {
return nil, fmt.Errorf("catalog zone %s has no NS record", origin)
}
if len(versionText) != 1 {
return nil, fmt.Errorf("catalog zone %s must have exactly one version TXT record", origin)
}
if versionText[0] != Version {
return nil, fmt.Errorf("catalog zone %s has unsupported version %q", origin, versionText[0])
}
seenZones := make(map[string]string)
members := make([]Member, 0, len(memberPTR))
for id, zones := range memberPTR {
if len(zones) != 1 {
return nil, fmt.Errorf("catalog member %s.%s must have exactly one PTR record", id, "zones."+origin)
}
zone := zones[0]
if prevID, ok := seenZones[zone]; ok {
return nil, fmt.Errorf("catalog member zone %s is listed by both %s and %s", zone, prevID, id)
}
seenZones[zone] = id
member := Member{ID: id, Zone: zone, Groups: append([]string(nil), groups[id]...)}
sort.Strings(member.Groups)
if values := coo[id]; len(values) > 1 {
return nil, fmt.Errorf("catalog member %s has more than one coo PTR record", id)
} else if len(values) == 1 {
member.ChangeOfOwnership = values[0]
}
members = append(members, member)
}
sort.Slice(members, func(i, j int) bool {
return members[i].ID < members[j].ID
})
return &Catalog{Origin: origin, Members: members}, nil
}
func normalizeName(name string) string {
return strings.ToLower(dns.Fqdn(name))
}
func txtValue(rr *dns.TXT) string {
return strings.Join(rr.Txt, "")
}
func isMemberOwner(owner string, zonesLabels []string) bool {
labels := dns.SplitDomainName(owner)
return len(labels) == len(zonesLabels)+1 && hasLabelSuffix(labels, zonesLabels)
}
func propertyOwner(owner string, zonesLabels []string) (property, id string, ok bool) {
labels := dns.SplitDomainName(owner)
if len(labels) != len(zonesLabels)+2 || !hasLabelSuffix(labels, zonesLabels) {
return "", "", false
}
return labels[0], labels[1], true
}
func hasLabelSuffix(labels, suffix []string) bool {
if len(labels) < len(suffix) {
return false
}
offset := len(labels) - len(suffix)
for i := range suffix {
if labels[offset+i] != suffix[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,191 @@
package catalog
import (
"strings"
"testing"
"github.com/miekg/dns"
)
func TestParse(t *testing.T) {
rrs := parseZone(t, `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "2"
b.zones 0 IN PTR Example.NET.
a.zones 0 IN PTR example.com.
group.a.zones 0 IN TXT "operator-default"
group.a.zones 0 IN TXT "unsigned"
coo.b.zones 0 IN PTR other-catalog.example.
ignored 0 IN TXT "value"
`)
cat, err := Parse("catalog.example.", rrs)
if err != nil {
t.Fatalf("Parse returned error: %v", err)
}
if cat.Origin != "catalog.example." {
t.Fatalf("expected origin catalog.example., got %q", cat.Origin)
}
if len(cat.Members) != 2 {
t.Fatalf("expected 2 members, got %d", len(cat.Members))
}
assertMember(t, cat.Members[0], Member{
ID: "a",
Zone: "example.com.",
Groups: []string{"operator-default", "unsigned"},
})
assertMember(t, cat.Members[1], Member{
ID: "b",
Zone: "example.net.",
ChangeOfOwnership: "other-catalog.example.",
})
}
func TestParseBrokenCatalog(t *testing.T) {
tests := []struct {
name string
zone string
err string
}{
{
name: "missing soa",
zone: `
$ORIGIN catalog.example.
@ 0 IN NS invalid.
version 0 IN TXT "2"
a.zones 0 IN PTR example.com.
`,
err: "no SOA",
},
{
name: "missing ns",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
version 0 IN TXT "2"
a.zones 0 IN PTR example.com.
`,
err: "no NS",
},
{
name: "missing version",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
a.zones 0 IN PTR example.com.
`,
err: "exactly one version",
},
{
name: "multiple version",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "2"
version 0 IN TXT "2"
a.zones 0 IN PTR example.com.
`,
err: "exactly one version",
},
{
name: "unsupported version",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "1"
a.zones 0 IN PTR example.com.
`,
err: "unsupported version",
},
{
name: "multiple member ptr",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "2"
a.zones 0 IN PTR example.com.
a.zones 0 IN PTR example.net.
`,
err: "exactly one PTR",
},
{
name: "duplicate member zone",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "2"
a.zones 0 IN PTR example.com.
b.zones 0 IN PTR example.com.
`,
err: "listed by both",
},
{
name: "multiple coo ptr",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "2"
a.zones 0 IN PTR example.com.
coo.a.zones 0 IN PTR new-a.example.
coo.a.zones 0 IN PTR new-b.example.
`,
err: "more than one coo",
},
{
name: "non in class",
zone: `
$ORIGIN catalog.example.
@ 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0
@ 0 IN NS invalid.
version 0 IN TXT "2"
a.zones 0 CH PTR example.com.
`,
err: "non-IN",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := Parse("catalog.example.", parseZone(t, tc.zone))
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), tc.err) {
t.Fatalf("expected error containing %q, got %q", tc.err, err)
}
})
}
}
func assertMember(t *testing.T, got, want Member) {
t.Helper()
if got.ID != want.ID || got.Zone != want.Zone || got.ChangeOfOwnership != want.ChangeOfOwnership {
t.Fatalf("expected member %+v, got %+v", want, got)
}
if strings.Join(got.Groups, ",") != strings.Join(want.Groups, ",") {
t.Fatalf("expected groups %v, got %v", want.Groups, got.Groups)
}
}
func parseZone(t *testing.T, zone string) []dns.RR {
t.Helper()
zp := dns.NewZoneParser(strings.NewReader(zone), "catalog.example.", "test")
var rrs []dns.RR
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
rrs = append(rrs, rr)
}
if err := zp.Err(); err != nil {
t.Fatalf("failed to parse test zone: %v", err)
}
return rrs
}

View File

@@ -27,6 +27,7 @@ A working syntax would be:
~~~ ~~~
secondary [zones...] { secondary [zones...] {
transfer from ADDRESS [ADDRESS...] transfer from ADDRESS [ADDRESS...]
catalog
fallthrough [ZONES...] fallthrough [ZONES...]
} }
~~~ ~~~
@@ -35,6 +36,9 @@ secondary [zones...] {
times; if one does not work, another will be tried. Transferring this zone outwards again can be times; if one does not work, another will be tried. Transferring this zone outwards again can be
done by enabling the *transfer* plugin. done by enabling the *transfer* plugin.
* `catalog` treats the transferred zone as an RFC 9432 catalog zone and parses the catalog member
list after each successful transfer.
* `fallthrough` If a query for a record in the zone results in NXDOMAIN, the query will be passed * `fallthrough` If a query for a record in the zone results in NXDOMAIN, the query will be passed
to the next plugin in the chain. If **[ZONES...]** are listed, then only queries for those zones to the next plugin in the chain. If **[ZONES...]** are listed, then only queries for those zones
will be subject to fallthrough. This can be useful in split DNS setups where the secondary zone will be subject to fallthrough. This can be useful in split DNS setups where the secondary zone

View File

@@ -0,0 +1,39 @@
package secondary
import (
"github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/pkg/catalog"
"github.com/coredns/coredns/plugin/transfer"
"github.com/miekg/dns"
)
func (s *Secondary) transferIn(origin string, z *file.Zone, t *transfer.Transfer) error {
if _, ok := s.catalogZones[origin]; !ok {
return z.TransferIn(t)
}
var parsed *catalog.Catalog
if err := z.TransferInWithRecords(t, func(rrs []dns.RR) error {
cat, err := catalog.Parse(origin, rrs)
if err != nil {
return err
}
parsed = cat
return nil
}); err != nil {
return err
}
if parsed == nil {
return nil
}
s.catalogMu.Lock()
if s.catalogs == nil {
s.catalogs = make(map[string]*catalog.Catalog)
}
s.catalogs[origin] = parsed
s.catalogMu.Unlock()
log.Infof("Parsed catalog zone %s with %d member zones", origin, len(parsed.Members))
return nil
}

View File

@@ -0,0 +1,153 @@
package secondary
import (
"strings"
"testing"
"github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/pkg/catalog"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/miekg/dns"
)
func TestTransferInCatalog(t *testing.T) {
const origin = "catalog.example."
rrs := catalogZoneRecords(t, true)
server := dnstest.NewServer(transferHandler(rrs))
defer server.Close()
z := file.NewZone(origin, "stdin")
z.TransferFrom = []string{server.Addr}
s := &Secondary{
catalogs: make(map[string]*catalog.Catalog),
catalogZones: map[string]struct{}{origin: {}},
}
if err := s.transferIn(origin, z, nil); err != nil {
t.Fatalf("transferIn returned error: %v", err)
}
s.catalogMu.RLock()
cat := s.catalogs[origin]
s.catalogMu.RUnlock()
if cat == nil {
t.Fatal("expected parsed catalog")
}
if len(cat.Members) != 1 {
t.Fatalf("expected 1 catalog member, got %d", len(cat.Members))
}
if cat.Members[0].Zone != "example.org." {
t.Fatalf("expected member zone example.org., got %q", cat.Members[0].Zone)
}
if strings.Join(cat.Members[0].Groups, ",") != "default" {
t.Fatalf("expected default member group, got %v", cat.Members[0].Groups)
}
if z.SOA == nil {
t.Fatal("expected zone data to be live after valid catalog transfer")
}
}
func TestTransferInCatalogRejectsInvalidCatalog(t *testing.T) {
const origin = "catalog.example."
rrs := catalogZoneRecords(t, false)
server := dnstest.NewServer(transferHandler(rrs))
defer server.Close()
z := file.NewZone(origin, "stdin")
z.TransferFrom = []string{server.Addr}
s := &Secondary{
catalogs: make(map[string]*catalog.Catalog),
catalogZones: map[string]struct{}{origin: {}},
}
err := s.transferIn(origin, z, nil)
if err == nil {
t.Fatal("expected invalid catalog transfer to fail")
}
if !strings.Contains(err.Error(), "version") {
t.Fatalf("expected version error, got %v", err)
}
if s.catalogs[origin] != nil {
t.Fatal("expected invalid catalog not to be stored")
}
if z.SOA != nil {
t.Fatal("expected invalid catalog transfer not to replace live zone data")
}
}
func TestTransferInSkipsCatalogParseForRegularZone(t *testing.T) {
const origin = "catalog.example."
rrs := catalogZoneRecords(t, false)
server := dnstest.NewServer(transferHandler(rrs))
defer server.Close()
z := file.NewZone(origin, "stdin")
z.TransferFrom = []string{server.Addr}
s := &Secondary{
catalogs: make(map[string]*catalog.Catalog),
}
if err := s.transferIn(origin, z, nil); err != nil {
t.Fatalf("transferIn returned error: %v", err)
}
if s.catalogs[origin] != nil {
t.Fatal("expected regular secondary transfer not to store catalog")
}
if z.SOA == nil {
t.Fatal("expected regular secondary transfer to replace live zone data")
}
}
func transferHandler(rrs []dns.RR) dns.HandlerFunc {
return func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
if len(req.Question) > 0 {
switch req.Question[0].Qtype {
case dns.TypeSOA:
for _, rr := range rrs {
if rr.Header().Rrtype == dns.TypeSOA {
m.Answer = []dns.RR{rr}
break
}
}
case dns.TypeAXFR:
m.Answer = rrs
}
}
_ = w.WriteMsg(m)
}
}
func catalogZoneRecords(t *testing.T, includeVersion bool) []dns.RR {
t.Helper()
soa := mustRR(t, "catalog.example. 0 IN SOA invalid. hostmaster.invalid. 1 3600 600 604800 0")
rrs := []dns.RR{
soa,
mustRR(t, "catalog.example. 0 IN NS invalid."),
}
if includeVersion {
rrs = append(rrs, mustRR(t, `version.catalog.example. 0 IN TXT "2"`))
}
rrs = append(rrs,
mustRR(t, "a.zones.catalog.example. 0 IN PTR example.org."),
mustRR(t, `group.a.zones.catalog.example. 0 IN TXT "default"`),
soa,
)
return rrs
}
func mustRR(t *testing.T, s string) dns.RR {
t.Helper()
rr, err := dns.NewRR(s)
if err != nil {
t.Fatalf("failed to parse RR %q: %v", s, err)
}
return rr
}

View File

@@ -1,13 +1,22 @@
// Package secondary implements a secondary plugin. // Package secondary implements a secondary plugin.
package secondary package secondary
import "github.com/coredns/coredns/plugin/file" import (
"sync"
"github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/pkg/catalog"
)
// Secondary implements a secondary plugin that allows CoreDNS to retrieve (via AXFR) // Secondary implements a secondary plugin that allows CoreDNS to retrieve (via AXFR)
// zone information from a primary server. // zone information from a primary server.
type Secondary struct { type Secondary struct {
file.File file.File
catalogMu sync.RWMutex
catalogs map[string]*catalog.Catalog
catalogZones map[string]struct{}
} }
// Name implements the Handler interface. // Name implements the Handler interface.
func (s Secondary) Name() string { return "secondary" } func (s *Secondary) Name() string { return "secondary" }

View File

@@ -8,6 +8,7 @@ import (
"github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/file" "github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/pkg/catalog"
"github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/fall"
clog "github.com/coredns/coredns/plugin/pkg/log" clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/parse" "github.com/coredns/coredns/plugin/pkg/parse"
@@ -20,12 +21,23 @@ var log = clog.NewWithPlugin("secondary")
func init() { plugin.Register("secondary", setup) } func init() { plugin.Register("secondary", setup) }
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
zones, fall, err := secondaryParse(c) zones, fall, catalogZones, err := secondaryParse(c)
if err != nil { if err != nil {
return plugin.Error("secondary", err) return plugin.Error("secondary", err)
} }
s := &Secondary{file.File{Zones: zones, Fall: fall}} s := &Secondary{
File: file.File{Zones: zones, Fall: fall},
catalogs: make(map[string]*catalog.Catalog),
catalogZones: catalogZones,
}
zoneNames := make(map[*file.Zone]string, len(zones.Z))
for name, zone := range zones.Z {
zoneNames[zone] = name
}
s.TransferInFunc = func(z *file.Zone, t *transfer.Transfer) error {
return s.transferIn(zoneNames[z], z, t)
}
var x *transfer.Transfer var x *transfer.Transfer
c.OnStartup(func() error { c.OnStartup(func() error {
t := dnsserver.GetConfig(c).Handler("transfer") t := dnsserver.GetConfig(c).Handler("transfer")
@@ -51,7 +63,7 @@ func setup(c *caddy.Controller) error {
dur := time.Millisecond * 250 dur := time.Millisecond * 250
max := time.Second * 10 max := time.Second * 10
for { for {
err := z.TransferIn(x) err := s.transferIn(n, z, x)
if err == nil { if err == nil {
break break
} }
@@ -69,7 +81,9 @@ func setup(c *caddy.Controller) error {
return return
default: default:
} }
z.Update(updateShutdown, x) z.UpdateWithTransfer(updateShutdown, x, func(z *file.Zone, t *transfer.Transfer) error {
return s.transferIn(n, z, t)
})
}() }()
}) })
return nil return nil
@@ -100,10 +114,11 @@ func waitForTransferRetry(updateShutdown <-chan bool, dur time.Duration) bool {
} }
} }
func secondaryParse(c *caddy.Controller) (file.Zones, fall.F, error) { func secondaryParse(c *caddy.Controller) (file.Zones, fall.F, map[string]struct{}, error) {
z := make(map[string]*file.Zone) z := make(map[string]*file.Zone)
names := []string{} names := []string{}
fall := fall.F{} fall := fall.F{}
catalogZones := map[string]struct{}{}
for c.Next() { for c.Next() {
if c.Val() == "secondary" { if c.Val() == "secondary" {
// secondary [origin] // secondary [origin]
@@ -122,13 +137,20 @@ func secondaryParse(c *caddy.Controller) (file.Zones, fall.F, error) {
var err error var err error
f, err = parse.TransferIn(c) f, err = parse.TransferIn(c)
if err != nil { if err != nil {
return file.Zones{}, fall, err return file.Zones{}, fall, nil, err
} }
hasTransfer = true hasTransfer = true
case "catalog":
if len(c.RemainingArgs()) != 0 {
return file.Zones{}, fall, nil, c.ArgErr()
}
for _, origin := range origins {
catalogZones[origin] = struct{}{}
}
case "fallthrough": case "fallthrough":
fall.SetZonesFromArgs(c.RemainingArgs()) fall.SetZonesFromArgs(c.RemainingArgs())
default: default:
return file.Zones{}, fall, c.Errf("unknown property '%s'", c.Val()) return file.Zones{}, fall, nil, c.Errf("unknown property '%s'", c.Val())
} }
for _, origin := range origins { for _, origin := range origins {
@@ -139,9 +161,9 @@ func secondaryParse(c *caddy.Controller) (file.Zones, fall.F, error) {
} }
} }
if !hasTransfer { if !hasTransfer {
return file.Zones{}, fall, c.Err("secondary zones require a transfer from property") return file.Zones{}, fall, nil, c.Err("secondary zones require a transfer from property")
} }
} }
} }
return file.Zones{Z: z, Names: names}, fall, nil return file.Zones{Z: z, Names: names}, fall, catalogZones, nil
} }

View File

@@ -14,6 +14,7 @@ func TestSecondaryParse(t *testing.T) {
transferFrom string transferFrom string
zones []string zones []string
fall fall.F fall fall.F
catalogZones []string
}{ }{
{ {
`secondary { `secondary {
@@ -23,6 +24,7 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53", "127.0.0.1:53",
nil, nil,
fall.F{}, fall.F{},
nil,
}, },
{ {
`secondary example.org { `secondary example.org {
@@ -32,6 +34,29 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53", "127.0.0.1:53",
[]string{"example.org."}, []string{"example.org."},
fall.F{}, fall.F{},
nil,
},
{
`secondary catalog.example {
transfer from 127.0.0.1
catalog
}`,
false,
"127.0.0.1:53",
[]string{"catalog.example."},
fall.F{},
[]string{"catalog.example."},
},
{
`secondary catalog.example {
transfer from 127.0.0.1
catalog extra
}`,
true,
"",
nil,
fall.F{},
nil,
}, },
{ {
`secondary`, `secondary`,
@@ -39,6 +64,7 @@ func TestSecondaryParse(t *testing.T) {
"", "",
nil, nil,
fall.F{}, fall.F{},
nil,
}, },
{ {
`secondary example.org { `secondary example.org {
@@ -48,6 +74,7 @@ func TestSecondaryParse(t *testing.T) {
"", "",
nil, nil,
fall.F{}, fall.F{},
nil,
}, },
// fallthrough: bare (all zones) // fallthrough: bare (all zones)
{ {
@@ -59,6 +86,7 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53", "127.0.0.1:53",
nil, nil,
fall.Root, fall.Root,
nil,
}, },
// fallthrough: specific zone // fallthrough: specific zone
{ {
@@ -70,12 +98,13 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53", "127.0.0.1:53",
[]string{"example.org."}, []string{"example.org."},
fall.F{Zones: []string{"example.org."}}, fall.F{Zones: []string{"example.org."}},
nil,
}, },
} }
for i, test := range tests { for i, test := range tests {
c := caddy.NewTestController("dns", test.inputFileRules) c := caddy.NewTestController("dns", test.inputFileRules)
s, f, err := secondaryParse(c) s, f, catalogZones, err := secondaryParse(c)
if err == nil && test.shouldErr { if err == nil && test.shouldErr {
t.Fatalf("Test %d expected errors, but got no error", i) t.Fatalf("Test %d expected errors, but got no error", i)
@@ -99,5 +128,13 @@ func TestSecondaryParse(t *testing.T) {
if !f.Equal(test.fall) { if !f.Equal(test.fall) {
t.Fatalf("Test %d fallthrough not equal: expected %v, got %v", i, test.fall, f) t.Fatalf("Test %d fallthrough not equal: expected %v, got %v", i, test.fall, f)
} }
if len(catalogZones) != len(test.catalogZones) {
t.Fatalf("Test %d catalog zone count mismatch: expected %d, got %d", i, len(test.catalogZones), len(catalogZones))
}
for _, name := range test.catalogZones {
if _, ok := catalogZones[name]; !ok {
t.Fatalf("Test %d expected catalog zone %q", i, name)
}
}
} }
} }