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
+22 -5
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"sync"
"time"
"github.com/hits/ExploreDNS/internal/dns"
miekgdns "github.com/miekg/dns"
@@ -141,7 +142,19 @@ func (t *Traverser) Traverse(ctx context.Context, name string) ([]TraversalResul
if resp.Type == RespCNAMEFollow {
follow := resp.CNAMEFollowReferral()
if follow != nil {
if !stack.Push(follow) {
// Detect CNAME loop: target name already appears in the ancestor chain.
if follow.Parent != nil && follow.Parent.IsNameInChain(follow.Name) {
mu.Lock()
results = append(results, TraversalResult{
Referral: follow,
Response: &Response{
Referral: follow,
Type: RespCNAMELoop,
ErrorMessage: fmt.Sprintf("CNAME loop detected: %s already in traversal chain", follow.Name),
},
})
mu.Unlock()
} else if !stack.Push(follow) {
mu.Lock()
results = append(results, TraversalResult{
Referral: follow,
@@ -412,12 +425,16 @@ func (t *Traverser) resolveGlueViaSystem(ctx context.Context, name string, cache
c := &miekgdns.Client{
Net: "udp",
ReadTimeout: 5,
WriteTimeout: 5,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
if deadline, ok := ctx.Deadline(); ok {
c.ReadTimeout = deadline.Sub(deadline)
c.WriteTimeout = deadline.Sub(deadline)
remaining := time.Until(deadline)
if remaining <= 0 {
return nil
}
c.ReadTimeout = remaining
c.WriteTimeout = remaining
}
fqdn := miekgdns.Fqdn(name)