resolver: error from ResolveIPAddresses when no nameserver answered (closes #190)
check / check (push) Failing after 2m7s

ResolveIPAddresses now returns an error, not no addresses, when no
nameserver of the name's zone answered. A nameserver with status
timeout or error is not an answer; one answer, even NXDOMAIN, is
enough for an empty result without an error.

When every server of a zone fails, FindAuthoritativeNameservers moves
on to the parent name, whose servers only refer the query onward. Such
a referral now has status error, so it is no answer either, and a
hostname's saved records show it as error. The only caller, the
nameserver address lookup, already keeps the previous addresses on an
error; its comment no longer says the resolver hides this case.

Model: opus-5-5
This commit was merged in pull request #194.
This commit is contained in:
2026-10-02 00:28:53 +02:00
parent 4c2932d6d6
commit d2f154b2cf
8 changed files with 168 additions and 10 deletions
+41 -4
View File
@@ -516,6 +516,7 @@ type queryState struct {
gotSERVFAIL bool
gotRefused bool
gotTimeout bool
gotReferral bool
netErr error
hasRecords bool
}
@@ -578,6 +579,18 @@ func (r *Resolver) querySingleType(
return
}
// A reply with no answer that lists other nameservers, from a server
// that does not hold the name's zone, is a referral and says nothing
// about the name's records. A parent zone's servers send one when
// every server of the name's own zone failed and
// FindAuthoritativeNameservers moved on to the parent name.
if !msg.Authoritative && len(msg.Answer) == 0 &&
len(extractNSSet(msg.Ns)) > 0 {
state.gotReferral = true
return
}
collectAnswerRecords(msg, resp, state)
}
@@ -626,6 +639,9 @@ func classifyResponse(resp *NameserverResponse, state queryState) {
case state.netErr != nil && !state.hasRecords:
resp.Status = StatusError
resp.Error = "network error: " + state.netErr.Error()
case state.gotReferral && !state.hasRecords:
resp.Status = StatusError
resp.Error = "server returned a referral"
case !state.hasRecords && !state.gotNXDomain:
resp.Status = StatusNoData
}
@@ -734,7 +750,9 @@ func (r *Resolver) LookupAllRecords(
}
// ResolveIPAddresses resolves a hostname to all IPv4 and IPv6
// addresses, following CNAME chains up to MaxCNAMEDepth.
// addresses, following CNAME chains up to MaxCNAMEDepth. When no
// nameserver of the name's zone answered, it returns an error rather
// than no addresses.
func (r *Resolver) ResolveIPAddresses(
ctx context.Context,
hostname string,
@@ -760,7 +778,10 @@ func (r *Resolver) resolveIPWithCNAME(
return nil, err
}
ips, cnameTarget := collectIPs(results)
ips, cnameTarget, err := collectIPs(results)
if err != nil {
return nil, fmt.Errorf("resolving %s: %w", hostname, err)
}
if len(ips) == 0 && cnameTarget != "" {
return r.resolveIPWithCNAME(ctx, cnameTarget, depth+1)
@@ -771,16 +792,28 @@ func (r *Resolver) resolveIPWithCNAME(
return ips, nil
}
// collectIPs returns the addresses in the nameservers' answers and the
// first CNAME target among them. It returns ErrNoNameserverAnswered when
// every nameserver timed out, failed or returned a referral: that is not
// a name with no addresses.
func collectIPs(
results map[string]*NameserverResponse,
) ([]string, string) {
) ([]string, string, error) {
seen := make(map[string]bool)
var ips []string
var cnameTarget string
answered := false
for _, resp := range results {
if resp.Status == StatusTimeout || resp.Status == StatusError {
continue
}
answered = true
if resp.Status == StatusNXDomain {
continue
}
@@ -804,5 +837,9 @@ func collectIPs(
}
}
return ips, cnameTarget
if !answered {
return nil, "", ErrNoNameserverAnswered
}
return ips, cnameTarget, nil
}