feat: implement Phase 2.3 DNS traversal engine
CI / test (pull_request) Failing after 2m40s

Add core traversal engine with iterative resolution from root servers:
- referral.go: Referral struct with name, type, bailiwick, addresses, state
- cache.go: Chained InfoCache with parent inheritance for NS and glue records
- stack.go: LIFO stack with configurable max depth enforcement
- response.go: Response classifier (referral, answer, CNAME follow, NODATA,
  NXDOMAIN, SERVFAIL) with child referral generation and probability splitting
- traverser.go: Traverser orchestrates full traversal from root to leaf,
  following all referral branches, handling CNAME chains, and respecting
  max depth limits

Add IterativeQuery/IterativeQueryWithExchange to dns package (RD=false queries).

57 new tests covering all acceptance criteria: traversal from root to leaf,
comprehensive branch following, max depth enforcement, probability distribution,
chained cache behavior, CNAME following, NXDOMAIN/SERVFAIL handling, context
cancellation, and concurrent access.

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Gary
2026-06-06 13:23:01 +10:00
co-authored by multica-agent
parent 194bc97526
commit 2835ee86cf
11 changed files with 2098 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
package traverse
import (
"net"
"strings"
miekgdns "github.com/miekg/dns"
)
type ResolutionState int
const (
StateUnresolved ResolutionState = iota
StateResolving
StateResolved
)
func (s ResolutionState) String() string {
switch s {
case StateUnresolved:
return "unresolved"
case StateResolving:
return "resolving"
case StateResolved:
return "resolved"
default:
return "unknown"
}
}
type Referral struct {
Name string
Qtype uint16
Qclass uint16
Bailiwick string
Addresses []net.IP
State ResolutionState
NSName string
Parent *Referral
Depth int
Prob float64
}
func NewReferral(name string, qtype uint16, bailiwick string, depth int, prob float64, parent *Referral) *Referral {
return &Referral{
Name: miekgdns.Fqdn(strings.ToLower(name)),
Qtype: qtype,
Qclass: miekgdns.ClassINET,
Bailiwick: miekgdns.Fqdn(strings.ToLower(bailiwick)),
Depth: depth,
Prob: prob,
Parent: parent,
State: StateUnresolved,
}
}
func (r *Referral) InBailiwick(name string) bool {
if r.Bailiwick == "" || r.Bailiwick == "." {
return true
}
fqdn := miekgdns.Fqdn(strings.ToLower(name))
return miekgdns.IsSubDomain(r.Bailiwick, fqdn)
}
func (r *Referral) HasAddresses() bool {
return len(r.Addresses) > 0
}
func (r *Referral) SetAddresses(addrs []net.IP) {
r.Addresses = addrs
if len(addrs) > 0 {
r.State = StateResolved
} else {
r.State = StateUnresolved
}
}