aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
authorGravatar Chris O'Haver <cohaver@infoblox.com> 2019-11-01 12:02:43 -0400
committerGravatar GitHub <noreply@github.com> 2019-11-01 12:02:43 -0400
commita7ab592e7895ee8369885f0d41251b9adc4f8cbf (patch)
tree6068f7a746d4e5034f5bc01ab284ea7694967d65 /plugin
parent5d8bda58a9cab89bd21860e7429b93cf65a728b1 (diff)
downloadcoredns-a7ab592e7895ee8369885f0d41251b9adc4f8cbf.tar.gz
coredns-a7ab592e7895ee8369885f0d41251b9adc4f8cbf.tar.zst
coredns-a7ab592e7895ee8369885f0d41251b9adc4f8cbf.zip
plugin/transfer: Zone transfer plugin (#3223)
* transfer plugin Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
Diffstat (limited to 'plugin')
-rw-r--r--plugin/transfer/OWNERS6
-rw-r--r--plugin/transfer/README.md32
-rw-r--r--plugin/transfer/setup.go102
-rw-r--r--plugin/transfer/setup_test.go85
-rw-r--r--plugin/transfer/transfer.go181
-rw-r--r--plugin/transfer/transfer_test.go291
6 files changed, 697 insertions, 0 deletions
diff --git a/plugin/transfer/OWNERS b/plugin/transfer/OWNERS
new file mode 100644
index 000000000..3a4ef23a1
--- /dev/null
+++ b/plugin/transfer/OWNERS
@@ -0,0 +1,6 @@
+reviewers:
+ - miekg
+ - chrisohaver
+approvers:
+ - miekg
+ - chrisohaver
diff --git a/plugin/transfer/README.md b/plugin/transfer/README.md
new file mode 100644
index 000000000..df3ab3eb4
--- /dev/null
+++ b/plugin/transfer/README.md
@@ -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.
diff --git a/plugin/transfer/setup.go b/plugin/transfer/setup.go
new file mode 100644
index 000000000..e83fd6d0b
--- /dev/null
+++ b/plugin/transfer/setup.go
@@ -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
+}
diff --git a/plugin/transfer/setup_test.go b/plugin/transfer/setup_test.go
new file mode 100644
index 000000000..421910d46
--- /dev/null
+++ b/plugin/transfer/setup_test.go
@@ -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)
+
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/transfer/transfer.go b/plugin/transfer/transfer.go
new file mode 100644
index 000000000..9f8691548
--- /dev/null
+++ b/plugin/transfer/transfer.go
@@ -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" }
diff --git a/plugin/transfer/transfer_test.go b/plugin/transfer/transfer_test.go
new file mode 100644
index 000000000..8dce4c6e1
--- /dev/null
+++ b/plugin/transfer/transfer_test.go
@@ -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])
+ }
+
+}