resolver: query a hostname at its own zone's servers (closes #189) #191
@@ -61,7 +61,9 @@ rejected.
|
|||||||
- Accepts a list of DNS hostnames (subdomains, distinguished from apex
|
- Accepts a list of DNS hostnames (subdomains, distinguished from apex
|
||||||
domains via the Public Suffix List).
|
domains via the Public Suffix List).
|
||||||
- Every **1 hour**, performs a full iterative trace to discover the
|
- Every **1 hour**, performs a full iterative trace to discover the
|
||||||
authoritative nameservers for the hostname's parent domain.
|
authoritative nameservers of the zone the hostname is in, which is not
|
||||||
|
always its last two labels (a name under `co.uk`, or in a delegated
|
||||||
|
subdomain).
|
||||||
- Queries **each** authoritative nameserver independently for **all**
|
- Queries **each** authoritative nameserver independently for **all**
|
||||||
record types: A, AAAA, CNAME, MX, TXT, SRV, CAA, NS.
|
record types: A, AAAA, CNAME, MX, TXT, SRV, CAA, NS.
|
||||||
- Stores results **per nameserver**. The state for a hostname is not a
|
- Stores results **per nameserver**. The state for a hostname is not a
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: a hostname is queried at the servers of the zone it is in, found
|
||||||
|
by following delegations for the name, not its last two labels (closes #189).
|
||||||
- 2026-10-01: each nameserver's addresses are saved with its domain, and a
|
- 2026-10-01: each nameserver's addresses are saved with its domain, and a
|
||||||
change while it stays in the delegation is notified (closes #105).
|
change while it stays in the delegation is notified (closes #105).
|
||||||
- 2026-10-01: the watcher saves state when it stops, and shutdown waits for that
|
- 2026-10-01: the watcher saves state when it stops, and shutdown waits for that
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const (
|
|||||||
maxRetries = 2
|
maxRetries = 2
|
||||||
maxDelegation = 20
|
maxDelegation = 20
|
||||||
timeoutMultiplier = 2
|
timeoutMultiplier = 2
|
||||||
minDomainLabels = 2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrRefused is returned when a DNS server refuses a query.
|
// ErrRefused is returned when a DNS server refuses a query.
|
||||||
@@ -225,6 +224,15 @@ func (r *Resolver) followDelegation(
|
|||||||
return ansNS, nil
|
return ansNS, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An authoritative reply comes from the servers of the zone
|
||||||
|
// domain is in; it is not a referral, even when its authority
|
||||||
|
// section lists that zone's NS records. Without NS records in
|
||||||
|
// the answer, domain is not the zone's apex and has no
|
||||||
|
// nameservers of its own.
|
||||||
|
if resp.Authoritative {
|
||||||
|
return nil, ErrNoNameservers
|
||||||
|
}
|
||||||
|
|
||||||
authNS := extractNSSet(resp.Ns)
|
authNS := extractNSSet(resp.Ns)
|
||||||
if len(authNS) == 0 {
|
if len(authNS) == 0 {
|
||||||
return r.resolveNSIterative(ctx, domain)
|
return r.resolveNSIterative(ctx, domain)
|
||||||
@@ -407,7 +415,9 @@ func (r *Resolver) resolveARecord(
|
|||||||
|
|
||||||
// FindAuthoritativeNameservers traces the delegation chain from
|
// FindAuthoritativeNameservers traces the delegation chain from
|
||||||
// root servers to discover all authoritative nameservers for the
|
// root servers to discover all authoritative nameservers for the
|
||||||
// given domain. Walks up the label hierarchy for subdomains.
|
// given domain. For a name that is not a zone apex it tries each
|
||||||
|
// parent name in turn, so it returns the nameservers of the zone the
|
||||||
|
// name is in.
|
||||||
func (r *Resolver) FindAuthoritativeNameservers(
|
func (r *Resolver) FindAuthoritativeNameservers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
domain string,
|
domain string,
|
||||||
@@ -653,22 +663,8 @@ func extractRecordValue(rr dns.RR) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parentDomain returns the registerable parent domain.
|
// QueryAllNameservers discovers the auth NSes of the zone the
|
||||||
func parentDomain(hostname string) string {
|
// hostname is in, then queries each one independently.
|
||||||
hostname = dns.Fqdn(strings.ToLower(hostname))
|
|
||||||
labels := dns.SplitDomainName(hostname)
|
|
||||||
|
|
||||||
if len(labels) <= minDomainLabels {
|
|
||||||
return strings.Join(labels, ".") + "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(
|
|
||||||
labels[len(labels)-minDomainLabels:], ".",
|
|
||||||
) + "."
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryAllNameservers discovers auth NSes for the hostname's
|
|
||||||
// parent domain, then queries each one independently.
|
|
||||||
func (r *Resolver) QueryAllNameservers(
|
func (r *Resolver) QueryAllNameservers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
hostname string,
|
hostname string,
|
||||||
@@ -677,9 +673,7 @@ func (r *Resolver) QueryAllNameservers(
|
|||||||
return nil, ErrContextCanceled
|
return nil, ErrContextCanceled
|
||||||
}
|
}
|
||||||
|
|
||||||
parent := parentDomain(hostname)
|
nameservers, err := r.FindAuthoritativeNameservers(ctx, hostname)
|
||||||
|
|
||||||
nameservers, err := r.FindAuthoritativeNameservers(ctx, parent)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ func TestFindAuthoritativeNameservers_Subdomain(
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
r := newTestResolver(t)
|
r := newTestResolver(t)
|
||||||
nameservers := liveFindAuthoritative(t, r, "www.google.com")
|
fromHost := liveFindAuthoritative(t, r, "www.google.com")
|
||||||
|
fromZone := liveFindAuthoritative(t, r, "google.com")
|
||||||
|
|
||||||
assert.NotEmpty(t, nameservers)
|
assert.Equal(t, fromZone, fromHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindAuthoritativeNameservers_ReturnsSorted(
|
func TestFindAuthoritativeNameservers_ReturnsSorted(
|
||||||
@@ -350,12 +351,29 @@ func TestQueryAllNameservers_ReturnsAllNS(t *testing.T) {
|
|||||||
func TestQueryAllNameservers_AllReturnOK(t *testing.T) {
|
func TestQueryAllNameservers_AllReturnOK(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
r := newTestResolver(t)
|
// The last two names are in zones other than their last two
|
||||||
results := liveQueryAllNameservers(t, r, "google.com")
|
// 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",
|
||||||
|
}
|
||||||
|
|
||||||
// A quorum, not unanimity: one authoritative server being
|
for _, hostname := range hostnames {
|
||||||
// slow or rate-limiting us is a property of the live
|
t.Run(hostname, func(t *testing.T) {
|
||||||
// internet, not a resolver defect.
|
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(
|
assert.GreaterOrEqual(
|
||||||
t,
|
t,
|
||||||
countStatus(results, resolver.StatusOK),
|
countStatus(results, resolver.StatusOK),
|
||||||
@@ -364,12 +382,13 @@ func TestQueryAllNameservers_AllReturnOK(t *testing.T) {
|
|||||||
describeStatuses(results),
|
describeStatuses(results),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Quorum tolerates SILENCE only. Every individual result must
|
// Quorum tolerates SILENCE only. Every individual
|
||||||
// be either the expected answer or a non-answer: ok, timeout
|
// result must be either the expected answer or a
|
||||||
// or error, and nothing else. Stated as a closed allowlist so
|
// non-answer: ok, timeout or error, and nothing else.
|
||||||
// that a wrong answer no one thought to ban — nxdomain and
|
// Stated as a closed allowlist so that a wrong answer
|
||||||
// nodata today, any status added later — fails here rather
|
// no one thought to ban — nxdomain and nodata today,
|
||||||
// than sliding through under the quorum.
|
// any status added later — fails here rather than
|
||||||
|
// sliding through under the quorum.
|
||||||
assert.Empty(
|
assert.Empty(
|
||||||
t,
|
t,
|
||||||
unsanctionedStatuses(
|
unsanctionedStatuses(
|
||||||
@@ -378,9 +397,12 @@ func TestQueryAllNameservers_AllReturnOK(t *testing.T) {
|
|||||||
resolver.StatusTimeout,
|
resolver.StatusTimeout,
|
||||||
resolver.StatusError,
|
resolver.StatusError,
|
||||||
),
|
),
|
||||||
"every nameserver must answer OK or not answer at all: %s",
|
"every nameserver must answer OK or not answer "+
|
||||||
|
"at all: %s",
|
||||||
describeStatuses(results),
|
describeStatuses(results),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryAllNameservers_NXDomainFromAllNS(
|
func TestQueryAllNameservers_NXDomainFromAllNS(
|
||||||
|
|||||||
Reference in New Issue
Block a user