mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 00:04:15 -04:00
plugin/transfer: Zone transfer plugin (#3223)
* transfer plugin Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
This commit is contained in:
@@ -35,6 +35,7 @@ var Directives = []string{
|
||||
"dnssec",
|
||||
"autopath",
|
||||
"template",
|
||||
"transfer",
|
||||
"hosts",
|
||||
"route53",
|
||||
"azure",
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
_ "github.com/coredns/coredns/plugin/template"
|
||||
_ "github.com/coredns/coredns/plugin/tls"
|
||||
_ "github.com/coredns/coredns/plugin/trace"
|
||||
_ "github.com/coredns/coredns/plugin/transfer"
|
||||
_ "github.com/coredns/coredns/plugin/whoami"
|
||||
_ "github.com/coredns/federation"
|
||||
)
|
||||
|
||||
@@ -44,6 +44,7 @@ rewrite:rewrite
|
||||
dnssec:dnssec
|
||||
autopath:autopath
|
||||
template:template
|
||||
transfer:transfer
|
||||
hosts:hosts
|
||||
route53:route53
|
||||
azure:azure
|
||||
|
||||
6
plugin/transfer/OWNERS
Normal file
6
plugin/transfer/OWNERS
Normal file
@@ -0,0 +1,6 @@
|
||||
reviewers:
|
||||
- miekg
|
||||
- chrisohaver
|
||||
approvers:
|
||||
- miekg
|
||||
- chrisohaver
|
||||
32
plugin/transfer/README.md
Normal file
32
plugin/transfer/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# transfer
|
||||
|
||||
## Name
|
||||
|
||||
*transfer* - answer zone transfers requests for compatible authoritative
|
||||
plugins.
|
||||
|
||||
## Description
|
||||
|
||||
This plugin answers zone transfers for authoritative plugins that implement
|
||||
`transfer.Transferer`.
|
||||
|
||||
Transfer answers AXFR requests and IXFR requests with AXFR fallback if the
|
||||
zone has changed.
|
||||
|
||||
Notifies are not currently supported.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
transfer [ZONE...] {
|
||||
to HOST...
|
||||
}
|
||||
~~~
|
||||
|
||||
* **ZONES** The zones *transfer* will answer zone requests for. If left blank,
|
||||
the zones are inherited from the enclosing server block. To answer zone
|
||||
transfers for a given zone, there must be another plugin in the same server
|
||||
block that serves the same zone, and implements `transfer.Transferer`.
|
||||
|
||||
* `to ` **HOST...** The hosts *transfer* will transfer to. Use `*` to permit
|
||||
transfers to all hosts.
|
||||
102
plugin/transfer/setup.go
Normal file
102
plugin/transfer/setup.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
parsepkg "github.com/coredns/coredns/plugin/pkg/parse"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("transfer", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
t, err := parse(c)
|
||||
|
||||
if err != nil {
|
||||
return plugin.Error("transfer", err)
|
||||
}
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
t.Next = next
|
||||
return t
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
// find all plugins that implement Transferer and add them to Transferers
|
||||
plugins := dnsserver.GetConfig(c).Handlers()
|
||||
for _, pl := range plugins {
|
||||
tr, ok := pl.(Transferer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
t.Transferers = append(t.Transferers, tr)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (*Transfer, error) {
|
||||
|
||||
t := &Transfer{}
|
||||
for c.Next() {
|
||||
x := &xfr{}
|
||||
zones := c.RemainingArgs()
|
||||
|
||||
if len(zones) != 0 {
|
||||
x.Zones = zones
|
||||
for i := 0; i < len(x.Zones); i++ {
|
||||
nzone, err := plugin.Host(x.Zones[i]).MustNormalize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.Zones[i] = nzone
|
||||
}
|
||||
} else {
|
||||
x.Zones = make([]string, len(c.ServerBlockKeys))
|
||||
for i := 0; i < len(c.ServerBlockKeys); i++ {
|
||||
nzone, err := plugin.Host(c.ServerBlockKeys[i]).MustNormalize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.Zones[i] = nzone
|
||||
}
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "to":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
for _, host := range args {
|
||||
if host == "*" {
|
||||
x.to = append(x.to, host)
|
||||
continue
|
||||
}
|
||||
normalized, err := parsepkg.HostPort(host, transport.Port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.to = append(x.to, normalized)
|
||||
}
|
||||
default:
|
||||
return nil, plugin.Error("transfer", c.Errf("unknown property '%s'", c.Val()))
|
||||
}
|
||||
}
|
||||
if len(x.to) == 0 {
|
||||
return nil, plugin.Error("transfer", c.Errf("'to' is required", c.Val()))
|
||||
}
|
||||
t.xfrs = append(t.xfrs, x)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
85
plugin/transfer/setup_test.go
Normal file
85
plugin/transfer/setup_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
exp *Transfer
|
||||
}{
|
||||
{`transfer example.net example.org {
|
||||
to 1.2.3.4 5.6.7.8:1053 [1::2]:34
|
||||
}
|
||||
transfer example.com example.edu {
|
||||
to * 1.2.3.4
|
||||
}`,
|
||||
false,
|
||||
&Transfer{
|
||||
xfrs: []*xfr{{
|
||||
Zones: []string{"example.net.", "example.org."},
|
||||
to: []string{"1.2.3.4:53", "5.6.7.8:1053", "[1::2]:34"},
|
||||
}, {
|
||||
Zones: []string{"example.com.", "example.edu."},
|
||||
to: []string{"*", "1.2.3.4:53"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
// errors
|
||||
{`transfer example.net example.org {
|
||||
}`,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
{`transfer example.net example.org {
|
||||
invalid option
|
||||
}`,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for i, tc := range tests {
|
||||
c := caddy.NewTestController("dns", tc.input)
|
||||
transfer, err := parse(c)
|
||||
|
||||
if err == nil && tc.shouldErr {
|
||||
t.Fatalf("Test %d expected errors, but got no error", i)
|
||||
}
|
||||
if err != nil && !tc.shouldErr {
|
||||
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
|
||||
}
|
||||
if tc.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(tc.exp.xfrs) != len(transfer.xfrs) {
|
||||
t.Fatalf("Test %d expected %d xfrs, got %d", i, len(tc.exp.xfrs), len(transfer.xfrs))
|
||||
}
|
||||
for j, x := range transfer.xfrs {
|
||||
// Check Zones
|
||||
if len(tc.exp.xfrs[j].Zones) != len(x.Zones) {
|
||||
t.Fatalf("Test %d expected %d zones, got %d", i, len(tc.exp.xfrs[i].Zones), len(x.Zones))
|
||||
}
|
||||
for k, zone := range x.Zones {
|
||||
if tc.exp.xfrs[j].Zones[k] != zone {
|
||||
t.Errorf("Test %d expected zone %v, got %v", i, tc.exp.xfrs[j].Zones[k], zone)
|
||||
|
||||
}
|
||||
}
|
||||
// Check to
|
||||
if len(tc.exp.xfrs[j].to) != len(x.to) {
|
||||
t.Fatalf("Test %d expected %d 'to' values, got %d", i, len(tc.exp.xfrs[i].to), len(x.to))
|
||||
}
|
||||
for k, to := range x.to {
|
||||
if tc.exp.xfrs[j].to[k] != to {
|
||||
t.Errorf("Test %d expected %v in 'to', got %v", i, tc.exp.xfrs[j].to[k], to)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
plugin/transfer/transfer.go
Normal file
181
plugin/transfer/transfer.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var log = clog.NewWithPlugin("transfer")
|
||||
|
||||
// Transfer is a plugin that handles zone transfers.
|
||||
type Transfer struct {
|
||||
Transferers []Transferer // the list of plugins that implement Transferer
|
||||
xfrs []*xfr
|
||||
Next plugin.Handler // the next plugin in the chain
|
||||
}
|
||||
|
||||
type xfr struct {
|
||||
Zones []string
|
||||
to []string
|
||||
}
|
||||
|
||||
// Transferer may be implemented by plugins to enable zone transfers
|
||||
type Transferer interface {
|
||||
// Transfer returns a channel to which it writes responses to the transfer request.
|
||||
// If the plugin is not authoritative for the zone, it should immediately return the
|
||||
// Transfer.ErrNotAuthoritative error.
|
||||
//
|
||||
// If serial is 0, handle as an AXFR request. Transfer should send all records
|
||||
// in the zone to the channel. The SOA should be written to the channel first, followed
|
||||
// by all other records, including all NS + glue records.
|
||||
//
|
||||
// If serial is not 0, handle as an IXFR request. If the serial is equal to or greater (newer) than
|
||||
// the current serial for the zone, send a single SOA record to the channel.
|
||||
// If the serial is less (older) than the current serial for the zone, perform an AXFR fallback
|
||||
// by proceeding as if an AXFR was requested (as above).
|
||||
Transfer(zone string, serial uint32) (<-chan []dns.RR, error)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNotAuthoritative is returned by Transfer() when the plugin is not authoritative for the zone
|
||||
ErrNotAuthoritative = errors.New("not authoritative for zone")
|
||||
)
|
||||
|
||||
// ServeDNS implements the plugin.Handler interface.
|
||||
func (t Transfer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
if state.QType() != dns.TypeAXFR && state.QType() != dns.TypeIXFR {
|
||||
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Find the first transfer instance for which the queried zone is a subdomain.
|
||||
var x *xfr
|
||||
for _, xfr := range t.xfrs {
|
||||
zone := plugin.Zones(xfr.Zones).Matches(state.Name())
|
||||
if zone == "" {
|
||||
continue
|
||||
}
|
||||
x = xfr
|
||||
}
|
||||
if x == nil {
|
||||
// Requested zone did not match any transfer instance zones.
|
||||
// Pass request down chain in case later plugins are capable of handling transfer requests themselves.
|
||||
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
if !x.allowed(state) {
|
||||
return dns.RcodeRefused, nil
|
||||
}
|
||||
|
||||
// Get serial from request if this is an IXFR
|
||||
var serial uint32
|
||||
if state.QType() == dns.TypeIXFR {
|
||||
soa, ok := r.Ns[0].(*dns.SOA)
|
||||
if !ok {
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
serial = soa.Serial
|
||||
}
|
||||
|
||||
// Get a receiving channel from the first Transferer plugin that returns one
|
||||
var fromPlugin <-chan []dns.RR
|
||||
for _, p := range t.Transferers {
|
||||
var err error
|
||||
fromPlugin, err = p.Transfer(state.QName(), serial)
|
||||
if err == ErrNotAuthoritative {
|
||||
// plugin was not authoritative for the zone, try next plugin
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if fromPlugin == nil {
|
||||
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Send response to client
|
||||
ch := make(chan *dns.Envelope)
|
||||
tr := new(dns.Transfer)
|
||||
wg := new(sync.WaitGroup)
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tr.Out(w, r, ch)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
var soa *dns.SOA
|
||||
rrs := []dns.RR{}
|
||||
l := 0
|
||||
|
||||
receive:
|
||||
for records := range fromPlugin {
|
||||
for _, record := range records {
|
||||
if soa == nil {
|
||||
if soa = record.(*dns.SOA); soa == nil {
|
||||
break receive
|
||||
}
|
||||
serial = soa.Serial
|
||||
}
|
||||
rrs = append(rrs, record)
|
||||
if len(rrs) > 500 {
|
||||
ch <- &dns.Envelope{RR: rrs}
|
||||
l += len(rrs)
|
||||
rrs = []dns.RR{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(rrs) > 0 {
|
||||
ch <- &dns.Envelope{RR: rrs}
|
||||
l += len(rrs)
|
||||
rrs = []dns.RR{}
|
||||
}
|
||||
|
||||
if soa != nil {
|
||||
ch <- &dns.Envelope{RR: []dns.RR{soa}} // closing SOA.
|
||||
l++
|
||||
}
|
||||
|
||||
close(ch) // Even though we close the channel here, we still have
|
||||
wg.Wait() // to wait before we can return and close the connection.
|
||||
|
||||
if soa == nil {
|
||||
return dns.RcodeServerFailure, fmt.Errorf("first record in zone %s is not SOA", state.QName())
|
||||
}
|
||||
|
||||
log.Infof("Outgoing transfer of %d records of zone %s to %s with %d SOA serial", l, state.QName(), state.IP(), serial)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
func (x xfr) allowed(state request.Request) bool {
|
||||
for _, h := range x.to {
|
||||
if h == "*" {
|
||||
return true
|
||||
}
|
||||
to, _, err := net.SplitHostPort(h)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// If remote IP matches we accept.
|
||||
remote := state.IP()
|
||||
if to == remote {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (Transfer) Name() string { return "transfer" }
|
||||
291
plugin/transfer/transfer_test.go
Normal file
291
plugin/transfer/transfer_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// transfererPlugin implements transfer.Transferer and plugin.Handler
|
||||
type transfererPlugin struct {
|
||||
Zone string
|
||||
Serial uint32
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// Name implements plugin.Handler
|
||||
func (transfererPlugin) Name() string { return "transfererplugin" }
|
||||
|
||||
// ServeDNS implements plugin.Handler
|
||||
func (p transfererPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
if r.Question[0].Name != p.Zone {
|
||||
return p.Next.ServeDNS(ctx, w, r)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Transfer implements transfer.Transferer - it returns a static AXFR response, or
|
||||
// if serial is current, an abbreviated IXFR response
|
||||
func (p transfererPlugin) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) {
|
||||
if zone != p.Zone {
|
||||
return nil, ErrNotAuthoritative
|
||||
}
|
||||
ch := make(chan []dns.RR, 2)
|
||||
defer close(ch)
|
||||
ch <- []dns.RR{test.SOA(fmt.Sprintf("%s 100 IN SOA ns.dns.%s hostmaster.%s %d 7200 1800 86400 100", p.Zone, p.Zone, p.Zone, p.Serial))}
|
||||
if serial >= p.Serial {
|
||||
return ch, nil
|
||||
}
|
||||
ch <- []dns.RR{
|
||||
test.NS(fmt.Sprintf("%s 100 IN NS ns.dns.%s", p.Zone, p.Zone)),
|
||||
test.A(fmt.Sprintf("ns.dns.%s 100 IN A 1.2.3.4", p.Zone)),
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type terminatingPlugin struct{}
|
||||
|
||||
// Name implements plugin.Handler
|
||||
func (terminatingPlugin) Name() string { return "testplugin" }
|
||||
|
||||
// ServeDNS implements plugin.Handler that returns NXDOMAIN for all requests
|
||||
func (terminatingPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(r, dns.RcodeNameError)
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeNameError, nil
|
||||
}
|
||||
|
||||
func newTestTransfer() Transfer {
|
||||
nextPlugin1 := transfererPlugin{Zone: "example.com.", Serial: 12345}
|
||||
nextPlugin2 := transfererPlugin{Zone: "example.org.", Serial: 12345}
|
||||
nextPlugin2.Next = terminatingPlugin{}
|
||||
nextPlugin1.Next = nextPlugin2
|
||||
|
||||
transfer := Transfer{
|
||||
Transferers: []Transferer{nextPlugin1, nextPlugin2},
|
||||
xfrs: []*xfr{
|
||||
{
|
||||
Zones: []string{"example.org."},
|
||||
to: []string{"*"},
|
||||
},
|
||||
{
|
||||
Zones: []string{"example.com."},
|
||||
to: []string{"*"},
|
||||
},
|
||||
},
|
||||
Next: nextPlugin1,
|
||||
}
|
||||
return transfer
|
||||
}
|
||||
|
||||
func TestTransferNonZone(t *testing.T) {
|
||||
|
||||
transfer := newTestTransfer()
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, tc := range []string{"sub.example.org.", "example.test."} {
|
||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetAxfr(tc)
|
||||
|
||||
_, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if w.Msg == nil {
|
||||
t.Fatalf("Got nil message for AXFR %s", tc)
|
||||
}
|
||||
|
||||
if w.Msg.Rcode != dns.RcodeNameError {
|
||||
t.Errorf("Expected NXDOMAIN for AXFR %s got %s", tc, dns.RcodeToString[w.Msg.Rcode])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferNotAXFRorIXFR(t *testing.T) {
|
||||
|
||||
transfer := newTestTransfer()
|
||||
|
||||
ctx := context.TODO()
|
||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetQuestion("test.domain.", dns.TypeA)
|
||||
|
||||
_, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if w.Msg == nil {
|
||||
t.Fatal("Got nil message")
|
||||
}
|
||||
|
||||
if w.Msg.Rcode != dns.RcodeNameError {
|
||||
t.Errorf("Expected NXDOMAIN got %s", dns.RcodeToString[w.Msg.Rcode])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferAXFRExampleOrg(t *testing.T) {
|
||||
|
||||
transfer := newTestTransfer()
|
||||
|
||||
ctx := context.TODO()
|
||||
w := dnstest.NewMultiRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetAxfr(transfer.xfrs[0].Zones[0])
|
||||
|
||||
_, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
validateAXFRResponse(t, w)
|
||||
}
|
||||
|
||||
func TestTransferAXFRExampleCom(t *testing.T) {
|
||||
|
||||
transfer := newTestTransfer()
|
||||
|
||||
ctx := context.TODO()
|
||||
w := dnstest.NewMultiRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetAxfr(transfer.xfrs[1].Zones[0])
|
||||
|
||||
_, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
validateAXFRResponse(t, w)
|
||||
}
|
||||
|
||||
func TestTransferIXFRFallback(t *testing.T) {
|
||||
|
||||
transfer := newTestTransfer()
|
||||
|
||||
testPlugin := transfer.Transferers[0].(transfererPlugin)
|
||||
|
||||
ctx := context.TODO()
|
||||
w := dnstest.NewMultiRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetIxfr(
|
||||
transfer.xfrs[0].Zones[0],
|
||||
testPlugin.Serial-1,
|
||||
"ns.dns."+testPlugin.Zone,
|
||||
"hostmaster.dns."+testPlugin.Zone,
|
||||
)
|
||||
|
||||
_, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
validateAXFRResponse(t, w)
|
||||
}
|
||||
|
||||
func TestTransferIXFRCurrent(t *testing.T) {
|
||||
|
||||
transfer := newTestTransfer()
|
||||
|
||||
testPlugin := transfer.Transferers[0].(transfererPlugin)
|
||||
|
||||
ctx := context.TODO()
|
||||
w := dnstest.NewMultiRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetIxfr(
|
||||
transfer.xfrs[0].Zones[0],
|
||||
testPlugin.Serial,
|
||||
"ns.dns."+testPlugin.Zone,
|
||||
"hostmaster.dns."+testPlugin.Zone,
|
||||
)
|
||||
|
||||
_, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(w.Msgs) == 0 {
|
||||
t.Logf("%+v\n", w)
|
||||
t.Fatal("Did not get back a zone response")
|
||||
}
|
||||
|
||||
if len(w.Msgs[0].Answer) != 1 {
|
||||
t.Logf("%+v\n", w)
|
||||
t.Fatalf("Expected 1 answer, got %d", len(w.Msgs[0].Answer))
|
||||
}
|
||||
|
||||
// Ensure the answer is the SOA
|
||||
if w.Msgs[0].Answer[0].Header().Rrtype != dns.TypeSOA {
|
||||
t.Error("Answer does not contain the SOA record")
|
||||
}
|
||||
}
|
||||
|
||||
func validateAXFRResponse(t *testing.T, w *dnstest.MultiRecorder) {
|
||||
if len(w.Msgs) == 0 {
|
||||
t.Logf("%+v\n", w)
|
||||
t.Fatal("Did not get back a zone response")
|
||||
}
|
||||
|
||||
if len(w.Msgs[0].Answer) == 0 {
|
||||
t.Logf("%+v\n", w)
|
||||
t.Fatal("Did not get back an answer")
|
||||
}
|
||||
|
||||
// Ensure the answer starts with SOA
|
||||
if w.Msgs[0].Answer[0].Header().Rrtype != dns.TypeSOA {
|
||||
t.Error("Answer does not start with SOA record")
|
||||
}
|
||||
|
||||
// Ensure the answer ends with SOA
|
||||
if w.Msgs[len(w.Msgs)-1].Answer[len(w.Msgs[len(w.Msgs)-1].Answer)-1].Header().Rrtype != dns.TypeSOA {
|
||||
t.Error("Answer does not end with SOA record")
|
||||
}
|
||||
|
||||
// Ensure the answer is the expected length
|
||||
c := 0
|
||||
for _, m := range w.Msgs {
|
||||
c += len(m.Answer)
|
||||
}
|
||||
if c != 4 {
|
||||
t.Errorf("Answer is not the expected length (expected 4, got %d)", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferNotAllowed(t *testing.T) {
|
||||
nextPlugin := transfererPlugin{Zone: "example.org.", Serial: 12345}
|
||||
|
||||
transfer := Transfer{
|
||||
Transferers: []Transferer{nextPlugin},
|
||||
xfrs: []*xfr{
|
||||
{
|
||||
Zones: []string{"example.org."},
|
||||
to: []string{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
Next: nextPlugin,
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
w := dnstest.NewMultiRecorder(&test.ResponseWriter{})
|
||||
dnsmsg := &dns.Msg{}
|
||||
dnsmsg.SetAxfr(transfer.xfrs[0].Zones[0])
|
||||
|
||||
rcode, err := transfer.ServeDNS(ctx, w, dnsmsg)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if rcode != dns.RcodeRefused {
|
||||
t.Errorf("Expected REFUSED response code, got %s", dns.RcodeToString[rcode])
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user