Files
ExploreDNS/internal/traverse/cache.go
T
Garyandmultica-agent 2835ee86cf
CI / test (pull_request) Failing after 2m40s
feat: implement Phase 2.3 DNS traversal engine
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>
2026-06-06 13:23:01 +10:00

125 lines
2.2 KiB
Go

package traverse
import (
"net"
"strings"
"sync"
miekgdns "github.com/miekg/dns"
)
type InfoCache struct {
parent *InfoCache
mu sync.RWMutex
ns map[string][]string
glue map[string][]net.IP
}
func NewInfoCache(parent *InfoCache) *InfoCache {
return &InfoCache{
parent: parent,
ns: make(map[string][]string),
glue: make(map[string][]net.IP),
}
}
func (c *InfoCache) StoreNS(zone string, nameservers []string) {
if len(nameservers) == 0 {
return
}
zone = normalize(zone)
c.mu.Lock()
seen := make(map[string]bool)
for _, ns := range nameservers {
ns = normalize(ns)
if !seen[ns] {
seen[ns] = true
c.ns[zone] = append(c.ns[zone], ns)
}
}
c.mu.Unlock()
}
func (c *InfoCache) LookupNS(zone string) []string {
zone = normalize(zone)
if names := c.localNS(zone); len(names) > 0 {
return names
}
if c.parent != nil {
return c.parent.LookupNS(zone)
}
return nil
}
func (c *InfoCache) localNS(zone string) []string {
c.mu.RLock()
defer c.mu.RUnlock()
names, ok := c.ns[zone]
if !ok {
return nil
}
result := make([]string, len(names))
copy(result, names)
return result
}
func (c *InfoCache) StoreGlue(name string, addrs []net.IP) {
if len(addrs) == 0 {
return
}
name = normalize(name)
c.mu.Lock()
seen := make(map[string]bool)
for _, addr := range addrs {
key := addr.String()
if !seen[key] {
seen[key] = true
c.glue[name] = append(c.glue[name], addr)
}
}
c.mu.Unlock()
}
func (c *InfoCache) LookupGlue(name string) []net.IP {
name = normalize(name)
if addrs := c.localGlue(name); len(addrs) > 0 {
return addrs
}
if c.parent != nil {
return c.parent.LookupGlue(name)
}
return nil
}
func (c *InfoCache) localGlue(name string) []net.IP {
c.mu.RLock()
defer c.mu.RUnlock()
addrs, ok := c.glue[name]
if !ok {
return nil
}
result := make([]net.IP, len(addrs))
copy(result, addrs)
return result
}
func (c *InfoCache) Child() *InfoCache {
return NewInfoCache(c)
}
func (c *InfoCache) NSCount() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.ns)
}
func (c *InfoCache) GlueCount() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.glue)
}
func normalize(name string) string {
return strings.ToLower(miekgdns.Fqdn(name))
}