mirror of
https://github.com/coredns/coredns.git
synced 2026-07-16 04:30:11 -04:00
plugin/secondary: parse catalog zones after transfer (#8209)
This commit is contained in:
@@ -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