diff --git a/internal/dns/decode.go b/internal/dns/decode.go index 3e868f1..5a9d605 100644 --- a/internal/dns/decode.go +++ b/internal/dns/decode.go @@ -1,7 +1,6 @@ package dns import ( - "fmt" "strings" "github.com/miekg/dns" @@ -234,12 +233,5 @@ func FormatRecord(rr dns.RR) string { if rr == nil { return "" } - header := rr.Header() - return fmt.Sprintf("%s %d %s %s %s", - header.Name, - header.Ttl, - dns.ClassToString[header.Class], - QNameType(header.Rrtype), - rr.String(), - ) + return rr.String() } diff --git a/internal/output/stats.go b/internal/output/stats.go index b7c1aa2..48109dd 100644 --- a/internal/output/stats.go +++ b/internal/output/stats.go @@ -196,3 +196,54 @@ func containsString(items []string, target string) bool { } return false } + +// DeduplicateResults collapses terminal results that represent the same +// outcome from the same server into a single entry with summed probability. +// This prevents the same nameserver failure (or answer) from appearing once +// per delegation path when several parent servers all refer to the same child. +func DeduplicateResults(results []traverse.TraversalResult) []traverse.TraversalResult { + type entry struct { + result traverse.TraversalResult + prob float64 + } + keys := make(map[string]*entry) + var order []string + + for _, r := range results { + if r.Response == nil || r.Referral == nil { + continue + } + key := resultDeduplicationKey(r) + if e, ok := keys[key]; ok { + e.prob += r.Referral.Prob + } else { + keys[key] = &entry{result: r, prob: r.Referral.Prob} + order = append(order, key) + } + } + + deduped := make([]traverse.TraversalResult, 0, len(order)) + for _, key := range order { + e := keys[key] + refCopy := *e.result.Referral + refCopy.Prob = e.prob + deduped = append(deduped, traverse.TraversalResult{ + Referral: &refCopy, + Response: e.result.Response, + }) + } + return deduped +} + +func resultDeduplicationKey(r traverse.TraversalResult) string { + bailiwick := strings.TrimSuffix(r.Referral.Bailiwick, ".") + switch r.Response.Type { + case traverse.RespAnswer: + key, _ := answerKey(r.Response) + return "answer:" + bailiwick + ":" + key + case traverse.RespNSResolutionFailed: + return "ns_error:" + r.Response.ErrorMessage + default: + return r.Response.Type.String() + ":" + bailiwick + ":" + r.Response.ErrorMessage + } +} diff --git a/internal/output/text.go b/internal/output/text.go index 53b657a..4891a21 100644 --- a/internal/output/text.go +++ b/internal/output/text.go @@ -120,7 +120,8 @@ func (f *textFormatter) writeResults(results []traverse.TraversalResult) error { } terminal := terminalResults(results) - for _, result := range terminal { + deduped := DeduplicateResults(terminal) + for _, result := range deduped { prefix := strings.Repeat(" ", result.Referral.Depth+1) line := prefix + f.formatResultLine(result) if _, err := fmt.Fprintln(f.w, line); err != nil { @@ -196,14 +197,15 @@ func (f *textFormatter) formatResultLine(result traverse.TraversalResult) string prob := formatProbability(result.Referral.Prob) switch result.Response.Type { case traverse.RespAnswer: - key, rrs := answerKey(result.Response) + key, _ := answerKey(result.Response) if key == "" { return fmt.Sprintf("%s resulted in answer", prob) } - if len(rrs) == 1 { - return f.colorize(fmt.Sprintf("%s answered with %s", prob, rrs[0]), colorGreen) + nsLabel := "" + if result.Referral.Bailiwick != "" && result.Referral.Bailiwick != "." { + nsLabel = trimDomain(result.Referral.Bailiwick) + " " } - return f.colorize(fmt.Sprintf("%s answered with %s", prob, strings.Join(rrs, " / ")), colorGreen) + return f.colorize(fmt.Sprintf("%s %sanswered with %s", prob, nsLabel, key), colorGreen) case traverse.RespNODATA: return fmt.Sprintf("%s found no such record", prob) case traverse.RespNXDOMAIN: diff --git a/internal/traverse/referral.go b/internal/traverse/referral.go index ca78481..c5a0917 100644 --- a/internal/traverse/referral.go +++ b/internal/traverse/referral.go @@ -230,7 +230,10 @@ func (r *Referral) Resolve(ctx context.Context, traverser *Traverser, cache *Inf if resp.Type == RespReferral { children := resp.ChildReferrals() for _, child := range children { - if visited != nil && visited[child.Name] { + // 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 } if !stack.Push(child) { diff --git a/internal/traverse/traverser.go b/internal/traverse/traverser.go index e139db1..185c2d1 100644 --- a/internal/traverse/traverser.go +++ b/internal/traverse/traverser.go @@ -227,15 +227,19 @@ func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *I } t.mu.Unlock() - ref.Addresses = t.resolveGlueViaSystem(ctx, ref.Name, cache) + // 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, ".") + + ref.Addresses = t.resolveGlueViaSystem(ctx, nsToResolve, cache) if len(ref.Addresses) > 0 { ref.State = StateResolved } else { - addrs, err := t.ResolveNS(ctx, ref.Name, cache, visitedCopy, t.depth) - nsName := strings.TrimSuffix(ref.Bailiwick, ".") - if nsName == "" || nsName == "." { - nsName = strings.TrimSuffix(ref.Name, ".") - } + addrs, err := t.ResolveNS(ctx, nsToResolve, cache, visitedCopy, t.depth) if err != nil { return &Response{ Referral: ref, @@ -375,7 +379,10 @@ func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCac if resp.Type == RespReferral { children := resp.ChildReferrals() for _, child := range children { - if visited != nil && visited[child.Name] { + // 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 } if !stack.Push(child) {