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:
@@ -0,0 +1,218 @@
|
||||
package traverse
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/hits/ExploreDNS/internal/dns"
|
||||
miekgdns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type ResponseType int
|
||||
|
||||
const (
|
||||
RespReferral ResponseType = iota
|
||||
RespAnswer
|
||||
RespCNAMEFollow
|
||||
RespNODATA
|
||||
RespNXDOMAIN
|
||||
RespSERVFAIL
|
||||
RespError
|
||||
)
|
||||
|
||||
func (rt ResponseType) String() string {
|
||||
switch rt {
|
||||
case RespReferral:
|
||||
return "referral"
|
||||
case RespAnswer:
|
||||
return "answer"
|
||||
case RespCNAMEFollow:
|
||||
return "cname_follow"
|
||||
case RespNODATA:
|
||||
return "nodata"
|
||||
case RespNXDOMAIN:
|
||||
return "nxdomain"
|
||||
case RespSERVFAIL:
|
||||
return "servfail"
|
||||
case RespError:
|
||||
return "error"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Referral *Referral
|
||||
Server net.IP
|
||||
Cache *InfoCache
|
||||
Decoded *dns.DecodedResponse
|
||||
Type ResponseType
|
||||
}
|
||||
|
||||
func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
|
||||
return &Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) Process(msg *miekgdns.Msg) *Response {
|
||||
if msg == nil {
|
||||
r.Type = RespError
|
||||
return r
|
||||
}
|
||||
|
||||
r.Decoded = dns.DecodeResponse(msg)
|
||||
if r.Decoded == nil {
|
||||
r.Type = RespError
|
||||
return r
|
||||
}
|
||||
|
||||
r.Type = r.classify()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Response) classify() ResponseType {
|
||||
switch r.Decoded.Classification {
|
||||
case dns.ResponseNXDOMAIN:
|
||||
return RespNXDOMAIN
|
||||
case dns.ResponseSERVFAIL:
|
||||
return RespSERVFAIL
|
||||
case dns.ResponseAnswer:
|
||||
if len(r.Decoded.CNAMEChain) > 0 && !r.hasFinalAnswer() {
|
||||
return RespCNAMEFollow
|
||||
}
|
||||
return RespAnswer
|
||||
case dns.ResponseReferral:
|
||||
return RespReferral
|
||||
case dns.ResponseNODATA:
|
||||
return RespNODATA
|
||||
default:
|
||||
return RespError
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) hasFinalAnswer() bool {
|
||||
for _, rr := range r.Decoded.Answers {
|
||||
if _, ok := rr.(*miekgdns.CNAME); ok {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Response) ChildReferrals() []*Referral {
|
||||
if r.Type != RespReferral {
|
||||
return nil
|
||||
}
|
||||
if r.Referral == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nameservers []string
|
||||
for _, rr := range r.Decoded.Authority {
|
||||
if ns, ok := rr.(*miekgdns.NS); ok {
|
||||
if r.Referral.InBailiwick(ns.Ns) {
|
||||
nameservers = append(nameservers, ns.Ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(nameservers) == 0 {
|
||||
for _, rr := range r.Decoded.Authority {
|
||||
if ns, ok := rr.(*miekgdns.NS); ok {
|
||||
nameservers = append(nameservers, ns.Ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.storeAuthority(nameservers)
|
||||
|
||||
prob := r.childProb(len(nameservers))
|
||||
var children []*Referral
|
||||
for _, ns := range nameservers {
|
||||
child := NewReferral(
|
||||
r.Referral.Name,
|
||||
r.Referral.Qtype,
|
||||
ns,
|
||||
r.Referral.Depth+1,
|
||||
prob,
|
||||
r.Referral,
|
||||
)
|
||||
r.resolveGlue(child)
|
||||
children = append(children, child)
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (r *Response) CNAMEFollowReferral() *Referral {
|
||||
if r.Type != RespCNAMEFollow || len(r.Decoded.CNAMEChain) == 0 {
|
||||
return nil
|
||||
}
|
||||
target := r.Decoded.CNAMEChain[len(r.Decoded.CNAMEChain)-1]
|
||||
follow := NewReferral(
|
||||
target,
|
||||
r.Referral.Qtype,
|
||||
r.Referral.Bailiwick,
|
||||
r.Referral.Depth+1,
|
||||
r.Referral.Prob,
|
||||
r.Referral,
|
||||
)
|
||||
if len(r.Referral.Addresses) > 0 {
|
||||
follow.Addresses = make([]net.IP, len(r.Referral.Addresses))
|
||||
copy(follow.Addresses, r.Referral.Addresses)
|
||||
follow.State = StateResolved
|
||||
}
|
||||
return follow
|
||||
}
|
||||
|
||||
func (r *Response) storeAuthority(nameservers []string) {
|
||||
if r.Cache == nil {
|
||||
return
|
||||
}
|
||||
zone := r.Referral.Name
|
||||
r.Cache.StoreNS(zone, nameservers)
|
||||
}
|
||||
|
||||
func (r *Response) resolveGlue(child *Referral) {
|
||||
if r.Cache == nil {
|
||||
return
|
||||
}
|
||||
nsName := child.Bailiwick
|
||||
for _, rr := range r.Decoded.Additional {
|
||||
switch v := rr.(type) {
|
||||
case *miekgdns.A:
|
||||
if normalize(v.Header().Name) == normalize(nsName) {
|
||||
child.Addresses = append(child.Addresses, v.A)
|
||||
}
|
||||
case *miekgdns.AAAA:
|
||||
if normalize(v.Header().Name) == normalize(nsName) {
|
||||
child.Addresses = append(child.Addresses, v.AAAA)
|
||||
}
|
||||
}
|
||||
}
|
||||
if child.HasAddresses() {
|
||||
child.State = StateResolved
|
||||
}
|
||||
r.Cache.StoreGlue(nsName, child.Addresses)
|
||||
}
|
||||
|
||||
func (r *Response) IsTerminal() bool {
|
||||
switch r.Type {
|
||||
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) childProb(n int) float64 {
|
||||
if n <= 0 {
|
||||
return 0
|
||||
}
|
||||
if r.Referral == nil {
|
||||
return 1.0 / float64(n)
|
||||
}
|
||||
return r.Referral.Prob / float64(n)
|
||||
}
|
||||
Reference in New Issue
Block a user