check / check (push) Canceled after 0s
Looking up a nameserver's own address followed only the addresses a referral gave, so a nameserver whose zone is delegated without them, such as a.ntpns.org of pool.ntp.org, never resolved. The walk to a name's nameservers looked addresses up only when a referral gave none, so when it gave some it asked only those. Both now ask every nameserver a referral names, looking up the addresses it lacks. A lookup can start further lookups; maxLookupDepth stops them two deep, so zones delegated to each other's nameservers, as desec.io and desec.org are, still end. A referral that gives addresses for only some nameservers now costs one lookup per nameserver without one. Model: opus-5-5
1057 lines
26 KiB
Go
1057 lines
26 KiB
Go
package resolver_test
|
|
|
|
import (
|
|
"context"
|
|
"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)
|
|
}
|
|
}
|
|
|
|
// TestResolveNSIPs_DelegationsPointAtEachOther looks up the address of
|
|
// ns1.desec.io. The io servers delegate desec.io to ns1.desec.io, with
|
|
// its address, and to ns2.desec.org, without; the org servers delegate
|
|
// desec.org to ns2.desec.org, with its address, and to ns1.desec.io,
|
|
// without. Looking up either address starts a lookup of the other, and
|
|
// the lookup must still end.
|
|
func TestResolveNSIPs_DelegationsPointAtEachOther(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
ips := liveResolveNSIPs(t, r, []string{"ns1.desec.io."}, 1)
|
|
|
|
for _, ip := range ips {
|
|
assert.NotNil(t, net.ParseIP(ip), "should be valid IP: %s", ip)
|
|
}
|
|
}
|
|
|
|
// givenAddresses returns the IPv4 addresses that resp, a referral,
|
|
// gives for the nameservers it names.
|
|
func givenAddresses(resp *dns.Msg) []string {
|
|
var ips []string
|
|
|
|
for _, rr := range resp.Extra {
|
|
if a, ok := rr.(*dns.A); ok {
|
|
ips = append(ips, a.A.String())
|
|
}
|
|
}
|
|
|
|
return ips
|
|
}
|
|
|
|
// TestReferralServers_SomeWithoutAddresses asks the org servers about
|
|
// ntp.org, as the walk to a name under ntp.org does. Their referral
|
|
// names four nameservers and gives an address for ns1.everett.org
|
|
// alone. The addresses of the other three are looked up, so that the
|
|
// walk can ask them too.
|
|
func TestReferralServers_SomeWithoutAddresses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var referral *dns.Msg
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"referral for ntp.org from the org servers",
|
|
func(ctx context.Context) error {
|
|
fromRoot, err := r.QueryServers(
|
|
ctx, resolver.RootServerList(), ".", "ntp.org.",
|
|
dns.TypeNS,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
referral, err = r.QueryServers(
|
|
ctx, givenAddresses(fromRoot), "org.", "ntp.org.",
|
|
dns.TypeNS,
|
|
)
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
given := givenAddresses(referral)
|
|
require.NotEmpty(t, given, "the referral gives no addresses")
|
|
|
|
var servers []string
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"ReferralServers(referral for ntp.org)",
|
|
func(ctx context.Context) error {
|
|
servers = r.ReferralServers(ctx, referral)
|
|
if len(servers) <= len(given) {
|
|
return fmt.Errorf(
|
|
"%w: %d addresses, the referral gives %d",
|
|
livednstest.ErrNoAnswer, len(servers), len(given),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
assert.Subset(t, servers, given)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// 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",
|
|
)
|
|
}
|
|
|
|
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",
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// TestQueryNameserverIP_RecursiveResolverRefused asks Quad9, a public
|
|
// recursive resolver, about google.com at both of its addresses. Quad9
|
|
// refuses a query that does not ask for recursion and answers one that
|
|
// does. The resolver never asks for recursion, so it must be reported
|
|
// as refusing, never as answering.
|
|
func TestQueryNameserverIP_RecursiveResolverRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
for _, ip := range []string{"9.9.9.9", "149.112.112.112"} {
|
|
var resp *resolver.NameserverResponse
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryNameserverIP("+ip+", google.com)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
resp, err = r.QueryNameserverIP(
|
|
ctx, ip, ip, "google.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, ip, resp.Error,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
assert.Equal(t, resolver.StatusError, resp.Status, ip)
|
|
assert.Equal(t, "server returned REFUSED", resp.Error, ip)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|