watcher: a hostname whose zone's nameservers all stop answering gets NS Failure (closes #284)
check / check (push) Canceled after 0s

When none of the nameservers of a hostname's zone answered, the walk to
them failed and the check was only logged. The walk now returns a
NoAnswerError naming the nameservers of the zone it reached when none
of them answers. When those are the nameservers the previous check
saved for the hostname, each is saved as failed with the reason, as a
domain's are, so it gets an NS Failure and later an NS Recovery. A walk
that fails before that zone, as when the host loses its network, is
still only logged.

Not tested live: the resolver naming a zone's nameservers that never
answer, as no live zone has such nameservers; the watcher test builds
that error.

Model: opus-5-5
This commit is contained in:
2026-10-13 21:16:31 +00:00
parent aaccabad50
commit 817df7fd34
9 changed files with 216 additions and 11 deletions
+15 -4
View File
@@ -209,16 +209,19 @@ func glueIPs(nsNames []string, glue map[string][]net.IP) []string {
// of the zone domain is in answer that domain does not exist, the error
// is ErrNXDomain. When they answer that it has no delegation of its own,
// because it is not the zone's apex, the set is empty and there is no
// error. Any other error means that no such answer came.
// error. Any other error means that no such answer came; it is a
// *NoAnswerError when the servers that did not answer are those of a
// zone a referral named.
func (r *Resolver) followDelegation(
ctx context.Context,
domain string,
servers []string,
) ([]string, error) {
// servers are the root servers, the servers of zone ".".
// servers are the root servers, the servers of zone ".". After a
// referral, nameservers are the names of the servers of zone.
zone := "."
var withoutAddresses []string
var nameservers, withoutAddresses []string
for range maxDelegation {
if checkCtx(ctx) != nil {
@@ -228,6 +231,12 @@ func (r *Resolver) followDelegation(
resp, err := r.queryZone(
ctx, servers, withoutAddresses, zone, domain, dns.TypeNS, 0,
)
// A query that ctx cut short says nothing about the servers.
if err != nil && len(nameservers) > 0 && checkCtx(ctx) == nil {
return nil, &NoAnswerError{Nameservers: nameservers, Err: err}
}
if err != nil {
return nil, err
}
@@ -256,6 +265,7 @@ func (r *Resolver) followDelegation(
return r.resolveNSIterative(ctx, domain)
}
nameservers = authNS
servers, withoutAddresses = referralNameservers(resp)
zone = referralZone(resp)
}
@@ -626,7 +636,8 @@ func (r *Resolver) resolveARecord(
// them. When the servers asked answer that the name has no delegation
// of its own, or does not exist, it tries each parent name in turn, so
// it returns the nameservers of the zone the name is in. When they do
// not answer, it returns the error and tries no parent name.
// not answer, it returns the error and tries no parent name; when a
// referral named them, the error is a *NoAnswerError naming them.
func (r *Resolver) FindAuthoritativeNameservers(
ctx context.Context,
domain string,