package output import ( "bytes" "context" "net" "strings" "testing" "time" idns "gitea.hansenits.com.au/hits/ExploreDNS/internal/dns" "gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse" "github.com/miekg/dns" ) // TestTextOutputMatchesReferenceCapture rebuilds the topology of // docs/captures/dnstraverse-ruby-www.example.com-A.txt (root → com → the two // cloudflare NS, three IPs each) through the mock exchange and asserts the // complete text output byte-for-byte against the reference format. It differs // from the capture only in volatile values: the root chosen, the number of // gTLD servers, and the server fingerprints (versions are disabled so no // network is touched). func TestTextOutputMatchesReferenceCapture(t *testing.T) { responses := map[string]*dns.Msg{} set := func(server, qname string, msg *dns.Msg) { responses[server+"/"+dns.Fqdn(qname)] = msg } a := func(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(), } } ns := func(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), } } // Upstream resolver: ". NS" returns one root with glue (root discovery). rootNS := new(dns.Msg) rootNS.Answer = []dns.RR{ns(".", "m.root-servers.net")} rootNS.Extra = []dns.RR{a("m.root-servers.net", "202.12.27.33")} set("10.0.0.53", ".", rootNS) // Root referral to com (three gTLD servers, all glued). comRef := new(dns.Msg) comRef.Ns = []dns.RR{ ns("com", "a.gtld-servers.net"), ns("com", "b.gtld-servers.net"), ns("com", "c.gtld-servers.net"), } comRef.Extra = []dns.RR{ a("a.gtld-servers.net", "192.5.6.30"), a("b.gtld-servers.net", "192.33.14.30"), a("c.gtld-servers.net", "192.26.92.30"), } set("202.12.27.33", "www.example.com", comRef) // gTLD referral to example.com: two NS, three glue addresses each. heraIPs := []string{"108.162.192.162", "172.64.32.162", "173.245.58.162"} elliottIPs := []string{"108.162.195.228", "162.159.44.228", "172.64.35.228"} exampleRef := new(dns.Msg) exampleRef.Ns = []dns.RR{ ns("example.com", "hera.ns.cloudflare.com"), ns("example.com", "elliott.ns.cloudflare.com"), } for _, ip := range heraIPs { exampleRef.Extra = append(exampleRef.Extra, a("hera.ns.cloudflare.com", ip)) } for _, ip := range elliottIPs { exampleRef.Extra = append(exampleRef.Extra, a("elliott.ns.cloudflare.com", ip)) } for _, ip := range []string{"192.5.6.30", "192.33.14.30", "192.26.92.30"} { set(ip, "www.example.com", exampleRef) } answer := new(dns.Msg) answer.Answer = []dns.RR{ a("www.example.com", "104.20.23.154"), a("www.example.com", "172.66.147.243"), } for _, ip := range append(append([]string{}, heraIPs...), elliottIPs...) { set(ip, "www.example.com", answer) } exchange := func(_ context.Context, server string, msg *dns.Msg, _ bool) (*dns.Msg, error) { host := server if h, _, err := net.SplitHostPort(server); err == nil { host = h } resp, ok := responses[host+"/"+msg.Question[0].Name] if !ok { return nil, &net.DNSError{Err: "no mock", Name: msg.Question[0].Name} } out := resp.Copy() out.SetReply(msg) out.Answer, out.Ns, out.Extra = resp.Answer, resp.Ns, resp.Extra return out, nil } tr := traverse.NewTraverser(&traverse.TraverserConfig{ MaxDepth: traverse.DefaultMaxDepth, QueryType: dns.TypeA, Fast: true, RootConfig: &idns.RootDiscoveryConfig{Resolver: "10.0.0.53:53"}, QueryConfig: &idns.QueryConfig{ Retries: 1, Timeout: time.Second, RetryDelay: time.Millisecond, }, }) tr.SetExchange(exchange) cfg := DefaultConfig() cfg.Domain = "www.example.com" cfg.QueryType = "a" cfg.ShowServers = true cfg.ShowVersions = false cfg.Color = false var buf bytes.Buffer formatter := NewFormatter(cfg, &buf) if _, err := RunTraversal(context.Background(), tr, cfg, formatter, "www.example.com"); err != nil { t.Fatalf("RunTraversal: %v", err) } answerBlock := func(server, ip string) string { return " 16.7%: Answer from " + server + " (" + ip + ")\n" + " www.example.com.\t300\tIN\tA\t104.20.23.154\n" + " www.example.com.\t300\tIN\tA\t172.66.147.243\n" } want := strings.Join([]string{ "# Using fast mode", "# Limiting traverse to one root", "# UDP size 2048 (EDNS0 is on)", "# Retries 2, max depth 20", "# Allow TCP is true, always TCP is false", "Using m.root-servers.net (202.12.27.33) as initial root", "Running query www.example.com type a", "1 m.root-servers.net (202.12.27.33)", "1.1 a.gtld-servers.net (192.5.6.30)", "1.1.1 hera.ns.cloudflare.com (108.162.192.162,172.64.32.162,173.245.58.162)", "1.1.2 elliott.ns.cloudflare.com (108.162.195.228,162.159.44.228,172.64.35.228)", "1.2 b.gtld-servers.net (192.33.14.30)", "1.2.1 hera.ns.cloudflare.com (108.162.192.162,172.64.32.162,173.245.58.162) -- completed earlier (1.1.1)", "1.2.2 elliott.ns.cloudflare.com (108.162.195.228,162.159.44.228,172.64.35.228) -- completed earlier (1.1.2)", "1.3 c.gtld-servers.net (192.26.92.30)", "1.3.1 hera.ns.cloudflare.com (108.162.192.162,172.64.32.162,173.245.58.162) -- completed earlier (1.1.1)", "1.3.2 elliott.ns.cloudflare.com (108.162.195.228,162.159.44.228,172.64.35.228) -- completed earlier (1.1.2)", "", "The following servers were encountered:", " hera.ns.cloudflare.com: 108.162.192.162", " hera.ns.cloudflare.com: 172.64.32.162", " hera.ns.cloudflare.com: 173.245.58.162", "elliott.ns.cloudflare.com: 108.162.195.228", "elliott.ns.cloudflare.com: 162.159.44.228", "elliott.ns.cloudflare.com: 172.64.35.228", " a.gtld-servers.net: 192.5.6.30", " b.gtld-servers.net: 192.33.14.30", " c.gtld-servers.net: 192.26.92.30", " m.root-servers.net: 202.12.27.33", "", "Results:", answerBlock("hera.ns.cloudflare.com", "108.162.192.162"), answerBlock("elliott.ns.cloudflare.com", "108.162.195.228"), answerBlock("elliott.ns.cloudflare.com", "162.159.44.228"), answerBlock("hera.ns.cloudflare.com", "172.64.32.162"), answerBlock("elliott.ns.cloudflare.com", "172.64.35.228"), answerBlock("hera.ns.cloudflare.com", "173.245.58.162") + "\n" + "Summary Results:\n" + " 100% answered with www.example.com. 300 IN A 104.20.23.154\n" + " www.example.com. 300 IN A 172.66.147.243\n", }, "\n") if got := buf.String(); got != want { t.Errorf("output does not match the reference capture format\n--- got ---\n%s\n--- want ---\n%s", got, want) } }