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 the nameservers whose addresses the referral gives first and, if none of them gives a usable reply, look up and ask the others; with no addresses given, all are looked up, as before. Looking up all of them at once sent too many queries to the root servers. maxLookupDepth stops lookups two deep, so delegations that point at each other still end. Model: opus-5-5
1033 lines
26 KiB
Go
1033 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)
|
|
}
|
|
}
|
|
|
|
// TestQueryNameservers_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 TestQueryNameservers_GivenAddressesFail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
|
|
var resp *dns.Msg
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryNameservers(192.0.2.1 and three ntp.org nameservers, "+
|
|
"pool.ntp.org)",
|
|
func(ctx context.Context) error {
|
|
var err error
|
|
|
|
resp, err = r.QueryNameservers(
|
|
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."))
|
|
}
|
|
|
|
// TestQueryNameservers_LookupDepth asks the servers of desec.io about
|
|
// ns1.desec.io, giving no address and naming ns1.desec.io without one.
|
|
// Below maxLookupDepth its address is looked up and it is asked. At the
|
|
// limit it is not, so nothing can be asked: this is where lookups of
|
|
// nameserver addresses stop when delegations point at each other.
|
|
func TestQueryNameservers_LookupDepth(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newTestResolver(t)
|
|
withoutAddresses := []string{"ns1.desec.io."}
|
|
|
|
livednstest.Retry(
|
|
t,
|
|
"QueryNameservers(ns1.desec.io without its address, ns1.desec.io)",
|
|
func(ctx context.Context) error {
|
|
_, err := r.QueryNameservers(
|
|
ctx, nil, withoutAddresses, "desec.io.", "ns1.desec.io.",
|
|
dns.TypeA, 0,
|
|
)
|
|
|
|
return err
|
|
},
|
|
)
|
|
|
|
_, err := r.QueryNameservers(
|
|
t.Context(), nil, withoutAddresses, "desec.io.", "ns1.desec.io.",
|
|
dns.TypeA, resolver.MaxLookupDepth,
|
|
)
|
|
require.ErrorIs(t, err, resolver.ErrNoNameservers)
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// 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)
|
|
}
|