check / check (push) Waiting to run
When a domain's parent zone's servers answer NXDOMAIN, LookupNS returns ErrNXDomain. The watcher then saves the domain with no nameservers and nxdomain set, shown on the dashboard and in /api/v1/status, asks for none of its records and removes those saved, so its old nameservers go in one NS Change. A domain with no delegation of its own gets an empty set and its records are still asked at the zone it is in. FindAuthoritativeNameservers moves to a parent name only on one of those two answers; when the servers do not answer, it returns the error. After an upgrade, a domain without its own delegation that was saved with its parent zone's nameservers gets one NS Change; the README says so. Model: opus-5-5
1307 lines
34 KiB
Go
1307 lines
34 KiB
Go
package resolver_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/livednstest"
|
|
"sneak.berlin/go/dnswatcher/internal/resolver"
|
|
)
|
|
|
|
// ----------------------------------------------------------------
|
|
// Test helpers
|
|
// ----------------------------------------------------------------
|
|
|
|
// nonexistentDomain is a .com domain that does not exist.
|
|
const nonexistentDomain = "dnswatcher-test-does-not-exist.com"
|
|
|
|
// noAnswerAddress is 192.0.2.1, a documentation address: nothing
|
|
// answers there.
|
|
const noAnswerAddress = "192.0.2.1"
|
|
|
|
func newTestResolver(t *testing.T) *resolver.Resolver {
|
|
t.Helper()
|
|
|
|
log := slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
|
))
|
|
|
|
return resolver.NewFromLogger(log)
|
|
}
|
|
|
|
// findOneNSForDomain picks one authoritative nameserver to aim a
|
|
// test at. Quorum handling lives in livedns_test.go, and the live-DNS
|
|
// retry and concurrency limit in package livednstest.
|
|
func findOneNSForDomain(
|
|
t *testing.T,
|
|
r *resolver.Resolver,
|
|
domain string,
|
|
) string {
|
|
t.Helper()
|
|
|
|
return liveFindAuthoritative(t, r, domain)[0]
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// FindAuthoritativeNameservers tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestFindAuthoritativeNameservers_ValidDomain(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
nameservers := liveFindAuthoritative(t, r, "google.com")
|
|
|
|
hasGoogleNS := false
|
|
|
|
for _, ns := range nameservers {
|
|
if strings.Contains(ns, "google") {
|
|
hasGoogleNS = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, hasGoogleNS,
|
|
"expected google nameservers, got: %v", nameservers,
|
|
)
|
|
}
|
|
|
|
func TestFindAuthoritativeNameservers_Subdomain(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
fromHost := liveFindAuthoritative(t, r, "www.google.com")
|
|
fromZone := liveFindAuthoritative(t, r, "google.com")
|
|
|
|
assert.Equal(t, fromZone, fromHost)
|
|
}
|
|
|
|
// TestFindAuthoritativeNameservers_DelegatedSubdomain looks up the
|
|
// nameservers of www.cs.cmu.edu, a name in cs.cmu.edu, a zone that
|
|
// cmu.edu delegates to other servers. The servers of cs.cmu.edu answer
|
|
// that the name has no delegation of its own, so it gets their names,
|
|
// not those of the cmu.edu servers. Every referral on the way gives the
|
|
// nameservers' addresses, so the walk sends few queries.
|
|
func TestFindAuthoritativeNameservers_DelegatedSubdomain(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
fromHost := liveFindAuthoritative(t, r, "www.cs.cmu.edu")
|
|
fromZone := liveLookupNS(t, r, "cs.cmu.edu")
|
|
fromParent := liveLookupNS(t, r, "cmu.edu")
|
|
|
|
assert.Equal(t, fromZone, fromHost)
|
|
assert.NotEqual(t, fromParent, fromHost)
|
|
}
|
|
|
|
// TestFindAuthoritativeNameservers_NoAnswer starts each walk for
|
|
// www.google.com at 192.0.2.1, a documentation address where nothing
|
|
// answers. A walk that got no answer does not say that the name has no
|
|
// delegation of its own, so the lookup returns that walk's error, about
|
|
// www.google.com, and tries no parent name: trying google.com and com
|
|
// would end in ErrNoNameservers, or in the error of a walk for one of
|
|
// them.
|
|
func TestFindAuthoritativeNameservers_NoAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := resolver.NewWithQueryTimeout(slog.Default(), 100*time.Millisecond)
|
|
|
|
nameservers, err := r.FindAuthoritativeNameserversFrom(
|
|
t.Context(), "www.google.com", []string{noAnswerAddress},
|
|
)
|
|
require.Error(t, err)
|
|
require.NotErrorIs(t, err, resolver.ErrNoNameservers)
|
|
assert.Contains(t, err.Error(), "query www.google.com. @"+noAnswerAddress)
|
|
assert.Empty(t, nameservers)
|
|
}
|
|
|
|
func TestFindAuthoritativeNameservers_ReturnsSorted(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
nameservers := liveFindAuthoritative(t, r, "google.com")
|
|
|
|
assert.True(
|
|
t,
|
|
sort.StringsAreSorted(nameservers),
|
|
"nameservers should be sorted, got: %v", nameservers,
|
|
)
|
|
}
|
|
|
|
func TestFindAuthoritativeNameservers_Deterministic(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
first := liveFindAuthoritative(t, r, "google.com")
|
|
second := liveFindAuthoritative(t, r, "google.com")
|
|
|
|
assert.Equal(t, first, second)
|
|
}
|
|
|
|
func TestFindAuthoritativeNameservers_TrailingDot(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns1 := liveFindAuthoritative(t, r, "google.com")
|
|
ns2 := liveFindAuthoritative(t, r, "google.com.")
|
|
|
|
assert.Equal(t, ns1, ns2)
|
|
}
|
|
|
|
func TestFindAuthoritativeNameservers_CloudflareDomain(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
nameservers := liveFindAuthoritative(t, r, "cloudflare.com")
|
|
|
|
for _, ns := range nameservers {
|
|
assert.True(t, strings.HasSuffix(ns, "."),
|
|
"NS should be FQDN with trailing dot: %s", ns,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestResolveNSIPs_EveryNameserver looks up the addresses of two of
|
|
// google.com's nameservers together, as the walk does when a referral
|
|
// names a zone's nameservers without their addresses, and compares them
|
|
// with each looked up alone. Together they must give the addresses of
|
|
// both, not only of the first that resolves, so that when one gives no
|
|
// usable reply the walk goes on to the other.
|
|
func TestResolveNSIPs_EveryNameserver(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
names := []string{"ns3.google.com.", "ns4.google.com."}
|
|
want := make([]string, 0, len(names))
|
|
|
|
for _, name := range names {
|
|
want = append(want, liveResolveNSIPs(t, r, []string{name}, 1)...)
|
|
}
|
|
|
|
got := liveResolveNSIPs(t, r, names, len(want))
|
|
|
|
assert.ElementsMatch(t, want, got)
|
|
}
|
|
|
|
// TestResolveNSIPs_ZoneDelegatedWithoutAddresses looks up the address
|
|
// of a.ntpns.org, a nameserver of pool.ntp.org. The org servers delegate
|
|
// ntpns.org to nameservers in other zones and give none of their
|
|
// addresses, so those are looked up on the way.
|
|
func TestResolveNSIPs_ZoneDelegatedWithoutAddresses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveNSIPs(t, r, []string{"a.ntpns.org."}, 1)
|
|
|
|
for _, ip := range ips {
|
|
assert.NotNil(t, net.ParseIP(ip), "should be valid IP: %s", ip)
|
|
}
|
|
}
|
|
|
|
// TestQueryZone_GivenAddressesFail asks the servers of ntp.org about
|
|
// pool.ntp.org, as the walk to a name under ntp.org does after the org
|
|
// servers' referral. That referral names four nameservers and gives an
|
|
// address for ns1.everett.org alone; here the given address is
|
|
// 192.0.2.1, where nothing answers, so the other three must be looked
|
|
// up and asked.
|
|
func TestQueryZone_GivenAddressesFail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var resp *dns.Msg
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryZone(192.0.2.1 and three ntp.org nameservers, pool.ntp.org)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
resp, err = r.QueryZone(
|
|
ctx, []string{"192.0.2.1"},
|
|
[]string{"anyns.pch.net.", "dns1.udel.edu.", "dns2.udel.edu."},
|
|
"ntp.org.", "pool.ntp.org.", dns.TypeNS, 0,
|
|
)
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
assert.NotEmpty(t, resolver.NSSetFrom(resp, "pool.ntp.org."))
|
|
}
|
|
|
|
// TestQueryZone_LookupDepth asks the servers of g.ntpns.org, a
|
|
// nameserver of pool.ntp.org, for its address, as looking that address
|
|
// up does when anyns.pch.net, one of the servers of ntpns.org, gives the
|
|
// referral to g.ntpns.org without addresses. Their addresses are looked
|
|
// up (here only a.ntpns.org's), and that needs a bitnames.com
|
|
// nameserver's address, as the org servers delegate ntpns.org without
|
|
// addresses. From depth 1, where looking up g.ntpns.org's address
|
|
// starts, that makes three lookups and the address is found. From one
|
|
// below maxLookupDepth, the bitnames.com lookup would be past the limit,
|
|
// so nothing can be asked, and the error says the limit is why.
|
|
func TestQueryZone_LookupDepth(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
withoutAddresses := []string{"a.ntpns.org."}
|
|
|
|
var resp *dns.Msg
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryZone(a.ntpns.org without its address, g.ntpns.org)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
resp, err = r.QueryZone(
|
|
ctx, nil, withoutAddresses, "g.ntpns.org.", "g.ntpns.org.",
|
|
dns.TypeA, 1,
|
|
)
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
assert.NotEmpty(t, resp.Answer)
|
|
|
|
var limitErr error
|
|
|
|
// Any other error is live DNS not answering, and is retried.
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryZone(a.ntpns.org without its address, g.ntpns.org, "+
|
|
"one below the limit)",
|
|
func(ctx context.Context) error {
|
|
_, limitErr = r.QueryZone(
|
|
ctx, nil, withoutAddresses, "g.ntpns.org.", "g.ntpns.org.",
|
|
dns.TypeA, resolver.MaxLookupDepth-1,
|
|
)
|
|
if limitErr == nil ||
|
|
errors.Is(limitErr, resolver.ErrLookupDepthExceeded) {
|
|
return nil
|
|
}
|
|
|
|
return limitErr
|
|
},
|
|
)
|
|
|
|
require.ErrorIs(t, limitErr, resolver.ErrLookupDepthExceeded)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// QueryNameserver tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestQueryNameserver_BasicA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp := liveQueryNameserver(t, r, ns, "www.google.com", "A", "CNAME")
|
|
|
|
require.NotNil(t, resp)
|
|
|
|
assert.Equal(t, resolver.StatusOK, resp.Status)
|
|
assert.Equal(t, ns, resp.Nameserver)
|
|
|
|
hasRecords := len(resp.Records["A"]) > 0 ||
|
|
len(resp.Records["CNAME"]) > 0
|
|
assert.True(t, hasRecords,
|
|
"expected A or CNAME records for www.google.com",
|
|
)
|
|
}
|
|
|
|
// TestQueryNameserver_ZoneDelegatedWithoutAddresses asks a.ntpns.org, a
|
|
// nameserver of pool.ntp.org, about pool.ntp.org, as the watcher does.
|
|
// The org servers delegate ntpns.org without the addresses of its
|
|
// nameservers, so finding a.ntpns.org's address needs a lookup inside
|
|
// the one QueryNameserver starts.
|
|
func TestQueryNameserver_ZoneDelegatedWithoutAddresses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
resp := liveQueryNameserver(t, r, "a.ntpns.org.", "pool.ntp.org", "A")
|
|
|
|
assert.Equal(t, resolver.StatusOK, resp.Status)
|
|
}
|
|
|
|
func TestQueryNameserver_AAAA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "cloudflare.com")
|
|
resp := liveQueryNameserver(t, r, ns, "cloudflare.com", "AAAA")
|
|
|
|
aaaaRecords := resp.Records["AAAA"]
|
|
require.NotEmpty(t, aaaaRecords,
|
|
"cloudflare.com should have AAAA records",
|
|
)
|
|
|
|
for _, ip := range aaaaRecords {
|
|
parsed := net.ParseIP(ip)
|
|
require.NotNil(t, parsed,
|
|
"should be valid IP: %s", ip,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestQueryNameserver_MX(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp := liveQueryNameserver(t, r, ns, "google.com", "MX")
|
|
|
|
mxRecords := resp.Records["MX"]
|
|
require.NotEmpty(t, mxRecords,
|
|
"google.com should have MX records",
|
|
)
|
|
}
|
|
|
|
func TestQueryNameserver_TXT(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp := liveQueryNameserver(t, r, ns, "google.com", "TXT")
|
|
|
|
txtRecords := resp.Records["TXT"]
|
|
require.NotEmpty(t, txtRecords,
|
|
"google.com should have TXT records",
|
|
)
|
|
|
|
hasSPF := false
|
|
|
|
for _, txt := range txtRecords {
|
|
if strings.Contains(txt, "v=spf1") {
|
|
hasSPF = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, hasSPF,
|
|
"google.com should have SPF TXT record",
|
|
)
|
|
}
|
|
|
|
// TestQueryNameserver_TruncatedReplyWhoseTCPRetryFails asks a google.com
|
|
// nameserver about google.com with a resolver whose retries over TCP
|
|
// fail. google.com's TXT records do not fit in a reply over UDP, so TXT
|
|
// is reported as failed, holding none of the records that fit, and
|
|
// logged with the reason, while the nameserver, which answered the other
|
|
// types, is ok.
|
|
func TestQueryNameserver_TruncatedReplyWhoseTCPRetryFails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ns := findOneNSForDomain(t, newTestResolver(t), "google.com")
|
|
|
|
var logs bytes.Buffer
|
|
|
|
r := resolver.NewWithFailingTCP(slog.New(slog.NewTextHandler(&logs, nil)))
|
|
resp := liveQueryNameserver(t, r, ns, "google.com")
|
|
|
|
assert.Equal(t, resolver.StatusOK, resp.Status)
|
|
assert.Contains(t, resp.FailedTypes, "TXT")
|
|
assert.NotContains(t, resp.Records, "TXT")
|
|
assert.Contains(t, logs.String(),
|
|
"hostname=google.com. nameserver="+ns+" type=TXT error=",
|
|
)
|
|
}
|
|
|
|
func TestQueryNameserver_NXDomain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp := liveQueryNameserver(
|
|
t, r, ns, "this-surely-does-not-exist-xyz.google.com",
|
|
)
|
|
|
|
assert.Equal(t, resolver.StatusNXDomain, resp.Status)
|
|
}
|
|
|
|
// TestQueryNameserver_Refused asks a google.com nameserver about
|
|
// cloudflare.com, a zone it does not serve, which it refuses. Refusing
|
|
// is a failure to answer, not an answer with no records.
|
|
func TestQueryNameserver_Refused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
|
|
var resp *resolver.NameserverResponse
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryNameserver("+ns+", cloudflare.com)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
resp, err = r.QueryNameserver(ctx, ns, "cloudflare.com")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A timeout or a network error is no reply at all.
|
|
if resp.Status == resolver.StatusTimeout ||
|
|
strings.HasPrefix(resp.Error, "network error") {
|
|
return fmt.Errorf(
|
|
"%w: %s: %s",
|
|
livednstest.ErrNoAnswer, ns, resp.Error,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
assert.Equal(t, resolver.StatusError, resp.Status)
|
|
assert.Equal(t, "server returned REFUSED", resp.Error)
|
|
}
|
|
|
|
// TestQueryServers_RecursiveResolverRefused passes a public recursive
|
|
// resolver to QueryServers as the server of google.com. These resolvers
|
|
// refuse a query that does not ask for recursion and answer one that
|
|
// does. The resolver never asks for recursion, so the query must be
|
|
// reported as refused, never answered. Each resolver is run by a
|
|
// different operator, and they are asked in turn until one replies, so
|
|
// one operator not answering does not fail the test.
|
|
func TestQueryServers_RecursiveResolverRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
resolvers := []string{
|
|
"64.6.64.6", "185.222.222.222", "4.2.2.1", "9.9.9.9",
|
|
}
|
|
|
|
var err error
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryServers(public recursive resolvers, google.com)",
|
|
func(ctx context.Context) error {
|
|
for _, ip := range resolvers {
|
|
_, err = r.QueryServers(
|
|
ctx, []string{ip}, "google.com.", "google.com.",
|
|
dns.TypeA,
|
|
)
|
|
|
|
// A refusal or an answer is a reply; anything else may
|
|
// be no reply at all, so the next resolver is asked.
|
|
if err == nil || errors.Is(err, resolver.ErrRefused) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("%w: %w", livednstest.ErrNoAnswer, err)
|
|
},
|
|
)
|
|
|
|
require.ErrorIs(t, err, resolver.ErrRefused)
|
|
}
|
|
|
|
// googleNameserverIPv4s returns the IPv4 addresses of google.com's
|
|
// nameservers, the only addresses the resolver asks servers at.
|
|
func googleNameserverIPv4s(t *testing.T, r *resolver.Resolver) []string {
|
|
t.Helper()
|
|
|
|
names := liveFindAuthoritative(t, r, "google.com")
|
|
|
|
return liveResolveNSIPs(t, r, names, len(names))
|
|
}
|
|
|
|
// TestQueryServers_EveryServerRefused asks all of google.com's
|
|
// nameservers about cloudflare.com, a zone they do not serve, which
|
|
// they all refuse. The error says every server refused; it is not
|
|
// ErrIntercepted, which only the root servers refusing shows.
|
|
func TestQueryServers_EveryServerRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
servers := googleNameserverIPv4s(t, r)
|
|
|
|
var err error
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryServers(google.com servers, cloudflare.com)",
|
|
func(ctx context.Context) error {
|
|
_, err = r.QueryServers(
|
|
ctx, servers, "google.com.", "cloudflare.com.",
|
|
dns.TypeNS,
|
|
)
|
|
|
|
// When not every server refused, one may have given no
|
|
// reply at all, so the attempt is tried again.
|
|
if err != nil &&
|
|
!strings.HasPrefix(err.Error(), "every server of") {
|
|
return fmt.Errorf(
|
|
"%w: %w", livednstest.ErrNoAnswer, err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
require.ErrorIs(t, err, resolver.ErrRefused)
|
|
require.NotErrorIs(t, err, resolver.ErrIntercepted)
|
|
require.EqualError(
|
|
t, err,
|
|
"every server of google.com. refused a query for "+
|
|
"cloudflare.com.: dns query refused",
|
|
)
|
|
}
|
|
|
|
// TestQueryServers_EveryRootServerRefused passes google.com's
|
|
// nameservers to QueryServers as the servers of the root zone. They
|
|
// refuse a query about cloudflare.com, as root servers would if
|
|
// something on the network answered in their place, so the error is
|
|
// ErrIntercepted.
|
|
func TestQueryServers_EveryRootServerRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
servers := googleNameserverIPv4s(t, r)
|
|
|
|
var err error
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryServers(google.com servers as root servers, cloudflare.com)",
|
|
func(ctx context.Context) error {
|
|
_, err = r.QueryServers(
|
|
ctx, servers, ".", "cloudflare.com.", dns.TypeNS,
|
|
)
|
|
|
|
// When not every server refused, one may have given no
|
|
// reply at all, so the attempt is tried again. Both errors
|
|
// for every server refusing say "refused a query for".
|
|
if err != nil &&
|
|
!strings.Contains(err.Error(), "refused a query for") {
|
|
return fmt.Errorf(
|
|
"%w: %w", livednstest.ErrNoAnswer, err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
require.ErrorIs(t, err, resolver.ErrIntercepted)
|
|
require.EqualError(
|
|
t, err,
|
|
"every root server refused a query for cloudflare.com.: "+
|
|
"this network intercepts DNS queries",
|
|
)
|
|
}
|
|
|
|
// TestQueryServers_NotEveryRootServerRefused passes google.com's
|
|
// nameservers and 192.0.2.1 to QueryServers as the servers of the root
|
|
// zone. The google.com nameservers refuse a query about cloudflare.com,
|
|
// but nothing answers at 192.0.2.1, a documentation address, so not
|
|
// every server refused, wherever 192.0.2.1 falls in the random order:
|
|
// the error is not ErrIntercepted and does not say every server refused.
|
|
func TestQueryServers_NotEveryRootServerRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
servers := googleNameserverIPv4s(t, r)
|
|
servers = append(servers, "192.0.2.1")
|
|
|
|
var err error
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryServers(google.com servers and 192.0.2.1, cloudflare.com)",
|
|
func(ctx context.Context) error {
|
|
_, err = r.QueryServers(
|
|
ctx, servers, ".", "cloudflare.com.", dns.TypeNS,
|
|
)
|
|
|
|
// An attempt that ran out of time may not have asked every
|
|
// server, so it is tried again.
|
|
if ctx.Err() != nil {
|
|
return fmt.Errorf(
|
|
"%w: %w", livednstest.ErrNoAnswer, err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
require.Error(t, err)
|
|
require.NotErrorIs(t, err, resolver.ErrIntercepted)
|
|
// Both errors for every server refusing say "refused a query for".
|
|
require.NotContains(t, err.Error(), "refused a query for")
|
|
}
|
|
|
|
func TestQueryNameserver_RecordsSorted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp := liveQueryNameserver(t, r, ns, "google.com")
|
|
|
|
for recordType, values := range resp.Records {
|
|
assert.True(
|
|
t,
|
|
sort.StringsAreSorted(values),
|
|
"%s records should be sorted", recordType,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestQueryNameserver_ResponseIncludesNameserver(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "cloudflare.com")
|
|
resp := liveQueryNameserver(t, r, ns, "cloudflare.com")
|
|
|
|
assert.Equal(t, ns, resp.Nameserver)
|
|
}
|
|
|
|
func TestQueryNameserver_EmptyRecordsOnNXDomain(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp := liveQueryNameserver(
|
|
t, r, ns, "this-surely-does-not-exist-xyz.google.com",
|
|
)
|
|
|
|
totalRecords := 0
|
|
for _, values := range resp.Records {
|
|
totalRecords += len(values)
|
|
}
|
|
|
|
assert.Zero(t, totalRecords)
|
|
}
|
|
|
|
func TestQueryNameserver_TrailingDotHandling(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "google.com")
|
|
resp1 := liveQueryNameserver(t, r, ns, "google.com")
|
|
resp2 := liveQueryNameserver(t, r, ns, "google.com.")
|
|
|
|
assert.Equal(t, resp1.Status, resp2.Status)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// QueryAllNameservers tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestQueryAllNameservers_ReturnsAllNS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
results := liveQueryAllNameservers(t, r, "google.com")
|
|
|
|
assert.GreaterOrEqual(t, len(results), minNameservers)
|
|
|
|
for ns, resp := range results {
|
|
assert.Equal(t, ns, resp.Nameserver)
|
|
}
|
|
}
|
|
|
|
func TestQueryAllNameservers_AllReturnOK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// The last two names are in zones other than their last two
|
|
// labels: google.co.uk, under the two-label suffix co.uk, and
|
|
// compute-1.amazonaws.com, which amazonaws.com delegates to other
|
|
// servers and which has a host name for each of its addresses.
|
|
// Servers above a name's zone only refer onward, which gives
|
|
// nodata, so ok shows the name was asked at its own zone's
|
|
// servers.
|
|
hostnames := []string{
|
|
"google.com",
|
|
"www.google.co.uk",
|
|
"ec2-3-80-0-1.compute-1.amazonaws.com",
|
|
}
|
|
|
|
for _, hostname := range hostnames {
|
|
t.Run(hostname, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
results := liveQueryAllNameservers(t, r, hostname)
|
|
|
|
// A quorum, not unanimity: one authoritative server
|
|
// being slow or rate-limiting us is a property of the
|
|
// live internet, not a resolver defect.
|
|
assert.GreaterOrEqual(
|
|
t,
|
|
countStatus(results, resolver.StatusOK),
|
|
liveQuorum(len(results)),
|
|
"a quorum of nameservers should answer OK: %s",
|
|
describeStatuses(results),
|
|
)
|
|
|
|
// Quorum tolerates SILENCE only. Every individual
|
|
// result must be either the expected answer or a
|
|
// non-answer: ok, timeout or error, and nothing else.
|
|
// Stated as a closed allowlist so that a wrong answer
|
|
// no one thought to ban — nxdomain and nodata today,
|
|
// any status added later — fails here rather than
|
|
// sliding through under the quorum.
|
|
assert.Empty(
|
|
t,
|
|
unsanctionedStatuses(
|
|
results,
|
|
resolver.StatusOK,
|
|
resolver.StatusTimeout,
|
|
resolver.StatusError,
|
|
),
|
|
"every nameserver must answer OK or not answer "+
|
|
"at all: %s",
|
|
describeStatuses(results),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryAllNameservers_NXDomainFromAllNS(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
results := liveQueryAllNameservers(
|
|
t, r, "this-surely-does-not-exist-xyz.google.com",
|
|
)
|
|
|
|
assert.GreaterOrEqual(
|
|
t,
|
|
countStatus(results, resolver.StatusNXDomain),
|
|
liveQuorum(len(results)),
|
|
"a quorum of nameservers should report NXDOMAIN: %s",
|
|
describeStatuses(results),
|
|
)
|
|
|
|
// Silence is tolerated; any actual answer other than NXDOMAIN
|
|
// is not. Closed allowlist for the same reason as above: a
|
|
// server answering `ok` or `nodata` for a name that must not
|
|
// exist is a wrong answer, not a slow one.
|
|
assert.Empty(
|
|
t,
|
|
unsanctionedStatuses(
|
|
results,
|
|
resolver.StatusNXDomain,
|
|
resolver.StatusTimeout,
|
|
resolver.StatusError,
|
|
),
|
|
"every nameserver must report NXDOMAIN or not answer "+
|
|
"at all: %s",
|
|
describeStatuses(results),
|
|
)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// LookupNS tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestLookupNS_ValidDomain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
nameservers := liveLookupNS(t, r, "google.com")
|
|
|
|
for _, ns := range nameservers {
|
|
assert.True(t, strings.HasSuffix(ns, "."),
|
|
"NS should have trailing dot: %s", ns,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestLookupNS_Sorted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
nameservers := liveLookupNS(t, r, "google.com")
|
|
|
|
assert.True(t, sort.StringsAreSorted(nameservers))
|
|
}
|
|
|
|
func TestLookupNS_MatchesFindAuthoritative(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
fromLookup := liveLookupNS(t, r, "google.com")
|
|
fromFind := liveFindAuthoritative(t, r, "google.com")
|
|
|
|
assert.Equal(t, fromFind, fromLookup)
|
|
}
|
|
|
|
// TestLookupNS_ParentZoneDelegatedWithoutAddresses looks up the
|
|
// nameservers of g.ntpns.org. The org servers delegate its parent zone,
|
|
// ntpns.org, without the addresses of its nameservers, so the walk has
|
|
// to look them up to ask them. If it did not, the walk for g.ntpns.org
|
|
// would fail.
|
|
func TestLookupNS_ParentZoneDelegatedWithoutAddresses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
nameservers := liveLookupNS(t, r, "g.ntpns.org")
|
|
|
|
assert.Contains(t, nameservers, "a.ntpns.org.")
|
|
}
|
|
|
|
// TestLookupNS_DomainThatDoesNotExist looks up the nameservers of a .com
|
|
// domain that does not exist. The .com servers answer NXDOMAIN, so the
|
|
// error is ErrNXDomain, and the domain does not get their names.
|
|
func TestLookupNS_DomainThatDoesNotExist(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var (
|
|
nameservers []string
|
|
err error
|
|
)
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"LookupNS("+nonexistentDomain+")",
|
|
func(ctx context.Context) error {
|
|
nameservers, err = r.LookupNS(ctx, nonexistentDomain)
|
|
if errors.Is(err, resolver.ErrNXDomain) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
require.ErrorIs(t, err, resolver.ErrNXDomain)
|
|
assert.Empty(t, nameservers)
|
|
}
|
|
|
|
// TestLookupNS_NoDelegationOfItsOwn looks up the nameservers of
|
|
// www.google.com, a name in the google.com zone with no delegation of
|
|
// its own, as a domain such as octocat.github.io is. The google.com
|
|
// servers answer with no NS records for it: the set is empty, and it is
|
|
// not ErrNXDomain.
|
|
func TestLookupNS_NoDelegationOfItsOwn(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var nameservers []string
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"LookupNS(www.google.com)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
nameservers, err = r.LookupNS(ctx, "www.google.com")
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
assert.Empty(t, nameservers)
|
|
}
|
|
|
|
// TestFollowDelegation_NoAnswer starts the walk LookupNS uses, for
|
|
// google.com, at 192.0.2.1, a documentation address where nothing
|
|
// answers. A walk that got no answer is an error, not an empty set,
|
|
// which the watcher would report as an NS Change with every nameserver
|
|
// removed.
|
|
func TestFollowDelegation_NoAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := resolver.NewWithQueryTimeout(slog.Default(), 100*time.Millisecond)
|
|
|
|
nameservers, err := r.FollowDelegation(
|
|
t.Context(), "google.com.", []string{noAnswerAddress},
|
|
)
|
|
require.Error(t, err)
|
|
assert.Empty(t, nameservers)
|
|
}
|
|
|
|
// TestResolveNSIterative_NoDelegationOfItsOwn walks to the nameservers
|
|
// of www.google.com as the fallback walk does. As in
|
|
// TestLookupNS_NoDelegationOfItsOwn, the set is empty, with no error.
|
|
func TestResolveNSIterative_NoDelegationOfItsOwn(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var nameservers []string
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"ResolveNSIterative(www.google.com)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
nameservers, err = r.ResolveNSIterative(ctx, "www.google.com")
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
assert.Empty(t, nameservers)
|
|
}
|
|
|
|
// TestResolveNSIterative_DomainThatDoesNotExist walks to the nameservers
|
|
// of a .com domain that does not exist as the fallback walk does. As in
|
|
// TestLookupNS_DomainThatDoesNotExist, the error is ErrNXDomain.
|
|
func TestResolveNSIterative_DomainThatDoesNotExist(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var err error
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"ResolveNSIterative("+nonexistentDomain+")",
|
|
func(ctx context.Context) error {
|
|
_, err = r.ResolveNSIterative(ctx, nonexistentDomain)
|
|
if errors.Is(err, resolver.ErrNXDomain) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
require.ErrorIs(t, err, resolver.ErrNXDomain)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// ResolveIPAddresses tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestResolveIPAddresses_ReturnsIPs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveIPs(t, r, "google.com")
|
|
|
|
for _, ip := range ips {
|
|
parsed := net.ParseIP(ip)
|
|
assert.NotNil(t, parsed,
|
|
"should be valid IP: %s", ip,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestResolveIPAddresses_Deduplicated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveIPs(t, r, "google.com")
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
for _, ip := range ips {
|
|
assert.False(t, seen[ip], "duplicate IP: %s", ip)
|
|
seen[ip] = true
|
|
}
|
|
}
|
|
|
|
func TestResolveIPAddresses_Sorted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveIPs(t, r, "google.com")
|
|
|
|
assert.True(t, sort.StringsAreSorted(ips))
|
|
}
|
|
|
|
func TestResolveIPAddresses_NXDomainReturnsEmpty(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveIPsAllowingEmpty(
|
|
t, r, "this-surely-does-not-exist-xyz.google.com",
|
|
)
|
|
|
|
assert.Empty(t, ips)
|
|
}
|
|
|
|
func TestResolveIPAddresses_CloudflareDomain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveIPs(t, r, "cloudflare.com")
|
|
|
|
assert.NotEmpty(t, ips)
|
|
}
|
|
|
|
// TestResolveIPAddresses_NameserverIPv4AndIPv6 looks up the addresses of
|
|
// one of cloudflare.com's nameservers, as a domain check does for each
|
|
// nameserver. That name has A and AAAA records, so both kinds of address
|
|
// come back.
|
|
func TestResolveIPAddresses_NameserverIPv4AndIPv6(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ns := findOneNSForDomain(t, r, "cloudflare.com")
|
|
ips := liveResolveIPs(t, r, ns)
|
|
|
|
var ipv4, ipv6 int
|
|
|
|
for _, ip := range ips {
|
|
parsed := net.ParseIP(ip)
|
|
require.NotNil(t, parsed, "should be valid IP: %s", ip)
|
|
|
|
if parsed.To4() != nil {
|
|
ipv4++
|
|
} else {
|
|
ipv6++
|
|
}
|
|
}
|
|
|
|
assert.Positive(t, ipv4, "no IPv4 address for %s: %v", ns, ips)
|
|
assert.Positive(t, ipv6, "no IPv6 address for %s: %v", ns, ips)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Context cancellation tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestFindAuthoritativeNameservers_ContextCanceled(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := r.FindAuthoritativeNameservers(ctx, "google.com")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestQueryNameserver_ContextCanceled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := r.QueryNameserver(
|
|
ctx, "ns1.google.com.", "google.com",
|
|
)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestQueryAllNameservers_ContextCanceled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := r.QueryAllNameservers(ctx, "google.com")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// TestQueryEachNS_CanceledDuringQuery cancels the context while a
|
|
// nameserver is being queried, as shutdown does. A lookup cut short
|
|
// says nothing about the nameserver, so it must return an error, not a
|
|
// failed response for it.
|
|
func TestQueryEachNS_CanceledDuringQuery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
|
|
// Finding the nameserver's address alone starts at the root
|
|
// servers and takes several round trips, so a cancel a few
|
|
// milliseconds in lands during the query.
|
|
time.AfterFunc(5*time.Millisecond, cancel)
|
|
|
|
results, err := r.QueryEachNS(
|
|
ctx, []string{"ns1.google.com."}, "google.com",
|
|
)
|
|
require.ErrorIs(t, err, resolver.ErrContextCanceled)
|
|
assert.Nil(t, results)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Timeout tests
|
|
// ----------------------------------------------------------------
|
|
|
|
func TestQueryNameserverIP_Timeout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
// Nothing answers at 192.0.2.1, a documentation address. The
|
|
// resolver tries each query twice, and the first try gives up
|
|
// after two seconds. A deadline that ends during the first try
|
|
// makes the status vary from run to run between error and
|
|
// timeout, so the deadline must outlast the first try.
|
|
ctx, cancel := context.WithTimeout(
|
|
context.Background(), 3*time.Second,
|
|
)
|
|
t.Cleanup(cancel)
|
|
|
|
resp, err := r.QueryNameserverIP(
|
|
ctx, "unreachable.test.", "192.0.2.1",
|
|
"example.com",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resolver.StatusTimeout, resp.Status)
|
|
assert.NotEmpty(t, resp.Error)
|
|
}
|
|
|
|
// TestQueryNameserverIP_CancelledLogsNothing cancels the context while
|
|
// a query to 192.0.2.1, where nothing answers, is waiting for a reply,
|
|
// as shutdown does. The query was cut short, not failed, so nothing is
|
|
// logged.
|
|
func TestQueryNameserverIP_CancelledLogsNothing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var logs bytes.Buffer
|
|
|
|
r := resolver.NewFromLogger(slog.New(slog.NewTextHandler(&logs, nil)))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
time.AfterFunc(100*time.Millisecond, cancel)
|
|
|
|
_, err := r.QueryNameserverIP(
|
|
ctx, "unreachable.test.", "192.0.2.1", "example.com",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, logs.String())
|
|
}
|
|
|
|
// TestCollectIPs_NoNameserverAnswered takes the response of a
|
|
// nameserver at 192.0.2.1, where nothing answers, as
|
|
// TestQueryNameserverIP_Timeout does. Addresses collected from
|
|
// nameservers that all failed to answer are an error, not none.
|
|
func TestCollectIPs_NoNameserverAnswered(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
// The deadline outlasts the first try, as in
|
|
// TestQueryNameserverIP_Timeout.
|
|
ctx, cancel := context.WithTimeout(
|
|
context.Background(), 3*time.Second,
|
|
)
|
|
t.Cleanup(cancel)
|
|
|
|
resp, err := r.QueryNameserverIP(
|
|
ctx, "unreachable.test.", "192.0.2.1",
|
|
"example.com",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
ips, _, err := resolver.CollectIPs(
|
|
map[string]*resolver.NameserverResponse{resp.Nameserver: resp},
|
|
)
|
|
require.ErrorIs(t, err, resolver.ErrNoNameserverAnswered)
|
|
assert.Empty(t, ips)
|
|
}
|
|
|
|
// TestCollectIPs_ReferralIsNoAnswer asks a root server about
|
|
// example.com, which the root zone does not hold, so it only refers the
|
|
// query to the com servers. That reply is no answer, as is a parent
|
|
// zone's when every server of the name's own zone failed.
|
|
func TestCollectIPs_ReferralIsNoAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var resp *resolver.NameserverResponse
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryNameserverIP(a.root-servers.net, example.com)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
resp, err = r.QueryNameserverIP(
|
|
ctx, "a.root-servers.net.", "198.41.0.4",
|
|
"example.com",
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A timeout or a network error is no reply at all.
|
|
if resp.Status == resolver.StatusTimeout ||
|
|
strings.HasPrefix(resp.Error, "network error") {
|
|
return fmt.Errorf(
|
|
"%w: %s", livednstest.ErrNoAnswer, resp.Error,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
assert.Equal(t, resolver.StatusError, resp.Status)
|
|
assert.Equal(t, "server returned a referral", resp.Error)
|
|
|
|
ips, _, err := resolver.CollectIPs(
|
|
map[string]*resolver.NameserverResponse{resp.Nameserver: resp},
|
|
)
|
|
require.ErrorIs(t, err, resolver.ErrNoNameserverAnswered)
|
|
assert.Empty(t, ips)
|
|
}
|
|
|
|
func TestResolveIPAddresses_ContextCanceled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := r.ResolveIPAddresses(ctx, "google.com")
|
|
assert.Error(t, err)
|
|
}
|