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:
Gary Hansen
2026-07-07 21:42:06 +10:00
co-authored by Claude Fable 5
parent af15c9c2d4
commit d71c7fbef2
53 changed files with 6685 additions and 9366 deletions
+200 -430
View File
@@ -1,9 +1,11 @@
// Package integration provides end-to-end tests for ExploreDNS using a mock
// DNS server that allows deterministic, network-independent testing.
// DNS exchange that allows deterministic, network-independent testing of the
// full engine through its exported API.
package integration
import (
"context"
"math"
"net"
"testing"
"time"
@@ -13,523 +15,291 @@ import (
"github.com/miekg/dns"
)
// mockZone represents a simple in-memory DNS zone for testing.
type mockZone struct {
// map[name][qtype] → []RR
records map[string]map[uint16][]dns.RR
// mockNet maps (server IP, qname, qtype) to a canned response, mirroring how
// distinct nameservers answer differently for the same question.
type mockNet struct {
responses map[string]*dns.Msg
}
func newMockZone() *mockZone {
return &mockZone{records: make(map[string]map[uint16][]dns.RR)}
func newMockNet() *mockNet {
return &mockNet{responses: make(map[string]*dns.Msg)}
}
func (z *mockZone) addA(name, ip string) {
fqdn := dns.Fqdn(name)
if z.records[fqdn] == nil {
z.records[fqdn] = make(map[uint16][]dns.RR)
func key(server, qname string, qtype uint16) string {
return server + "|" + dns.Fqdn(qname) + "|" + dns.TypeToString[qtype]
}
func (m *mockNet) on(server, qname string, qtype uint16, msg *dns.Msg) {
m.responses[key(server, qname, qtype)] = msg
}
func (m *mockNet) exchange(_ context.Context, server string, msg *dns.Msg, _ bool) (*dns.Msg, error) {
host := server
if h, _, err := net.SplitHostPort(server); err == nil {
host = h
}
z.records[fqdn][dns.TypeA] = append(z.records[fqdn][dns.TypeA], &dns.A{
Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
A: net.ParseIP(ip),
})
q := msg.Question[0]
resp, ok := m.responses[key(host, q.Name, q.Qtype)]
if !ok {
// Unknown question: NXDOMAIN, like an authoritative miss.
out := new(dns.Msg)
out.SetReply(msg)
out.Rcode = dns.RcodeNameError
return out, nil
}
out := resp.Copy()
out.SetReply(msg)
out.Answer, out.Ns, out.Extra = resp.Answer, resp.Ns, resp.Extra
out.Rcode = resp.Rcode
return out, nil
}
func (z *mockZone) addNS(zone, ns string) {
fqdn := dns.Fqdn(zone)
if z.records[fqdn] == nil {
z.records[fqdn] = make(map[uint16][]dns.RR)
func aRR(name, ip string) dns.RR {
return &dns.A{
Hdr: dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
A: net.ParseIP(ip).To4(),
}
z.records[fqdn][dns.TypeNS] = append(z.records[fqdn][dns.TypeNS], &dns.NS{
Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300},
Ns: dns.Fqdn(ns),
})
}
func (z *mockZone) addCNAME(name, target string) {
fqdn := dns.Fqdn(name)
if z.records[fqdn] == nil {
z.records[fqdn] = make(map[uint16][]dns.RR)
func nsRR(zone, target string) dns.RR {
return &dns.NS{
Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300},
Ns: dns.Fqdn(target),
}
z.records[fqdn][dns.TypeCNAME] = append(z.records[fqdn][dns.TypeCNAME], &dns.CNAME{
Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 300},
}
func cnameRR(name, target string) dns.RR {
return &dns.CNAME{
Hdr: dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 300},
Target: dns.Fqdn(target),
})
}
// makeExchange creates a mock ExchangeFunc that serves responses from the zone.
// It simulates referral behavior: if a name matches a zone NS record, it returns
// a referral with glue. If it matches an A record, it returns the answer.
func (z *mockZone) makeExchange() dnsinternal.ExchangeFunc {
return func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
if len(msg.Question) == 0 {
return nil, nil
}
q := msg.Question[0]
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Authoritative = true
// Direct answer
if rrs, ok := z.records[q.Name]; ok {
if answers, ok := rrs[q.Qtype]; ok {
resp.Answer = append(resp.Answer, answers...)
return resp, nil
}
// CNAME chain — return CNAME + answer for target if qtype != CNAME
if cnameRRs, ok := rrs[dns.TypeCNAME]; ok && q.Qtype != dns.TypeCNAME {
resp.Answer = append(resp.Answer, cnameRRs...)
return resp, nil
}
}
// Check for zone delegation: look for NS records covering any suffix of qname
labels := dns.SplitDomainName(q.Name)
for i := 0; i < len(labels); i++ {
zone := dns.Fqdn(joinLabels(labels[i:]))
if nsRRs, ok := z.records[zone][dns.TypeNS]; ok && zone != q.Name {
// Return referral
resp.Authoritative = false
resp.Ns = append(resp.Ns, nsRRs...)
for _, ns := range nsRRs {
nsName := ns.(*dns.NS).Ns
if aRRs, ok := z.records[nsName][dns.TypeA]; ok {
resp.Extra = append(resp.Extra, aRRs...)
}
}
return resp, nil
}
}
// NXDOMAIN
resp.Authoritative = true
resp.Rcode = dns.RcodeNameError
return resp, nil
}
}
func joinLabels(labels []string) string {
result := ""
for i, l := range labels {
if i > 0 {
result += "."
}
result += l
}
return result
func answerMsg(rrs ...dns.RR) *dns.Msg {
m := new(dns.Msg)
m.Answer = rrs
return m
}
// setupTestZone creates a mock zone with a typical referral hierarchy:
//
// root → com (referral) → example.com (referral) → www.example.com (A)
func setupTestZone() *mockZone {
z := newMockZone()
// Root server glue
z.addA("a.root-servers.test", "198.41.0.4")
// com TLD referral from root
z.addNS("com", "a.gtld-servers.test")
z.addA("a.gtld-servers.test", "192.5.6.30")
// example.com NS referral from com TLD
z.addNS("example.com", "ns1.example.com")
z.addA("ns1.example.com", "1.2.3.4")
// Actual A records
z.addA("example.com", "93.184.216.34")
z.addA("www.example.com", "93.184.216.34")
return z
func referralMsg(nsRRs []dns.RR, glue ...dns.RR) *dns.Msg {
m := new(dns.Msg)
m.Ns = nsRRs
m.Extra = glue
return m
}
// TestIntegrationSimpleAQuery verifies end-to-end traversal with mock DNS
// that returns A record answers without network dependency.
func TestIntegrationSimpleAQuery(t *testing.T) {
z := setupTestZone()
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
func newTraverser(maxDepth int) *traverse.Traverser {
return traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: maxDepth,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
QueryConfig: &dnsinternal.QueryConfig{
Retries: 1,
Timeout: time.Second,
RetryDelay: time.Millisecond,
},
})
tr.SetExchange(z.makeExchange())
}
func run(t *testing.T, tr *traverse.Traverser, m *mockNet, qname string) *traverse.Referral {
t.Helper()
tr.SetExchange(m.exchange)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "example.com")
root, err := tr.Run(ctx, qname)
if err != nil {
t.Fatalf("Traverse: %v", err)
}
if len(results) == 0 {
t.Fatal("expected results from traversal")
t.Fatalf("Run(%q): %v", qname, err)
}
assertProbabilityInvariant(t, root)
return root
}
var foundAnswer bool
for _, r := range results {
if r.Response != nil && r.Response.Type == traverse.RespAnswer {
foundAnswer = true
if r.Response.Decoded != nil && len(r.Response.Decoded.Answers) > 0 {
for _, rr := range r.Response.Decoded.Answers {
if a, ok := rr.(*dns.A); ok {
t.Logf("Found A record: %v", a.A)
}
}
}
}
// assertProbabilityInvariant checks the engine ground rule: aggregated leaf
// probabilities at the root sum to 1.0.
func assertProbabilityInvariant(t *testing.T, root *traverse.Referral) {
t.Helper()
sum := 0.0
for _, leaf := range root.StatsList() {
sum += leaf.Prob
}
if !foundAnswer {
t.Errorf("expected to find an answer response; got types: %v", responseTypes(results))
if math.Abs(sum-1.0) > 1e-9 {
t.Errorf("leaf probabilities sum to %v, want 1.0", sum)
}
}
// TestIntegrationReferralChain verifies multi-hop referral traversal:
// root → com → example.com, with glue records at each step.
func statuses(root *traverse.Referral) map[traverse.Status]float64 {
out := make(map[traverse.Status]float64)
for _, leaf := range root.StatsList() {
out[leaf.Response.Status] += leaf.Prob
}
return out
}
// TestIntegrationReferralChain verifies the classic delegation walk:
// root → com → example.com with per-server responses.
func TestIntegrationReferralChain(t *testing.T) {
z := newMockZone()
m := newMockNet()
m.on("198.41.0.4", "www.example.com", dns.TypeA, referralMsg(
[]dns.RR{nsRR("com", "a.gtld-servers.test")},
aRR("a.gtld-servers.test", "192.5.6.30"),
))
m.on("192.5.6.30", "www.example.com", dns.TypeA, referralMsg(
[]dns.RR{nsRR("example.com", "ns1.example.com")},
aRR("ns1.example.com", "1.2.3.4"),
))
m.on("1.2.3.4", "www.example.com", dns.TypeA, answerMsg(aRR("www.example.com", "93.184.216.34")))
// Root delegates to com
z.addNS("com", "a.gtld-servers.test")
z.addA("a.gtld-servers.test", "192.5.6.30")
// TLD delegates to example.com
z.addNS("example.com", "ns1.example.com")
z.addA("ns1.example.com", "1.2.3.4")
// Authoritative answer
z.addA("example.com", "93.184.216.34")
exchange := z.makeExchange()
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(exchange)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "example.com")
if err != nil {
t.Fatalf("Traverse: %v", err)
}
// Count referrals and answers
var referrals, answers int
for _, r := range results {
if r.Response == nil {
continue
}
switch r.Response.Type {
case traverse.RespReferral:
referrals++
case traverse.RespAnswer:
answers++
}
}
t.Logf("referrals=%d answers=%d total=%d", referrals, answers, len(results))
if answers == 0 {
t.Errorf("expected at least one answer; types: %v", responseTypes(results))
root := run(t, newTraverser(10), m, "www.example.com")
got := statuses(root)
if math.Abs(got[traverse.StatusAnswered]-1.0) > 1e-9 {
t.Errorf("statuses = %v, want 100%% answered", got)
}
}
// TestIntegrationCNAMEResolution verifies that CNAME chains are followed correctly.
func TestIntegrationCNAMEResolution(t *testing.T) {
z := newMockZone()
// TestIntegrationCNAMERestart verifies that an out-of-zone CNAME target
// restarts the traversal from the branch cache.
func TestIntegrationCNAMERestart(t *testing.T) {
m := newMockNet()
m.on("198.41.0.4", "www.example.com", dns.TypeA, referralMsg(
[]dns.RR{nsRR("example.com", "ns1.example.com")},
aRR("ns1.example.com", "1.2.3.4"),
))
m.on("1.2.3.4", "www.example.com", dns.TypeA, answerMsg(cnameRR("www.example.com", "cdn.example.net")))
m.on("198.41.0.4", "cdn.example.net", dns.TypeA, referralMsg(
[]dns.RR{nsRR("example.net", "ns1.example.net")},
aRR("ns1.example.net", "5.6.7.8"),
))
m.on("5.6.7.8", "cdn.example.net", dns.TypeA, answerMsg(aRR("cdn.example.net", "93.184.216.35")))
// www.example.com → CNAME → example.com → A record
z.addCNAME("www.example.com", "example.com")
z.addA("example.com", "93.184.216.34")
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(z.makeExchange())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "www.example.com")
if err != nil {
t.Fatalf("Traverse CNAME: %v", err)
root := run(t, newTraverser(10), m, "www.example.com")
got := statuses(root)
if math.Abs(got[traverse.StatusAnswered]-1.0) > 1e-9 {
t.Errorf("statuses = %v, want 100%% answered via restart", got)
}
if len(results) == 0 {
t.Fatal("expected results")
}
var foundCNAME, foundAnswer bool
for _, r := range results {
if r.Response == nil {
continue
}
if r.Response.Type == traverse.RespCNAMEFollow {
foundCNAME = true
}
if r.Response.Type == traverse.RespAnswer {
foundAnswer = true
for _, leaf := range root.StatsList() {
if leaf.Response.Status == traverse.StatusAnswered && leaf.Response.Qname != "cdn.example.net" {
t.Errorf("answered qname = %q, want the CNAME target", leaf.Response.Qname)
}
}
t.Logf("CNAME traversal: foundCNAME=%v foundAnswer=%v types=%v", foundCNAME, foundAnswer, responseTypes(results))
}
// TestIntegrationNXDOMAIN verifies that NXDOMAIN responses are correctly classified.
// TestIntegrationNXDOMAIN verifies rcode errors surface as error leaves with
// the reference wording.
func TestIntegrationNXDOMAIN(t *testing.T) {
z := newMockZone()
// Zone has no records for nonexistent.example.com
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(z.makeExchange())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "nonexistent.example.test")
if err != nil {
t.Fatalf("Traverse NXDOMAIN: %v", err)
m := newMockNet()
// mock returns NXDOMAIN for anything unmocked
root := run(t, newTraverser(10), m, "nonexistent.example.test")
got := statuses(root)
if math.Abs(got[traverse.StatusError]-1.0) > 1e-9 {
t.Errorf("statuses = %v, want 100%% error", got)
}
if len(results) == 0 {
t.Fatal("expected at least one result for NXDOMAIN")
}
var foundNXDOMAIN bool
for _, r := range results {
if r.Response != nil && r.Response.Type == traverse.RespNXDOMAIN {
foundNXDOMAIN = true
break
for _, leaf := range root.StatsList() {
if leaf.Response.DQ.ErrorMessage != "No such domain (NXDOMAIN)" {
t.Errorf("error message = %q", leaf.Response.DQ.ErrorMessage)
}
}
if !foundNXDOMAIN {
t.Errorf("expected NXDOMAIN result; got: %v", responseTypes(results))
}
}
// TestIntegrationSERVFAIL verifies that SERVFAIL responses are correctly handled.
// TestIntegrationSERVFAIL verifies SERVFAIL classification.
func TestIntegrationSERVFAIL(t *testing.T) {
sfMsg := new(dns.Msg)
sfMsg.Rcode = dns.RcodeServerFailure
m := newMockNet()
sf := new(dns.Msg)
sf.Rcode = dns.RcodeServerFailure
m.on("198.41.0.4", "example.com", dns.TypeA, sf)
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
return sfMsg.Copy(), nil
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "example.com")
if err != nil {
t.Fatalf("Traverse SERVFAIL: %v", err)
}
if len(results) == 0 {
t.Fatal("expected at least one result")
}
if results[0].Response.Type != traverse.RespSERVFAIL {
t.Errorf("expected SERVFAIL, got %v", results[0].Response.Type)
root := run(t, newTraverser(10), m, "example.com")
for _, leaf := range root.StatsList() {
if leaf.Response.Status != traverse.StatusError {
t.Errorf("status = %q, want error", leaf.Response.Status)
}
if leaf.Response.DQ.ErrorMessage != "Server failure (SERVFAIL)" {
t.Errorf("error message = %q", leaf.Response.DQ.ErrorMessage)
}
}
}
// TestIntegrationCNAMELoop verifies that CNAME loops are detected and reported.
// TestIntegrationCNAMELoop verifies cross-response CNAME loops terminate as
// cname_loop leaves.
func TestIntegrationCNAMELoop(t *testing.T) {
callCount := 0
// www.a.test → CNAME → www.b.test → CNAME → www.a.test (loop)
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
callCount++
if len(msg.Question) == 0 {
return nil, nil
}
q := msg.Question[0]
m := newMockNet()
m.on("198.41.0.4", "www.a.test", dns.TypeA, answerMsg(cnameRR("www.a.test", "www.b.test")))
m.on("198.41.0.4", "www.b.test", dns.TypeA, answerMsg(cnameRR("www.b.test", "www.a.test")))
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Authoritative = true
switch q.Name {
case "www.a.test.":
resp.Answer = append(resp.Answer, &dns.CNAME{
Hdr: dns.RR_Header{Name: "www.a.test.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET},
Target: "www.b.test.",
})
case "www.b.test.":
resp.Answer = append(resp.Answer, &dns.CNAME{
Hdr: dns.RR_Header{Name: "www.b.test.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET},
Target: "www.a.test.",
})
default:
resp.Rcode = dns.RcodeNameError
}
return resp, nil
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "www.a.test")
if err != nil {
t.Fatalf("Traverse CNAME loop: %v", err)
}
if len(results) == 0 {
t.Fatal("expected results from CNAME loop traversal")
}
var foundLoop bool
for _, r := range results {
if r.Response != nil && r.Response.Type == traverse.RespCNAMELoop {
foundLoop = true
break
}
}
if !foundLoop {
t.Logf("types found: %v", responseTypes(results))
// CNAME loop detection may vary based on implementation; warn rather than fail
t.Logf("CNAME loop not detected as RespCNAMELoop (may be handled differently)")
root := run(t, newTraverser(10), m, "www.a.test")
got := statuses(root)
if math.Abs(got[traverse.StatusCNAMELoop]-1.0) > 1e-9 {
t.Errorf("statuses = %v, want 100%% cname_loop", got)
}
}
// TestIntegrationMaxDepthExceeded verifies that infinite referral chains are
// cut off at the configured max depth.
// TestIntegrationMaxDepthExceeded verifies that an endless referral chain is
// cut off with a "Maxdepth N exceeded" exception leaf.
func TestIntegrationMaxDepthExceeded(t *testing.T) {
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 3,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
// Always return a referral to ns.example.com
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Ns = append(resp.Ns, &dns.NS{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Class: dns.ClassINET},
Ns: "ns.example.com.",
})
resp.Extra = append(resp.Extra, &dns.A{
Hdr: dns.RR_Header{Name: "ns.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET},
A: net.ParseIP("1.2.3.4"),
})
return resp, nil
})
m := newMockNet()
// Each hop delegates one label deeper: the node at depth 3 (refid 1.1.1)
// is never queried because MaxDepth 3 injects the exception first.
m.on("198.41.0.4", "www.d2.d1", dns.TypeA, referralMsg(
[]dns.RR{nsRR("d1", "ns.d1")},
aRR("ns.d1", "10.0.0.1"),
))
m.on("10.0.0.1", "www.d2.d1", dns.TypeA, referralMsg(
[]dns.RR{nsRR("d2.d1", "ns.d2.d1")},
aRR("ns.d2.d1", "10.0.0.2"),
))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
root := run(t, newTraverser(3), m, "www.d2.d1")
results, err := tr.Traverse(ctx, "deep.example.com")
if err != nil {
t.Fatalf("Traverse: %v", err)
foundMaxdepth := false
for _, leaf := range root.StatsList() {
if leaf.Response.Status == traverse.StatusException &&
leaf.Response.DQ.ExceptionMessage == "Maxdepth 3 exceeded" {
foundMaxdepth = true
}
}
t.Logf("max depth test: %d results, types: %v", len(results), responseTypes(results))
if len(results) == 0 {
t.Fatal("expected results even with max depth exceeded")
if !foundMaxdepth {
t.Errorf("expected a Maxdepth 3 exceeded exception leaf, got %v", statuses(root))
}
}
// TestIntegrationHooksReceiveEvents verifies that traversal hooks receive
// the expected start and complete events.
// TestIntegrationHooksReceiveEvents verifies start/answer events pair up.
func TestIntegrationHooksReceiveEvents(t *testing.T) {
answerMsg := new(dns.Msg)
answerMsg.SetReply(new(dns.Msg))
answerMsg.Answer = append(answerMsg.Answer, &dns.A{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
A: net.ParseIP("93.184.216.34"),
})
m := newMockNet()
m.on("198.41.0.4", "example.com", dns.TypeA, answerMsg(aRR("example.com", "93.184.216.34")))
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
return answerMsg.Copy(), nil
})
var startEvents, completeEvents int
tr := newTraverser(10)
var startEvents, answerEvents int
tr.SetHooks(&traverse.TraverserHooks{
OnEvent: func(e traverse.TraversalEvent) {
switch e.Stage {
case traverse.EventStart:
case traverse.StageStart:
startEvents++
case traverse.EventComplete:
completeEvents++
case traverse.StageAnswer:
answerEvents++
}
},
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := tr.Traverse(ctx, "example.com")
if err != nil {
t.Fatalf("Traverse: %v", err)
}
_ = results
run(t, tr, m, "example.com")
if startEvents == 0 {
t.Error("expected at least one start event")
}
if completeEvents == 0 {
t.Error("expected at least one complete event")
}
if startEvents != completeEvents {
t.Errorf("start events (%d) != complete events (%d)", startEvents, completeEvents)
if startEvents != answerEvents {
t.Errorf("start events (%d) != answer events (%d)", startEvents, answerEvents)
}
}
// TestIntegrationContextCancellation verifies that the traversal respects
// context cancellation and returns an appropriate error.
func TestIntegrationContextCancellation(t *testing.T) {
tr := traverse.NewTraverser(&traverse.TraverserConfig{
MaxDepth: 10,
QueryType: dnsinternal.TypeA,
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
})
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
// Always return referral to keep loop going
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Ns = append(resp.Ns, &dns.NS{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS},
Ns: "ns.example.com.",
})
resp.Extra = append(resp.Extra, &dns.A{
Hdr: dns.RR_Header{Name: "ns.example.com.", Rrtype: dns.TypeA},
A: net.ParseIP("1.2.3.4"),
})
return resp, nil
})
m := newMockNet()
m.on("198.41.0.4", "example.com", dns.TypeA, answerMsg(aRR("example.com", "93.184.216.34")))
tr := newTraverser(10)
tr.SetExchange(m.exchange)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel before traversal starts
cancel() // cancel before traversal starts
_, err := tr.Traverse(ctx, "example.com")
if err == nil {
if _, err := tr.Run(ctx, "example.com"); err == nil {
t.Fatal("expected error when context is cancelled")
}
}
// responseTypes returns a summary of response types for debugging.
func responseTypes(results []traverse.TraversalResult) []string {
var types []string
for _, r := range results {
if r.Response != nil {
types = append(types, r.Response.Type.String())
} else {
types = append(types, "nil")
}
}
return types
}