internal/traverse/robustness_test.go: CNAME loop (2-step + direct self-loop); REFUSED/NOTIMP traversal; graceful degradation with partial and total server failure; DNAME follow without synthesized CNAME; IsNameInChain; all response type strings
All existing tests pass; go test -race ./... is clean.
Implements Phase 4.1 hardening for the ExploreDNS traversal engine.
## Changes
### `internal/dns/decode.go`
- Add `ResponseREFUSED` and `ResponseNOTIMPL` response classifications
- Handle `REFUSED` and `NOTIMP` rcodes in `classify()`
- Add `DNAMEMapping` struct and `DNAMEMappings` field on `DecodedResponse`
- Add `SynthesizeCNAMEFromDNAME()` for DNAME redirect synthesis
### `internal/dns/query.go`
- Replace fixed 100ms retry delay with **exponential backoff**: 100ms → 200ms → 400ms, capped at 2s
- `backoffDelay(attempt)` helper is also tested independently
### `internal/traverse/response.go`
- Add `RespREFUSED`, `RespNOTIMPL`, `RespCNAMELoop` terminal response types
- Add `ErrorMessage string` field for surfacing errors to users (no silent drops)
- Synthesize CNAME from DNAME in `Process()` when server omits RFC 6672 synthesized CNAME
- Exclude DNAME records from `hasFinalAnswer()` so DNAME-only responses classify as `RespCNAMEFollow`
- `IsTerminal()` updated to cover all new terminal types
### `internal/traverse/referral.go`
- Add `IsNameInChain()` method: walks the ancestor chain to detect if a name already appears
### `internal/traverse/traverser.go`
- **Fix bug**: `resolveGlueViaSystem` used `deadline.Sub(deadline)` (always 0) for timeout; replaced with `time.Until(deadline)`
- **CNAME loop detection**: before pushing a CNAME follow referral, check `follow.Parent.IsNameInChain(follow.Name)`; loops emit `RespCNAMELoop` instead of recursing
### Output
- `text.go`: display strings for `refused`, `notimp`, `cname_loop` (with `ErrorMessage` when present)
- `stats.go`: summary labels for all new types
## Tests
- `internal/dns/robustness_test.go`: REFUSED, NOTIMP decoding; DNAME mapping extraction; CNAME synthesis; backoff delay values
- `internal/traverse/robustness_test.go`: CNAME loop (2-step + direct self-loop); REFUSED/NOTIMP traversal; graceful degradation with partial and total server failure; DNAME follow without synthesized CNAME; `IsNameInChain`; all response type strings
All existing tests pass; `go test -race ./...` is clean.
- 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>
- Fast mode cache isolation: TraverserConfig.Fast=false gives each
referral its own independent InfoCache with no cross-branch glue
inheritance; Fast=true (default) retains the shared root cache so
earlier branch discoveries are reused
- Wire cfg.Fast from CLI config into TraverserConfig in main.go
- IDN/Punycode: NewReferral now converts unicode domain labels to their
ACE/punycode form via golang.org/x/net/idna before querying, with a
graceful fallback when conversion fails
- DNSSEC: hasFinalAnswer() now skips RRSIG records alongside CNAME so
a signed referral does not prevent CNAME following
- Tests: DNSSEC RRSIG does not block CNAME follow, fast/non-fast cache
isolation, 12-NS referral, IDN conversion, wildcard answer, long CNAME
chain depth limit, partial branch failure with graceful degradation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements Phase 4.1 hardening for the ExploreDNS traversal engine.
Changes
internal/dns/decode.goResponseREFUSEDandResponseNOTIMPLresponse classificationsREFUSEDandNOTIMPrcodes inclassify()DNAMEMappingstruct andDNAMEMappingsfield onDecodedResponseSynthesizeCNAMEFromDNAME()for DNAME redirect synthesisinternal/dns/query.gobackoffDelay(attempt)helper is also tested independentlyinternal/traverse/response.goRespREFUSED,RespNOTIMPL,RespCNAMELoopterminal response typesErrorMessage stringfield for surfacing errors to users (no silent drops)Process()when server omits RFC 6672 synthesized CNAMEhasFinalAnswer()so DNAME-only responses classify asRespCNAMEFollowIsTerminal()updated to cover all new terminal typesinternal/traverse/referral.goIsNameInChain()method: walks the ancestor chain to detect if a name already appearsinternal/traverse/traverser.goresolveGlueViaSystemuseddeadline.Sub(deadline)(always 0) for timeout; replaced withtime.Until(deadline)follow.Parent.IsNameInChain(follow.Name); loops emitRespCNAMELoopinstead of recursingOutput
text.go: display strings forrefused,notimp,cname_loop(withErrorMessagewhen present)stats.go: summary labels for all new typesTests
internal/dns/robustness_test.go: REFUSED, NOTIMP decoding; DNAME mapping extraction; CNAME synthesis; backoff delay valuesinternal/traverse/robustness_test.go: CNAME loop (2-step + direct self-loop); REFUSED/NOTIMP traversal; graceful degradation with partial and total server failure; DNAME follow without synthesized CNAME;IsNameInChain; all response type stringsAll existing tests pass;
go test -race ./...is clean.