resolver: ask a referral's nameservers that come without addresses (closes #221)
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.
Both now go through queryZone, which asks the nameservers whose
addresses the referral gives first and, if none of them gives a usable
reply, looks up and asks the others. maxLookupDepth stops lookups three
deep, so delegations that point at each other still end; when the limit
is why no address was found, the error is ErrLookupDepthExceeded, not
"no address".

Model: opus-5-5
This commit was merged in pull request #242.
This commit is contained in:
2026-10-02 10:12:08 +02:00
parent e46db71821
commit 008ec5d13a
6 changed files with 300 additions and 46 deletions
+136
View File
@@ -2,6 +2,7 @@ package resolver_test
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
@@ -162,6 +163,112 @@ func TestResolveNSIPs_EveryNameserver(t *testing.T) {
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
// ----------------------------------------------------------------
@@ -185,6 +292,20 @@ func TestQueryNameserver_BasicA(t *testing.T) {
)
}
// 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()
@@ -681,6 +802,21 @@ func TestLookupNS_MatchesFindAuthoritative(t *testing.T) {
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
// ----------------------------------------------------------------