2018-01-09 00:39:49 +00:00
|
|
|
package libnetwork
|
|
|
|
|
|
|
|
import (
|
2023-06-23 00:33:17 +00:00
|
|
|
"context"
|
2023-01-06 23:25:00 +00:00
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
2018-01-09 00:39:49 +00:00
|
|
|
"net"
|
2018-03-20 16:11:43 +00:00
|
|
|
"syscall"
|
2018-01-09 00:39:49 +00:00
|
|
|
"testing"
|
2018-03-20 16:11:43 +00:00
|
|
|
"time"
|
2018-01-09 00:39:49 +00:00
|
|
|
|
2023-09-13 15:41:45 +00:00
|
|
|
"github.com/containerd/log"
|
2023-07-26 19:06:35 +00:00
|
|
|
"github.com/docker/docker/internal/testutils/netnsutils"
|
2018-01-09 00:39:49 +00:00
|
|
|
"github.com/miekg/dns"
|
2021-05-28 00:15:56 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2023-01-06 23:25:00 +00:00
|
|
|
"gotest.tools/v3/assert"
|
2023-05-18 18:10:44 +00:00
|
|
|
is "gotest.tools/v3/assert/cmp"
|
2018-01-09 00:39:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// a simple/null address type that will be used to fake a local address for unit testing
|
|
|
|
type tstaddr struct {
|
2023-05-19 13:03:33 +00:00
|
|
|
network string
|
2018-01-09 00:39:49 +00:00
|
|
|
}
|
|
|
|
|
2023-05-19 13:03:33 +00:00
|
|
|
func (a *tstaddr) Network() string {
|
|
|
|
if a.network != "" {
|
|
|
|
return a.network
|
|
|
|
}
|
|
|
|
return "tcp"
|
|
|
|
}
|
2018-01-09 00:39:49 +00:00
|
|
|
|
2023-05-19 13:03:33 +00:00
|
|
|
func (a *tstaddr) String() string { return "(fake)" }
|
2018-01-09 00:39:49 +00:00
|
|
|
|
|
|
|
// a simple writer that implements dns.ResponseWriter for unit testing purposes
|
|
|
|
type tstwriter struct {
|
2023-05-19 13:03:33 +00:00
|
|
|
network string
|
|
|
|
msg *dns.Msg
|
2018-01-09 00:39:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *tstwriter) WriteMsg(m *dns.Msg) (err error) {
|
|
|
|
w.msg = m
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *tstwriter) Write(m []byte) (int, error) { return 0, nil }
|
|
|
|
|
2023-01-06 23:25:00 +00:00
|
|
|
func (w *tstwriter) LocalAddr() net.Addr {
|
2023-05-19 13:03:33 +00:00
|
|
|
return &tstaddr{network: w.network}
|
2023-01-06 23:25:00 +00:00
|
|
|
}
|
2018-01-09 00:39:49 +00:00
|
|
|
|
2023-05-19 13:03:33 +00:00
|
|
|
func (w *tstwriter) RemoteAddr() net.Addr {
|
|
|
|
return &tstaddr{network: w.network}
|
|
|
|
}
|
2018-01-09 00:39:49 +00:00
|
|
|
|
|
|
|
func (w *tstwriter) TsigStatus() error { return nil }
|
|
|
|
|
|
|
|
func (w *tstwriter) TsigTimersOnly(b bool) {}
|
|
|
|
|
|
|
|
func (w *tstwriter) Hijack() {}
|
|
|
|
|
|
|
|
func (w *tstwriter) Close() error { return nil }
|
|
|
|
|
|
|
|
func (w *tstwriter) GetResponse() *dns.Msg { return w.msg }
|
|
|
|
|
|
|
|
func (w *tstwriter) ClearResponse() { w.msg = nil }
|
|
|
|
|
|
|
|
func checkNonNullResponse(t *testing.T, m *dns.Msg) {
|
2022-12-13 23:54:43 +00:00
|
|
|
t.Helper()
|
2018-01-09 00:39:49 +00:00
|
|
|
if m == nil {
|
|
|
|
t.Fatal("Null DNS response found. Non Null response msg expected.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkDNSAnswersCount(t *testing.T, m *dns.Msg, expected int) {
|
2022-12-13 23:54:43 +00:00
|
|
|
t.Helper()
|
2018-01-09 00:39:49 +00:00
|
|
|
answers := len(m.Answer)
|
|
|
|
if answers != expected {
|
|
|
|
t.Fatalf("Expected number of answers in response: %d. Found: %d", expected, answers)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkDNSResponseCode(t *testing.T, m *dns.Msg, expected int) {
|
2022-12-13 23:54:43 +00:00
|
|
|
t.Helper()
|
2018-01-09 00:39:49 +00:00
|
|
|
if m.MsgHdr.Rcode != expected {
|
|
|
|
t.Fatalf("Expected DNS response code: %d. Found: %d", expected, m.MsgHdr.Rcode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkDNSRRType(t *testing.T, actual, expected uint16) {
|
2022-12-13 23:54:43 +00:00
|
|
|
t.Helper()
|
2018-01-09 00:39:49 +00:00
|
|
|
if actual != expected {
|
|
|
|
t.Fatalf("Expected DNS Rrtype: %d. Found: %d", expected, actual)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-20 16:11:43 +00:00
|
|
|
func newDNSHandlerServFailOnce(requests *int) func(w dns.ResponseWriter, r *dns.Msg) {
|
|
|
|
return func(w dns.ResponseWriter, r *dns.Msg) {
|
|
|
|
m := new(dns.Msg)
|
|
|
|
m.SetReply(r)
|
|
|
|
m.Compress = false
|
|
|
|
if *requests == 0 {
|
|
|
|
m.SetRcode(r, dns.RcodeServerFailure)
|
|
|
|
}
|
|
|
|
*requests = *requests + 1
|
2021-05-28 00:15:56 +00:00
|
|
|
if err := w.WriteMsg(m); err != nil {
|
2023-06-23 00:33:17 +00:00
|
|
|
log.G(context.TODO()).WithError(err).Error("Error writing dns response")
|
2021-05-28 00:15:56 +00:00
|
|
|
}
|
2018-03-20 16:11:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func waitForLocalDNSServer(t *testing.T) {
|
|
|
|
retries := 0
|
|
|
|
maxRetries := 10
|
|
|
|
|
|
|
|
for retries < maxRetries {
|
|
|
|
t.Log("Try connecting to DNS server ...")
|
|
|
|
// this test and retry mechanism only works for TCP. With UDP there is no
|
|
|
|
// connection and the test becomes inaccurate leading to unpredictable results
|
|
|
|
tconn, err := net.DialTimeout("tcp", "127.0.0.1:53", 10*time.Second)
|
|
|
|
retries = retries + 1
|
|
|
|
if err != nil {
|
|
|
|
if oerr, ok := err.(*net.OpError); ok {
|
|
|
|
// server is probably initializing
|
|
|
|
if oerr.Err == syscall.ECONNREFUSED {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// something is wrong: we should stop for analysis
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if tconn != nil {
|
|
|
|
tconn.Close()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 23:25:00 +00:00
|
|
|
// Packet 24 extracted from
|
|
|
|
// https://gist.github.com/vojtad/3bac63b8c91b1ec50e8d8b36047317fa/raw/7d75eb3d3448381bf252ae55ea5123a132c46658/host.pcap
|
|
|
|
// (https://github.com/moby/moby/issues/44575)
|
|
|
|
// which is a non-compliant DNS reply > 512B (w/o EDNS(0)) to the query
|
|
|
|
//
|
|
|
|
// s3.amazonaws.com. IN A
|
|
|
|
const oversizedDNSReplyMsg = "\xf5\x11\x81\x80\x00\x01\x00\x20\x00\x00\x00\x00\x02\x73\x33\x09" +
|
|
|
|
"\x61\x6d\x61\x7a\x6f\x6e\x61\x77\x73\x03\x63\x6f\x6d\x00\x00\x01" +
|
|
|
|
"\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\x11\x9e\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\x4c\x66\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\xda\x10\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\x01\x3e\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\x88\x68\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\x66\x9e\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\x5f\x28\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\x8e\x4e\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x36\xe7" +
|
|
|
|
"\x84\xf0\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x34\xd8" +
|
|
|
|
"\x92\x45\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\x8f\xa6\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x36\xe7" +
|
|
|
|
"\xc0\xd0\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\xfe\x28\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\xaa\x3d\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\x4e\x56\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd9" +
|
|
|
|
"\xea\xb0\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\x6d\xed\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x04\x00\x04\x34\xd8" +
|
|
|
|
"\x28\x00\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x34\xd9" +
|
|
|
|
"\xe9\x78\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x34\xd9" +
|
|
|
|
"\x6e\x9e\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x34\xd9" +
|
|
|
|
"\x45\x86\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x34\xd8" +
|
|
|
|
"\x30\x38\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x36\xe7" +
|
|
|
|
"\xc6\xa8\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x03\x05" +
|
|
|
|
"\x01\x9d\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd9" +
|
|
|
|
"\xa8\xe8\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd9" +
|
|
|
|
"\x64\xa6\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd8" +
|
|
|
|
"\x3c\x48\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd8" +
|
|
|
|
"\x35\x20\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd9" +
|
|
|
|
"\x54\xf6\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd9" +
|
|
|
|
"\x5d\x36\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x34\xd9" +
|
|
|
|
"\x30\x36\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x05\x00\x04\x36\xe7" +
|
|
|
|
"\x83\x90"
|
|
|
|
|
|
|
|
// Regression test for https://github.com/moby/moby/issues/44575
|
|
|
|
func TestOversizedDNSReply(t *testing.T) {
|
|
|
|
srv, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
|
|
assert.NilError(t, err)
|
|
|
|
defer srv.Close()
|
|
|
|
go func() {
|
|
|
|
buf := make([]byte, 65536)
|
|
|
|
for {
|
|
|
|
n, src, err := srv.ReadFrom(buf)
|
|
|
|
if errors.Is(err, net.ErrClosed) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t.Logf("[<-%v]\n%s", src, hex.Dump(buf[:n]))
|
|
|
|
if n < 2 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
resp := []byte(oversizedDNSReplyMsg)
|
|
|
|
resp[0], resp[1] = buf[0], buf[1] // Copy query ID into response.
|
|
|
|
_, err = srv.WriteTo(resp, src)
|
|
|
|
if errors.Is(err, net.ErrClosed) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
t.Log(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
srvAddr := srv.LocalAddr().(*net.UDPAddr)
|
2023-01-20 21:58:23 +00:00
|
|
|
rsv := NewResolver("", true, noopDNSBackend{})
|
2023-05-19 15:27:15 +00:00
|
|
|
// The resolver logs lots of valuable info at level debug. Redirect it
|
|
|
|
// to t.Log() so the log spew is emitted only if the test fails.
|
|
|
|
rsv.logger = testLogger(t)
|
2023-01-06 23:25:00 +00:00
|
|
|
rsv.SetExtServers([]extDNSEntry{
|
|
|
|
{IPStr: srvAddr.IP.String(), port: uint16(srvAddr.Port), HostLoopback: true},
|
|
|
|
})
|
|
|
|
|
2023-05-19 13:03:33 +00:00
|
|
|
w := &tstwriter{network: srvAddr.Network()}
|
2023-01-06 23:25:00 +00:00
|
|
|
q := new(dns.Msg).SetQuestion("s3.amazonaws.com.", dns.TypeA)
|
2023-01-20 21:58:23 +00:00
|
|
|
rsv.serveDNS(w, q)
|
2023-01-06 23:25:00 +00:00
|
|
|
resp := w.GetResponse()
|
|
|
|
checkNonNullResponse(t, resp)
|
|
|
|
t.Log("Response: ", resp.String())
|
|
|
|
checkDNSResponseCode(t, resp, dns.RcodeSuccess)
|
|
|
|
assert.Assert(t, len(resp.Answer) >= 1)
|
|
|
|
checkDNSRRType(t, resp.Answer[0].Header().Rrtype, dns.TypeA)
|
|
|
|
}
|
|
|
|
|
2023-09-27 09:15:38 +00:00
|
|
|
func testLogger(t *testing.T) *logrus.Entry {
|
2023-05-19 15:27:15 +00:00
|
|
|
logger := logrus.New()
|
|
|
|
logger.SetLevel(logrus.DebugLevel)
|
|
|
|
logger.SetOutput(tlogWriter{t})
|
2023-09-27 09:15:38 +00:00
|
|
|
return logrus.NewEntry(logger)
|
2022-12-13 23:54:43 +00:00
|
|
|
}
|
|
|
|
|
2023-01-06 23:25:00 +00:00
|
|
|
type tlogWriter struct{ t *testing.T }
|
|
|
|
|
|
|
|
func (w tlogWriter) Write(p []byte) (n int, err error) {
|
|
|
|
w.t.Logf("%s", p)
|
|
|
|
return len(p), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type noopDNSBackend struct{ DNSBackend }
|
|
|
|
|
2023-09-23 09:44:55 +00:00
|
|
|
func (noopDNSBackend) ResolveName(_ context.Context, name string, iplen int) ([]net.IP, bool) {
|
|
|
|
return nil, false
|
|
|
|
}
|
2023-01-06 23:25:00 +00:00
|
|
|
|
|
|
|
func (noopDNSBackend) ExecFunc(f func()) error { f(); return nil }
|
|
|
|
|
|
|
|
func (noopDNSBackend) NdotsSet() bool { return false }
|
|
|
|
|
|
|
|
func (noopDNSBackend) HandleQueryResp(name string, ip net.IP) {}
|
2022-12-13 23:54:43 +00:00
|
|
|
|
2022-12-16 20:03:55 +00:00
|
|
|
func TestReplySERVFAIL(t *testing.T) {
|
|
|
|
cases := []struct {
|
|
|
|
name string
|
|
|
|
q *dns.Msg
|
|
|
|
proxyDNS bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "InternalError",
|
|
|
|
q: new(dns.Msg).SetQuestion("_sip._tcp.example.com.", dns.TypeSRV),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ProxyDNS=false",
|
|
|
|
q: new(dns.Msg).SetQuestion("example.com.", dns.TypeA),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ProxyDNS=true", // No extDNS servers configured -> no answer from any upstream
|
|
|
|
q: new(dns.Msg).SetQuestion("example.com.", dns.TypeA),
|
|
|
|
proxyDNS: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2023-01-20 21:58:23 +00:00
|
|
|
rsv := NewResolver("", tt.proxyDNS, badSRVDNSBackend{})
|
2023-05-19 15:27:15 +00:00
|
|
|
rsv.logger = testLogger(t)
|
2022-12-16 20:03:55 +00:00
|
|
|
w := &tstwriter{}
|
2023-01-20 21:58:23 +00:00
|
|
|
rsv.serveDNS(w, tt.q)
|
2022-12-16 20:03:55 +00:00
|
|
|
resp := w.GetResponse()
|
|
|
|
checkNonNullResponse(t, resp)
|
|
|
|
t.Log("Response: ", resp.String())
|
|
|
|
checkDNSResponseCode(t, resp, dns.RcodeServerFailure)
|
|
|
|
})
|
|
|
|
}
|
2022-12-13 23:54:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type badSRVDNSBackend struct{ noopDNSBackend }
|
|
|
|
|
2023-09-23 09:44:55 +00:00
|
|
|
func (badSRVDNSBackend) ResolveService(_ context.Context, _ string) ([]*net.SRV, []net.IP) {
|
2022-12-13 23:54:43 +00:00
|
|
|
return []*net.SRV{nil, nil, nil}, nil // Mismatched slice lengths
|
|
|
|
}
|
2023-05-18 18:10:44 +00:00
|
|
|
|
|
|
|
func TestProxyNXDOMAIN(t *testing.T) {
|
|
|
|
mockSOA, err := dns.NewRR(". 86367 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2023051800 1800 900 604800 86400\n")
|
|
|
|
assert.NilError(t, err)
|
|
|
|
assert.Assert(t, mockSOA != nil)
|
|
|
|
|
|
|
|
serveStarted := make(chan struct{})
|
|
|
|
srv := &dns.Server{
|
|
|
|
Net: "udp",
|
|
|
|
Addr: "127.0.0.1:0",
|
|
|
|
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
|
|
|
msg := new(dns.Msg).SetRcode(r, dns.RcodeNameError)
|
|
|
|
msg.Ns = append(msg.Ns, dns.Copy(mockSOA))
|
|
|
|
w.WriteMsg(msg)
|
|
|
|
}),
|
|
|
|
NotifyStartedFunc: func() { close(serveStarted) },
|
|
|
|
}
|
|
|
|
serveDone := make(chan error, 1)
|
|
|
|
go func() {
|
|
|
|
defer close(serveDone)
|
|
|
|
serveDone <- srv.ListenAndServe()
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case err := <-serveDone:
|
|
|
|
t.Fatal(err)
|
|
|
|
case <-serveStarted:
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := srv.Shutdown(); err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
<-serveDone
|
|
|
|
}()
|
|
|
|
|
2023-05-19 23:27:29 +00:00
|
|
|
// This test, by virtue of running a server and client in different
|
|
|
|
// not-locked-to-thread goroutines, happens to be a good canary for
|
|
|
|
// whether we are leaking unlocked OS threads set to the wrong network
|
|
|
|
// namespace. Make a best-effort attempt to detect that situation so we
|
|
|
|
// are not left chasing ghosts next time.
|
2023-07-26 19:06:35 +00:00
|
|
|
netnsutils.AssertSocketSameNetNS(t, srv.PacketConn.(*net.UDPConn))
|
2023-05-19 23:27:29 +00:00
|
|
|
|
2023-05-18 18:10:44 +00:00
|
|
|
srvAddr := srv.PacketConn.LocalAddr().(*net.UDPAddr)
|
|
|
|
rsv := NewResolver("", true, noopDNSBackend{})
|
|
|
|
rsv.SetExtServers([]extDNSEntry{
|
|
|
|
{IPStr: srvAddr.IP.String(), port: uint16(srvAddr.Port), HostLoopback: true},
|
|
|
|
})
|
|
|
|
|
|
|
|
// The resolver logs lots of valuable info at level debug. Redirect it
|
|
|
|
// to t.Log() so the log spew is emitted only if the test fails.
|
2023-05-19 15:27:15 +00:00
|
|
|
rsv.logger = testLogger(t)
|
2023-05-18 18:10:44 +00:00
|
|
|
|
2023-05-19 13:03:33 +00:00
|
|
|
w := &tstwriter{network: srvAddr.Network()}
|
2023-05-18 18:10:44 +00:00
|
|
|
q := new(dns.Msg).SetQuestion("example.net.", dns.TypeA)
|
|
|
|
rsv.serveDNS(w, q)
|
|
|
|
resp := w.GetResponse()
|
|
|
|
checkNonNullResponse(t, resp)
|
|
|
|
t.Log("Response:\n" + resp.String())
|
|
|
|
checkDNSResponseCode(t, resp, dns.RcodeNameError)
|
|
|
|
assert.Assert(t, is.Len(resp.Answer, 0))
|
|
|
|
assert.Assert(t, is.Len(resp.Ns, 1))
|
|
|
|
assert.Equal(t, resp.Ns[0].String(), mockSOA.String())
|
|
|
|
}
|