mirror of
https://github.com/coredns/coredns.git
synced 2026-07-10 09:40:12 -04:00
plugin/secondary: parse catalog zones after transfer (#8209)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
157
plugin/pkg/catalog/catalog.go
Normal file
157
plugin/pkg/catalog/catalog.go
Normal 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
|
||||
}
|
||||
191
plugin/pkg/catalog/catalog_test.go
Normal file
191
plugin/pkg/catalog/catalog_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
39
plugin/secondary/catalog.go
Normal file
39
plugin/secondary/catalog.go
Normal 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
|
||||
}
|
||||
153
plugin/secondary/catalog_test.go
Normal file
153
plugin/secondary/catalog_test.go
Normal 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
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user