resolver: query a hostname at its own zone's servers (closes #189)
check / check (push) Failing after 1m23s
check / check (push) Failing after 1m23s
A hostname's nameservers came from its last two labels, so a name under co.uk was asked at the co.uk servers and a name in a delegated subdomain at the parent's servers; both only refer onward. The hostname now goes through FindAuthoritativeNameservers, which follows delegations for the name and walks up its labels until it finds the zone it is in. followDelegation now stops at an authoritative reply: that server holds the zone, so its reply is not a referral. Without this a CNAME answered with the zone's NS records in the authority section, as Route 53 does, was followed as a referral until the delegation limit. Model: opus-5-5
This commit is contained in:
@@ -56,7 +56,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
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ nameserver IP address changes: https://git.eeqj.de/sneak/dnswatcher/issues/105
|
|||||||
|
|
||||||
# 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: a port or TLS check that shutdown cuts short saves nothing and
|
- 2026-10-01: a port or TLS check that shutdown cuts short saves nothing and
|
||||||
sends no notification, as a cut-short DNS lookup already did (closes #185).
|
sends no notification, as a cut-short DNS lookup already did (closes #185).
|
||||||
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry that
|
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry 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,28 @@ 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, where EC2 host names are, which
|
||||||
|
// amazonaws.com delegates to other servers. 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 +381,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 +396,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