Phase 4.1: Error handling, edge cases, and robustness
CI / test (pull_request) Failing after 3m40s

- Exponential backoff retry logic (100ms, 200ms, 400ms... capped at 2s)
  replacing fixed 100ms delay between retries
- Explicit REFUSED and NOTIMP response types (RespREFUSED, RespNOTIMPL)
  surfaced as terminal results with user-visible messages
- CNAME loop detection: walking the ancestor referral chain before
  following a CNAME prevents infinite recursion; produces RespCNAMELoop
- DNAME record support: synthesize CNAME target from DNAME mapping when
  the server omits the RFC 6672 synthesized CNAME record
- ErrorMessage field on Response for surfacing error details to users
- Fix resolveGlueViaSystem timeout bug: deadline.Sub(deadline) was
  always 0; replaced with time.Until(deadline)
- DNAME records excluded from hasFinalAnswer so DNAME-only responses
  are correctly classified as RespCNAMEFollow
- Text and JSON output updated with labels for all new response types
- Tests: CNAME loop (2-step and direct), REFUSED, NOTIMP, graceful
  degradation (partial and total server failure), DNAME synthesis,
  IsNameInChain, backoffDelay, ResponseClassification strings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Gary Hansen
2026-06-08 03:11:46 +10:00
co-authored by Copilot multica-agent
parent 76f5010a5e
commit 368f200d23
9 changed files with 670 additions and 14 deletions
+36 -6
View File
@@ -16,6 +16,9 @@ const (
RespNODATA
RespNXDOMAIN
RespSERVFAIL
RespREFUSED
RespNOTIMPL
RespCNAMELoop
RespError
)
@@ -33,6 +36,12 @@ func (rt ResponseType) String() string {
return "nxdomain"
case RespSERVFAIL:
return "servfail"
case RespREFUSED:
return "refused"
case RespNOTIMPL:
return "notimp"
case RespCNAMELoop:
return "cname_loop"
case RespError:
return "error"
default:
@@ -41,11 +50,12 @@ func (rt ResponseType) String() string {
}
type Response struct {
Referral *Referral
Server net.IP
Cache *InfoCache
Decoded *dns.DecodedResponse
Type ResponseType
Referral *Referral
Server net.IP
Cache *InfoCache
Decoded *dns.DecodedResponse
Type ResponseType
ErrorMessage string
}
func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
@@ -59,15 +69,28 @@ func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
func (r *Response) Process(msg *miekgdns.Msg) *Response {
if msg == nil {
r.Type = RespError
r.ErrorMessage = "nil DNS response"
return r
}
r.Decoded = dns.DecodeResponse(msg)
if r.Decoded == nil {
r.Type = RespError
r.ErrorMessage = "failed to decode DNS response"
return r
}
// Synthesize CNAME from DNAME when the server didn't include a synthesized CNAME record.
if len(r.Decoded.CNAMEChain) == 0 && r.Referral != nil && len(r.Decoded.DNAMEMappings) > 0 {
for _, dm := range r.Decoded.DNAMEMappings {
synthesized := dns.SynthesizeCNAMEFromDNAME(r.Referral.Name, dm.Owner, dm.Target)
if synthesized != "" {
r.Decoded.CNAMEChain = append(r.Decoded.CNAMEChain, synthesized)
break
}
}
}
r.Type = r.classify()
return r
}
@@ -78,6 +101,10 @@ func (r *Response) classify() ResponseType {
return RespNXDOMAIN
case dns.ResponseSERVFAIL:
return RespSERVFAIL
case dns.ResponseREFUSED:
return RespREFUSED
case dns.ResponseNOTIMPL:
return RespNOTIMPL
case dns.ResponseAnswer:
if len(r.Decoded.CNAMEChain) > 0 && !r.hasFinalAnswer() {
return RespCNAMEFollow
@@ -97,6 +124,9 @@ func (r *Response) hasFinalAnswer() bool {
if _, ok := rr.(*miekgdns.CNAME); ok {
continue
}
if _, ok := rr.(*miekgdns.DNAME); ok {
continue
}
return true
}
return false
@@ -200,7 +230,7 @@ func (r *Response) resolveGlue(child *Referral) {
func (r *Response) IsTerminal() bool {
switch r.Type {
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespError:
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError:
return true
default:
return false