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
Zones
Xfer *transfer.Transfer
TransferInFunc
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)
ok, err := z.shouldTransfer()
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 {
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.
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 {
err string
zone string

View File

@@ -9,8 +9,17 @@ import (
"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.
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 {
return nil
}
@@ -22,6 +31,7 @@ func (z *Zone) TransferIn(t *transfer.Transfer) error {
Err error
tr string
)
var transferred []dns.RR
Transfer:
for _, tr = range z.TransferFrom {
@@ -32,6 +42,7 @@ Transfer:
Err = err
continue Transfer
}
var records []dns.RR
for env := range c {
if env.Error != nil {
log.Errorf("Failed to transfer `%s' from %q: %v", z.origin, tr, env.Error)
@@ -44,14 +55,23 @@ Transfer:
Err = err
continue Transfer
}
if validate != nil {
records = append(records, rr)
}
}
}
transferred = records
Err = nil
break
}
if Err != nil {
return Err
}
if validate != nil {
if err := validate(transferred); err != nil {
return err
}
}
z.setData(z1.Apex, z1.Tree)
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
// will be marked expired.
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.
for z.getSOA() == nil {
if waitOrShutdown(updateShutdown, time.Second) {
@@ -162,7 +187,7 @@ Restart:
}
if ok {
if err := z.TransferIn(t); err != nil {
if err := transferIn(z, t); err != nil {
// transfer failed, leave retryActive true
break
}
@@ -188,7 +213,7 @@ Restart:
}
if ok {
if err := z.TransferIn(t); err != nil {
if err := transferIn(z, t); err != nil {
// transfer failed
retryActive = true
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...] {
transfer from ADDRESS [ADDRESS...]
catalog
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
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
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

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
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)
// zone information from a primary server.
type Secondary struct {
file.File
catalogMu sync.RWMutex
catalogs map[string]*catalog.Catalog
catalogZones map[string]struct{}
}
// 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/plugin"
"github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/pkg/catalog"
"github.com/coredns/coredns/plugin/pkg/fall"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/parse"
@@ -20,12 +21,23 @@ var log = clog.NewWithPlugin("secondary")
func init() { plugin.Register("secondary", setup) }
func setup(c *caddy.Controller) error {
zones, fall, err := secondaryParse(c)
zones, fall, catalogZones, err := secondaryParse(c)
if err != nil {
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
c.OnStartup(func() error {
t := dnsserver.GetConfig(c).Handler("transfer")
@@ -51,7 +63,7 @@ func setup(c *caddy.Controller) error {
dur := time.Millisecond * 250
max := time.Second * 10
for {
err := z.TransferIn(x)
err := s.transferIn(n, z, x)
if err == nil {
break
}
@@ -69,7 +81,9 @@ func setup(c *caddy.Controller) error {
return
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
@@ -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)
names := []string{}
fall := fall.F{}
catalogZones := map[string]struct{}{}
for c.Next() {
if c.Val() == "secondary" {
// secondary [origin]
@@ -122,13 +137,20 @@ func secondaryParse(c *caddy.Controller) (file.Zones, fall.F, error) {
var err error
f, err = parse.TransferIn(c)
if err != nil {
return file.Zones{}, fall, err
return file.Zones{}, fall, nil, err
}
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":
fall.SetZonesFromArgs(c.RemainingArgs())
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 {
@@ -139,9 +161,9 @@ func secondaryParse(c *caddy.Controller) (file.Zones, fall.F, error) {
}
}
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
zones []string
fall fall.F
catalogZones []string
}{
{
`secondary {
@@ -23,6 +24,7 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53",
nil,
fall.F{},
nil,
},
{
`secondary example.org {
@@ -32,6 +34,29 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53",
[]string{"example.org."},
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`,
@@ -39,6 +64,7 @@ func TestSecondaryParse(t *testing.T) {
"",
nil,
fall.F{},
nil,
},
{
`secondary example.org {
@@ -48,6 +74,7 @@ func TestSecondaryParse(t *testing.T) {
"",
nil,
fall.F{},
nil,
},
// fallthrough: bare (all zones)
{
@@ -59,6 +86,7 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53",
nil,
fall.Root,
nil,
},
// fallthrough: specific zone
{
@@ -70,12 +98,13 @@ func TestSecondaryParse(t *testing.T) {
"127.0.0.1:53",
[]string{"example.org."},
fall.F{Zones: []string{"example.org."}},
nil,
},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.inputFileRules)
s, f, err := secondaryParse(c)
s, f, catalogZones, err := secondaryParse(c)
if err == nil && test.shouldErr {
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) {
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)
}
}
}
}