check / check (push) Successful in 1m7s
The test asked one operator's recursive resolver for eight record types at each of its two addresses, and passed only when all eight were refused within one attempt; when that operator stopped answering, next went red. It is now TestQueryServers_RecursiveResolverRefused: through the existing QueryServers test export it sends one A query to public resolvers of four operators in turn, inside livednstest.Retry, moving on when one gives no reply. Each refuses a query not asking for recursion and answers one that does, so putting the resend asking for recursion back still fails the test at once. Model: opus-5-5
1135 lines
30 KiB
Go
1135 lines
30 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
|
|
// ----------------------------------------------------------------
|
|
|
|
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)
|
|
}
|
|
|
|
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 and LookupNS would return the nameservers of ntpns.org,
|
|
// which a.ntpns.org is not one of.
|
|
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.")
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// 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)
|
|
}
|