feat: rework engine and CLI for dnstraverse parity
Port the traversal engine to the Ruby dnstraverse model so behaviour and output match dns.squish.net: - dns: single RD=0 query path (RD=1 only for upstream root discovery), per-run packet cache, EDNS0 512-fallback with warnings, UDP->TCP on truncation; fix --retries 0 and --root-server IP-literal handling; drop all hardcoded 127.0.0.1:53 resolvers - traverse: hierarchical per-branch InfoCache, 7-step response classification with the full 10-status vocabulary, bailiwick partitioning, strictly-deeper lame-referral rule, refid grammar with .0 resolve subtrees and childset digits, per-IP branching at 1/n weight, cache-based glue resolution with noglue/loop dead ends, CNAME restarts from the deepest cached zone, fast-mode memoization, probability aggregation with Ruby-identical stats keys (sums to 1.0) - output: byte-for-byte reference text format pinned by a golden test, reference CLI defaults, working --quiet/--show-X=false, TTY-aware colour, deduplicated deterministic JSON - web: adapt API/SPA to the new engine, SSE events carry refid/status, fix subscribe/snapshot duplicate-event race and a statusCls TDZ bug, align SPA type list with the backend - delete the old engine and dead code (net -4,350 lines) Verified against live runs of the reference Ruby engine across five domains (answers, NXDOMAIN, null MX, CNAME restart, glueless resolve) with no divergences beyond the documented typo fixes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
af15c9c2d4
commit
d71c7fbef2
+252
-419
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||
miekgdns "github.com/miekg/dns"
|
||||
@@ -14,495 +13,329 @@ import (
|
||||
|
||||
// TraverserConfig configures the behaviour of a Traverser.
|
||||
type TraverserConfig struct {
|
||||
// MaxDepth is the maximum referral depth before the traversal gives up.
|
||||
MaxDepth int
|
||||
// MaxDepth is the maximum referral depth (non-zero refid components)
|
||||
// before a "Maxdepth N exceeded" exception is injected.
|
||||
MaxDepth int
|
||||
// QueryType is the DNS record type to query (e.g. dns.TypeA).
|
||||
QueryType uint16
|
||||
QueryType uint16
|
||||
// RootConfig controls how root servers are discovered.
|
||||
RootConfig *dns.RootDiscoveryConfig
|
||||
RootConfig *dns.RootDiscoveryConfig
|
||||
// QueryConfig controls per-query transport parameters.
|
||||
QueryConfig *dns.QueryConfig
|
||||
// RootAddrs is an optional pre-seeded list of root server IP addresses.
|
||||
// When non-empty, root discovery via RootConfig is skipped.
|
||||
RootAddrs []net.IP
|
||||
// When non-empty, root discovery via RootConfig is skipped and each
|
||||
// address becomes one root (named by its address).
|
||||
RootAddrs []net.IP
|
||||
// Hooks provides optional callbacks for traversal events.
|
||||
Hooks *TraverserHooks
|
||||
// Fast controls cache sharing across branches. When true (default), child
|
||||
// branches inherit glue discovered by earlier branches via the shared root
|
||||
// cache, trading accuracy for speed. When false, each branch gets a
|
||||
// completely independent cache — slower but results are not contaminated by
|
||||
// sibling branch observations.
|
||||
Hooks *TraverserHooks
|
||||
// Fast enables the completed-referral memo (traverser.rb @answered):
|
||||
// a referral identical to an earlier completed one (same qname/qclass/
|
||||
// qtype/server and per-IP weights) is replaced by it instead of being
|
||||
// walked again. Non-fast mode re-walks every branch.
|
||||
Fast bool
|
||||
}
|
||||
|
||||
func DefaultTraverserConfig() *TraverserConfig {
|
||||
return &TraverserConfig{
|
||||
MaxDepth: DefaultMaxDepth,
|
||||
QueryType: dns.TypeA,
|
||||
RootConfig: nil,
|
||||
QueryConfig: nil,
|
||||
RootAddrs: nil,
|
||||
Fast: true,
|
||||
MaxDepth: DefaultMaxDepth,
|
||||
QueryType: dns.TypeA,
|
||||
Fast: true,
|
||||
}
|
||||
}
|
||||
|
||||
// TraversalResult pairs a Referral with the Response received when it was processed.
|
||||
type TraversalResult struct {
|
||||
Referral *Referral
|
||||
Response *Response
|
||||
}
|
||||
|
||||
// Traverser performs an exhaustive iterative DNS traversal starting from the
|
||||
// root servers. Create one via NewTraverser and call Traverse to start a run.
|
||||
// Traverser drives the traversal: it owns the packet-cached query client,
|
||||
// the fast-mode memo and the explicit stack loop (traverser.rb).
|
||||
type Traverser struct {
|
||||
config *TraverserConfig
|
||||
client *dns.Client
|
||||
exchange dns.ExchangeFunc
|
||||
visited map[string]bool
|
||||
depth int
|
||||
mu sync.Mutex
|
||||
// answered is the fast-mode memo of completed referrals.
|
||||
answered map[string]*Referral
|
||||
// seen maps every server name encountered to its IP addresses.
|
||||
seen map[string][]string
|
||||
// roots memoises root discovery so Roots() and Run() share one lookup.
|
||||
roots []StartServer
|
||||
}
|
||||
|
||||
func NewTraverser(cfg *TraverserConfig) *Traverser {
|
||||
if cfg == nil {
|
||||
cfg = DefaultTraverserConfig()
|
||||
}
|
||||
if cfg.MaxDepth <= 0 {
|
||||
cfg.MaxDepth = DefaultMaxDepth
|
||||
}
|
||||
if cfg.QueryType == 0 {
|
||||
cfg.QueryType = dns.TypeA
|
||||
}
|
||||
return &Traverser{
|
||||
config: cfg,
|
||||
exchange: nil,
|
||||
visited: make(map[string]bool),
|
||||
depth: 0,
|
||||
client: dns.NewClient(cfg.QueryConfig, nil),
|
||||
answered: make(map[string]*Referral),
|
||||
seen: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
// SetExchange injects a mock wire exchange into the single query path (both
|
||||
// traversal queries and root discovery); tests use this so no packets leave
|
||||
// the process.
|
||||
func (t *Traverser) SetExchange(fn dns.ExchangeFunc) {
|
||||
t.exchange = fn
|
||||
t.client = dns.NewClient(t.config.QueryConfig, fn)
|
||||
}
|
||||
|
||||
func (t *Traverser) SetHooks(hooks *TraverserHooks) {
|
||||
if t.config == nil {
|
||||
t.config = DefaultTraverserConfig()
|
||||
}
|
||||
t.config.Hooks = hooks
|
||||
}
|
||||
|
||||
func (t *Traverser) Traverse(ctx context.Context, name string) ([]TraversalResult, error) {
|
||||
name = miekgdns.Fqdn(name)
|
||||
|
||||
roots, err := t.discoverRoots(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("root discovery: %w", err)
|
||||
}
|
||||
|
||||
initial := NewReferral(name, t.config.QueryType, ".", 0, 1.0, nil)
|
||||
initial.Addresses = roots
|
||||
|
||||
stack := NewStack(t.config.MaxDepth)
|
||||
stack.Push(initial)
|
||||
|
||||
rootCache := NewInfoCache(nil)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
results []TraversalResult
|
||||
)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return results, fmt.Errorf("traversal cancelled: %w", ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
ref := stack.Pop()
|
||||
if ref == nil {
|
||||
break
|
||||
}
|
||||
|
||||
var cache *InfoCache
|
||||
if t.config.Fast {
|
||||
// Fast mode: inherit glue from the shared root cache so earlier
|
||||
// branch discoveries are visible to later branches.
|
||||
cache = rootCache
|
||||
if ref.Parent != nil {
|
||||
cache = rootCache.Child()
|
||||
}
|
||||
} else {
|
||||
// Non-fast mode: every referral gets its own independent cache so
|
||||
// no cross-branch glue is reused, ensuring each path is resolved
|
||||
// from scratch.
|
||||
cache = NewInfoCache(nil)
|
||||
}
|
||||
|
||||
if t.config.Hooks != nil {
|
||||
t.config.Hooks.emit(EventStart, TraversalResult{Referral: ref}, false)
|
||||
}
|
||||
|
||||
resp := t.processReferral(ctx, ref, cache)
|
||||
|
||||
result := TraversalResult{Referral: ref, Response: resp}
|
||||
if t.config.Hooks != nil {
|
||||
t.config.Hooks.emit(EventComplete, result, false)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
|
||||
if resp.IsTerminal() {
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.Type == RespReferral {
|
||||
children := resp.ChildReferrals()
|
||||
for _, child := range children {
|
||||
if !stack.Push(child) {
|
||||
mu.Lock()
|
||||
results = append(results, TraversalResult{
|
||||
Referral: child,
|
||||
Response: &Response{
|
||||
Referral: child,
|
||||
Type: RespError,
|
||||
},
|
||||
})
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Type == RespCNAMEFollow {
|
||||
follow := resp.CNAMEFollowReferral()
|
||||
if follow != nil {
|
||||
// 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,
|
||||
Response: &Response{
|
||||
Referral: follow,
|
||||
Type: RespError,
|
||||
},
|
||||
})
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
// ServersEncountered returns every server name seen during the run mapped to
|
||||
// its known IP addresses (traverser.rb servers_encountered).
|
||||
func (t *Traverser) ServersEncountered() map[string][]string {
|
||||
return t.seen
|
||||
}
|
||||
|
||||
func (t *Traverser) discoverRoots(ctx context.Context) ([]net.IP, error) {
|
||||
if len(t.config.RootAddrs) > 0 {
|
||||
return t.config.RootAddrs, nil
|
||||
// Roots performs (and memoises) root discovery, returning the start servers
|
||||
// the traversal will begin from. Callers may use it before Run to report the
|
||||
// initial root; Run reuses the memoised result.
|
||||
func (t *Traverser) Roots(ctx context.Context) ([]StartServer, error) {
|
||||
if t.roots == nil {
|
||||
roots, err := t.rootStartServers(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("root discovery: %w", err)
|
||||
}
|
||||
t.roots = roots
|
||||
}
|
||||
return t.roots, nil
|
||||
}
|
||||
|
||||
servers, err := dns.DiscoverRoots(ctx, t.config.RootConfig)
|
||||
// Run traverses the DNS for name and returns the synthetic rootroot node
|
||||
// (never displayed) whose Stats aggregate every leaf outcome; the per-leaf
|
||||
// probabilities sum to 1.0.
|
||||
func (t *Traverser) Run(ctx context.Context, name string) (*Referral, error) {
|
||||
roots, err := t.Roots(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var addrs []net.IP
|
||||
for _, srv := range servers {
|
||||
addrs = append(addrs, srv.AllIPs(false)...)
|
||||
cache := NewInfoCache(nil)
|
||||
cache.AddHints("", roots)
|
||||
|
||||
root := &Referral{
|
||||
RefID: "",
|
||||
Qname: canonicalName(toASCII(name)),
|
||||
Qclass: miekgdns.ClassINET,
|
||||
Qtype: t.config.QueryType,
|
||||
NSAType: dns.TypeA,
|
||||
Server: "",
|
||||
Bailiwick: "",
|
||||
InfoCache: cache,
|
||||
Status: RefStatusNormal,
|
||||
Responses: make(map[string]*ServerResponse),
|
||||
Children: make(map[string][]*Referral),
|
||||
ServerWeights: make(map[string]float64),
|
||||
client: t.client,
|
||||
maxdepth: t.config.MaxDepth,
|
||||
}
|
||||
return addrs, nil
|
||||
t.config.Hooks.emit(StageNew, root, "")
|
||||
|
||||
if err := t.run(ctx, root); err != nil {
|
||||
return root, err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *InfoCache) *Response {
|
||||
if !ref.HasAddresses() {
|
||||
t.mu.Lock()
|
||||
visitedCopy := make(map[string]bool)
|
||||
for k, v := range t.visited {
|
||||
visitedCopy[k] = v
|
||||
}
|
||||
t.mu.Unlock()
|
||||
// stack markers mirroring Ruby's :calc_resolve / :calc_answer placeholders:
|
||||
// the referral is revisited after its resolves/children finished, giving
|
||||
// post-order statistics calculation without recursion.
|
||||
type stackMarker int
|
||||
|
||||
// Resolve the nameserver's IP address. The NS hostname is stored in
|
||||
// Bailiwick; ref.Name is the domain being queried (not the NS name).
|
||||
nsToResolve := ref.Bailiwick
|
||||
if nsToResolve == "" || nsToResolve == "." {
|
||||
nsToResolve = ref.Name
|
||||
}
|
||||
nsName := strings.TrimSuffix(nsToResolve, ".")
|
||||
const (
|
||||
markerNone stackMarker = iota
|
||||
markerCalcResolve
|
||||
markerCalcAnswer
|
||||
)
|
||||
|
||||
ref.Addresses = t.resolveGlueViaSystem(ctx, nsToResolve, cache)
|
||||
if len(ref.Addresses) > 0 {
|
||||
ref.State = StateResolved
|
||||
} else {
|
||||
addrs, err := t.ResolveNS(ctx, nsToResolve, cache, visitedCopy, t.depth)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
Referral: ref,
|
||||
Type: RespNSResolutionFailed,
|
||||
ErrorMessage: fmt.Sprintf("nameserver %s could not be resolved", nsName),
|
||||
}
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
ref.Addresses = addrs
|
||||
ref.State = StateResolved
|
||||
} else {
|
||||
return &Response{
|
||||
Referral: ref,
|
||||
Type: RespNSResolutionFailed,
|
||||
ErrorMessage: fmt.Sprintf("nameserver %s could not be resolved", nsName),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range ref.Addresses {
|
||||
resp := t.queryServer(ctx, ref, addr, cache)
|
||||
if resp != nil && resp.Type != RespSERVFAIL {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
return &Response{
|
||||
Referral: ref,
|
||||
Type: RespSERVFAIL,
|
||||
}
|
||||
type stackEntry struct {
|
||||
ref *Referral
|
||||
marker stackMarker
|
||||
}
|
||||
|
||||
func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCache, visited map[string]bool, depth int) ([]net.IP, error) {
|
||||
if cache != nil {
|
||||
if addrs := cache.LookupGlue(nsName); len(addrs) > 0 {
|
||||
return addrs, nil
|
||||
}
|
||||
func (t *Traverser) run(ctx context.Context, root *Referral) error {
|
||||
stack := []stackEntry{{ref: root}}
|
||||
pop := func() stackEntry {
|
||||
e := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
return e
|
||||
}
|
||||
|
||||
if visited != nil {
|
||||
if visited[nsName] {
|
||||
return nil, &CircularReferralError{
|
||||
Name: nsName,
|
||||
Chain: getVisitedNames(visited),
|
||||
}
|
||||
}
|
||||
visited[nsName] = true
|
||||
}
|
||||
|
||||
if depth > DefaultMaxDepth {
|
||||
return nil, &UnresolvableNameserverError{
|
||||
Name: nsName,
|
||||
Reason: "max depth exceeded",
|
||||
}
|
||||
}
|
||||
|
||||
roots, err := t.discoverRoots(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("root discovery: %w", err)
|
||||
}
|
||||
|
||||
ref := NewReferral(nsName, dns.TypeA, ".", 0, 1.0, nil)
|
||||
ref.Addresses = roots
|
||||
ref.State = StateResolved
|
||||
|
||||
traversalCache := NewInfoCache(nil)
|
||||
if visited != nil {
|
||||
for name := range visited {
|
||||
traversalCache.StoreGlue(name, []net.IP{})
|
||||
}
|
||||
}
|
||||
|
||||
var addrs []net.IP
|
||||
var lastErr error
|
||||
|
||||
stack := NewStack(DefaultMaxDepth)
|
||||
stack.Push(ref)
|
||||
|
||||
for {
|
||||
for len(stack) > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("resolution cancelled: %w", ctx.Err())
|
||||
return fmt.Errorf("traversal cancelled: %w", ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
current := stack.Pop()
|
||||
if current == nil {
|
||||
break
|
||||
}
|
||||
e := pop()
|
||||
r := e.ref
|
||||
|
||||
cacheForStep := traversalCache
|
||||
if current.Parent != nil {
|
||||
cacheForStep = traversalCache.Child()
|
||||
}
|
||||
|
||||
if t.config.Hooks != nil {
|
||||
t.config.Hooks.emit(EventStart, TraversalResult{Referral: current}, true)
|
||||
}
|
||||
|
||||
resp := t.processReferral(ctx, current, cacheForStep)
|
||||
|
||||
if t.config.Hooks != nil {
|
||||
t.config.Hooks.emit(EventComplete, TraversalResult{Referral: current, Response: resp}, true)
|
||||
}
|
||||
|
||||
if resp.Type == RespAnswer && len(resp.Decoded.Answers) > 0 {
|
||||
for _, rr := range resp.Decoded.Answers {
|
||||
if a, ok := rr.(*miekgdns.A); ok {
|
||||
addrs = append(addrs, a.A)
|
||||
}
|
||||
if aaaa, ok := rr.(*miekgdns.AAAA); ok {
|
||||
addrs = append(addrs, aaaa.AAAA)
|
||||
}
|
||||
switch e.marker {
|
||||
case markerCalcResolve:
|
||||
r.resolveCalculate()
|
||||
t.config.Hooks.emit(StageResolve, r, "")
|
||||
stack = append(stack, stackEntry{ref: r}) // now needs processing
|
||||
continue
|
||||
case markerCalcAnswer:
|
||||
r.answerCalculate()
|
||||
t.config.Hooks.emit(StageAnswer, r, "")
|
||||
if t.config.Fast && r.Status == RefStatusNormal && !hasLameResponse(r) {
|
||||
t.answered[fastKey(r)] = r
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
if cache != nil {
|
||||
cache.StoreGlue(nsName, addrs)
|
||||
}
|
||||
return addrs, nil
|
||||
if !r.IsRootRoot() {
|
||||
t.recordSeen(r)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Type == RespNXDOMAIN {
|
||||
lastErr = &UnresolvableNameserverError{
|
||||
Name: nsName,
|
||||
Reason: "NXDOMAIN",
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if resp.Type == RespSERVFAIL || resp.Type == RespError || resp.Type == RespNSResolutionFailed {
|
||||
lastErr = fmt.Errorf("server error resolving %s: %s", nsName, resp.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.Type == RespReferral {
|
||||
children := resp.ChildReferrals()
|
||||
for _, child := range children {
|
||||
// Only skip visited names when they have no addresses; if glue
|
||||
// was included in the referral response we still need to query
|
||||
// that child to get the authoritative answer.
|
||||
if visited != nil && visited[child.Name] && !child.HasAddresses() {
|
||||
continue
|
||||
// A new item. Fast mode: an identical completed referral replaces
|
||||
// this one wholesale. noglue/loop nodes are excluded because their
|
||||
// stats carry node-specific attributes and are cheap to recreate.
|
||||
if t.config.Fast && r.Parent != nil {
|
||||
if memo, ok := t.answered[fastKey(r)]; ok && !r.isNoGlue() && !r.isLoop() {
|
||||
r.Parent.replaceChild(r, memo)
|
||||
t.config.Hooks.emit(StageAnswerFast, r, memo.RefID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
t.config.Hooks.emit(StageStart, r, "")
|
||||
|
||||
if !r.Resolved() {
|
||||
// Push the resolve subtree with a calc_resolve placeholder so the
|
||||
// weights are folded in once every resolve leaf completed.
|
||||
stack = append(stack, stackEntry{ref: r, marker: markerCalcResolve})
|
||||
resolves, err := r.resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range resolves {
|
||||
t.config.Hooks.emit(StageNew, c, "")
|
||||
}
|
||||
for i := len(resolves) - 1; i >= 0; i-- {
|
||||
stack = append(stack, stackEntry{ref: resolves[i]})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
stack = append(stack, stackEntry{ref: r, marker: markerCalcAnswer})
|
||||
childrenSets, err := r.process(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
seenParentIP := make(map[string]bool)
|
||||
var flat []*Referral
|
||||
for _, set := range childrenSets {
|
||||
for _, c := range set {
|
||||
if len(childrenSets) > 1 && !seenParentIP[c.ParentIP] {
|
||||
t.config.Hooks.emit(StageNewReferralSet, c, "")
|
||||
seenParentIP[c.ParentIP] = true
|
||||
}
|
||||
if !stack.Push(child) {
|
||||
lastErr = &UnresolvableNameserverError{
|
||||
Name: nsName,
|
||||
Reason: "max depth exceeded during resolution",
|
||||
stage, earlier := StageNew, ""
|
||||
if t.config.Fast {
|
||||
if memo, ok := t.answered[fastKey(c)]; ok {
|
||||
stage, earlier = StageNewFast, memo.RefID
|
||||
}
|
||||
}
|
||||
t.config.Hooks.emit(stage, c, earlier)
|
||||
flat = append(flat, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(addrs) > 0 {
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
return nil, &UnresolvableNameserverError{
|
||||
Name: nsName,
|
||||
Reason: "resolution exhausted without answer",
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Traverser) queryServer(ctx context.Context, ref *Referral, server net.IP, cache *InfoCache) *Response {
|
||||
var msg *miekgdns.Msg
|
||||
var err error
|
||||
|
||||
if t.exchange != nil {
|
||||
msg, err = t.iterativeQueryWithExchange(ctx, server, ref.Name, ref.Qtype)
|
||||
} else {
|
||||
msg, err = dns.Query(ctx, server, ref.Name, ref.Qtype, t.config.QueryConfig)
|
||||
if err == nil {
|
||||
msg = t.ensureRDFalse(msg, server, ref.Name, ref.Qtype, t.config.QueryConfig)
|
||||
for i := len(flat) - 1; i >= 0; i-- {
|
||||
stack = append(stack, stackEntry{ref: flat[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return &Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: RespError,
|
||||
}
|
||||
}
|
||||
|
||||
resp := NewResponse(ref, server, cache)
|
||||
resp.Process(msg)
|
||||
return resp
|
||||
}
|
||||
|
||||
func (t *Traverser) iterativeQueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16) (*miekgdns.Msg, error) {
|
||||
if t.config.QueryConfig == nil {
|
||||
return dns.IterativeQueryWithExchange(ctx, server, name, qtype, nil, t.exchange)
|
||||
}
|
||||
return dns.IterativeQueryWithExchange(ctx, server, name, qtype, t.config.QueryConfig, t.exchange)
|
||||
}
|
||||
|
||||
func (t *Traverser) ensureRDFalse(msg *miekgdns.Msg, server net.IP, name string, qtype uint16, cfg *dns.QueryConfig) *miekgdns.Msg {
|
||||
if msg != nil && msg.RecursionDesired {
|
||||
if t.exchange != nil {
|
||||
ctx := context.Background()
|
||||
var err error
|
||||
msg, err = t.iterativeQueryWithExchange(ctx, server, name, qtype)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return msg
|
||||
}
|
||||
msg.RecursionDesired = false
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (t *Traverser) resolveGlueViaSystem(ctx context.Context, name string, cache *InfoCache) []net.IP {
|
||||
if cache != nil {
|
||||
if addrs := cache.LookupGlue(name); len(addrs) > 0 {
|
||||
return addrs
|
||||
}
|
||||
}
|
||||
|
||||
c := &miekgdns.Client{
|
||||
Net: "udp",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return nil
|
||||
}
|
||||
c.ReadTimeout = remaining
|
||||
c.WriteTimeout = remaining
|
||||
}
|
||||
|
||||
fqdn := miekgdns.Fqdn(name)
|
||||
|
||||
aMsg, _, err := c.ExchangeContext(ctx, newAQuery(fqdn), "127.0.0.1:53")
|
||||
if err == nil {
|
||||
var addrs []net.IP
|
||||
for _, rr := range aMsg.Answer {
|
||||
if a, ok := rr.(*miekgdns.A); ok {
|
||||
addrs = append(addrs, a.A)
|
||||
}
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
if cache != nil {
|
||||
cache.StoreGlue(name, addrs)
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newAQuery(name string) *miekgdns.Msg {
|
||||
m := new(miekgdns.Msg)
|
||||
m.SetQuestion(name, miekgdns.TypeA)
|
||||
m.RecursionDesired = true
|
||||
return m
|
||||
// fastKey is the fast-mode memo key (traverser.rb): qname/qclass/qtype/
|
||||
// server plus the per-IP weights, lowercased.
|
||||
func fastKey(r *Referral) string {
|
||||
return strings.ToLower(fmt.Sprintf("%s:%s:%s:%s:%s",
|
||||
r.Qname, ClassToString(r.Qclass), TypeToString(r.Qtype), r.Server, r.TxtIPsVerbose()))
|
||||
}
|
||||
|
||||
func hasLameResponse(r *Referral) bool {
|
||||
for _, resp := range r.Responses {
|
||||
if resp.Status == StatusReferralLame {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Traverser) recordSeen(r *Referral) {
|
||||
name := strings.ToLower(r.Server)
|
||||
existing := t.seen[name]
|
||||
for _, ip := range r.IPsAsArray() {
|
||||
found := false
|
||||
for _, have := range existing {
|
||||
if have == ip {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
existing = append(existing, ip)
|
||||
}
|
||||
}
|
||||
t.seen[name] = existing
|
||||
}
|
||||
|
||||
// rootStartServers returns the root servers as start-server hints: either
|
||||
// the pre-seeded RootAddrs or the servers found via root discovery (one by
|
||||
// default, all of them with AllRoots). IPv4 only, like the reference.
|
||||
func (t *Traverser) rootStartServers(ctx context.Context) ([]StartServer, error) {
|
||||
if len(t.config.RootAddrs) > 0 {
|
||||
var out []StartServer
|
||||
for _, ip := range t.config.RootAddrs {
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
out = append(out, StartServer{Name: v4.String(), IPs: []string{v4.String()}})
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("no usable IPv4 root addresses")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
rootCfg := t.config.RootConfig
|
||||
if t.exchange != nil {
|
||||
var cp dns.RootDiscoveryConfig
|
||||
if rootCfg != nil {
|
||||
cp = *rootCfg
|
||||
}
|
||||
cp.Exchange = t.exchange
|
||||
rootCfg = &cp
|
||||
}
|
||||
|
||||
servers, err := dns.DiscoverRoots(ctx, rootCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []StartServer
|
||||
for _, srv := range servers {
|
||||
var ips []string
|
||||
for _, ip := range srv.IPv4 {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, StartServer{Name: canonicalName(srv.Name), IPs: ips})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("no root servers with IPv4 addresses")
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user