resolver, watcher: a record type whose query fails keeps its previous records (closes #231)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The resolver lists in FailedTypes each record type whose query to a nameserver got no usable reply (no reply, a code other than NOERROR or NXDOMAIN, a referral, or a truncated reply whose TCP retry failed) and logs it unless shutdown cut it short. A nameserver that answered no type has failed. The watcher saves such a type in failedTypes with the previous check's records, leaves it out of the comparison with other nameservers on that check, and compares it with the next answer. When the previous check did not know its records either, it is also in unknownTypes and not compared until it answers. A nameserver whose A, AAAA or CNAME query failed is no answer when following a CNAME or resolving addresses. Model: opus-5-5
This commit is contained in:
@@ -99,6 +99,9 @@ func (r *Resolver) tryExchange(
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// retryTCP returns the reply to msg over TCP when resp, its reply over
|
||||
// UDP, is truncated. When that fails it returns resp, still truncated,
|
||||
// which holds only the records that fit.
|
||||
func (r *Resolver) retryTCP(
|
||||
ctx context.Context,
|
||||
msg *dns.Msg,
|
||||
@@ -320,13 +323,20 @@ func (r *Resolver) queryServers(
|
||||
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
||||
}
|
||||
|
||||
// isErrorReply reports whether msg is an error reply: one with any code
|
||||
// but NOERROR and NXDOMAIN, such as SERVFAIL, NOTIMP or FORMERR. An error
|
||||
// reply says nothing about the name's records.
|
||||
func isErrorReply(msg *dns.Msg) bool {
|
||||
return msg.Rcode != dns.RcodeSuccess && msg.Rcode != dns.RcodeNameError
|
||||
}
|
||||
|
||||
// usableReply reports whether resp, a reply from one of the servers of
|
||||
// zone to a query about name, is usable. An error reply such as SERVFAIL
|
||||
// is not. Nor is a referral, unless it refers the query to a zone below
|
||||
// zone that name is in: a server that refers it back to zone, up or
|
||||
// sideways does not serve zone as it should.
|
||||
func usableReply(resp *dns.Msg, zone string, name string) bool {
|
||||
if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {
|
||||
if isErrorReply(resp) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -715,14 +725,19 @@ func (r *Resolver) queryTypes(
|
||||
|
||||
type queryState struct {
|
||||
gotNXDomain bool
|
||||
gotSERVFAIL bool
|
||||
errorReply string // code of an error reply, such as SERVFAIL
|
||||
gotRefused bool
|
||||
gotTimeout bool
|
||||
gotReferral bool
|
||||
netErr error
|
||||
hasRecords bool
|
||||
answered bool
|
||||
}
|
||||
|
||||
// queryEachType asks the nameserver at nsIP about hostname once for each
|
||||
// record type in qtypes, and lists in resp.FailedTypes the types whose
|
||||
// query got no usable reply, logging each with the reason unless ctx was
|
||||
// cancelled: shutdown cancels it, and a query it cut short did not fail.
|
||||
func (r *Resolver) queryEachType(
|
||||
ctx context.Context,
|
||||
nsIP string,
|
||||
@@ -737,7 +752,34 @@ func (r *Resolver) queryEachType(
|
||||
break
|
||||
}
|
||||
|
||||
r.querySingleType(ctx, nsIP, hostname, qtype, resp, &state)
|
||||
err := r.querySingleType(ctx, nsIP, hostname, qtype, resp, &state)
|
||||
if err == nil {
|
||||
state.answered = true
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
rtype := dns.TypeToString[qtype]
|
||||
resp.FailedTypes = append(resp.FailedTypes, rtype)
|
||||
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
continue
|
||||
}
|
||||
|
||||
r.log.Warn(
|
||||
"record type query failed",
|
||||
"hostname", hostname,
|
||||
"nameserver", resp.Nameserver,
|
||||
"type", rtype,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
// The reply about another type can carry the name's CNAME. When the
|
||||
// query for CNAME itself failed, that is left out too, so Records
|
||||
// holds nothing for a failed type.
|
||||
for _, rtype := range resp.FailedTypes {
|
||||
delete(resp.Records, rtype)
|
||||
}
|
||||
|
||||
for k := range resp.Records {
|
||||
@@ -747,6 +789,9 @@ func (r *Resolver) queryEachType(
|
||||
return state
|
||||
}
|
||||
|
||||
// querySingleType asks the nameserver at nsIP about hostname's records
|
||||
// of type qtype. It returns nil when the nameserver answered: with
|
||||
// records, with none, or with NXDOMAIN; otherwise it returns why not.
|
||||
func (r *Resolver) querySingleType(
|
||||
ctx context.Context,
|
||||
nsIP string,
|
||||
@@ -754,7 +799,7 @@ func (r *Resolver) querySingleType(
|
||||
qtype uint16,
|
||||
resp *NameserverResponse,
|
||||
state *queryState,
|
||||
) {
|
||||
) error {
|
||||
msg, err := r.queryDNS(ctx, nsIP, hostname, qtype)
|
||||
if err != nil {
|
||||
switch {
|
||||
@@ -766,19 +811,33 @@ func (r *Resolver) querySingleType(
|
||||
state.netErr = err
|
||||
}
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
return readReply(msg, resp, state)
|
||||
}
|
||||
|
||||
// readReply adds to resp the records in msg, a nameserver's reply to a
|
||||
// query about one record type. It returns nil when the nameserver
|
||||
// answered: with records, with none, or with NXDOMAIN; otherwise it
|
||||
// returns why not.
|
||||
func readReply(
|
||||
msg *dns.Msg,
|
||||
resp *NameserverResponse,
|
||||
state *queryState,
|
||||
) error {
|
||||
if msg.Rcode == dns.RcodeNameError {
|
||||
state.gotNXDomain = true
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if msg.Rcode == dns.RcodeServerFailure {
|
||||
state.gotSERVFAIL = true
|
||||
if isErrorReply(msg) {
|
||||
state.errorReply = dns.RcodeToString[msg.Rcode]
|
||||
|
||||
return
|
||||
return fmt.Errorf(
|
||||
"server returned %s: %w", state.errorReply, ErrUnusableReply,
|
||||
)
|
||||
}
|
||||
|
||||
// A reply with no answer that lists other nameservers, from a server
|
||||
@@ -791,10 +850,20 @@ func (r *Resolver) querySingleType(
|
||||
len(extractNSSet(msg.Ns)) > 0 {
|
||||
state.gotReferral = true
|
||||
|
||||
return
|
||||
return fmt.Errorf("server returned a referral: %w", ErrUnusableReply)
|
||||
}
|
||||
|
||||
// A reply still truncated is one whose TCP retry failed, and holds
|
||||
// only the records that fit.
|
||||
if msg.Truncated {
|
||||
state.netErr = ErrTruncated
|
||||
|
||||
return ErrTruncated
|
||||
}
|
||||
|
||||
collectAnswerRecords(msg, resp, state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectAnswerRecords adds the records in msg's answer to resp, each
|
||||
@@ -833,23 +902,26 @@ func isTimeout(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// classifyResponse sets the nameserver's status. One that answered no
|
||||
// record type has failed, and Error says why; one that answered some has
|
||||
// the status of those answers.
|
||||
func classifyResponse(resp *NameserverResponse, state queryState) {
|
||||
switch {
|
||||
case state.gotNXDomain && !state.hasRecords:
|
||||
resp.Status = StatusNXDomain
|
||||
case state.gotTimeout && !state.hasRecords:
|
||||
case state.gotTimeout && !state.answered:
|
||||
resp.Status = StatusTimeout
|
||||
resp.Error = "all queries timed out"
|
||||
case state.gotSERVFAIL && !state.hasRecords:
|
||||
case state.errorReply != "" && !state.answered:
|
||||
resp.Status = StatusError
|
||||
resp.Error = "server returned SERVFAIL"
|
||||
case state.gotRefused && !state.hasRecords:
|
||||
resp.Error = "server returned " + state.errorReply
|
||||
case state.gotRefused && !state.answered:
|
||||
resp.Status = StatusError
|
||||
resp.Error = "server returned REFUSED"
|
||||
case state.netErr != nil && !state.hasRecords:
|
||||
case state.netErr != nil && !state.answered:
|
||||
resp.Status = StatusError
|
||||
resp.Error = "network error: " + state.netErr.Error()
|
||||
case state.gotReferral && !state.hasRecords:
|
||||
case state.gotReferral && !state.answered:
|
||||
resp.Status = StatusError
|
||||
resp.Error = "server returned a referral"
|
||||
case !state.hasRecords && !state.gotNXDomain:
|
||||
@@ -1010,9 +1082,11 @@ func (r *Resolver) resolveIPWithCNAME(
|
||||
}
|
||||
|
||||
// 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.
|
||||
// first CNAME target among them. A nameserver whose query for one of the
|
||||
// types failed gave only part of the addresses, and is left out. It
|
||||
// returns ErrNoNameserverAnswered when every nameserver timed out,
|
||||
// failed, returned a referral or was left out: that is not a name with
|
||||
// no addresses.
|
||||
func collectIPs(
|
||||
results map[string]*NameserverResponse,
|
||||
) ([]string, string, error) {
|
||||
@@ -1025,7 +1099,8 @@ func collectIPs(
|
||||
answered := false
|
||||
|
||||
for _, resp := range results {
|
||||
if resp.Status == StatusTimeout || resp.Status == StatusError {
|
||||
if resp.Status == StatusTimeout || resp.Status == StatusError ||
|
||||
len(resp.FailedTypes) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user