package traverse import ( "testing" "github.com/miekg/dns" ) // rootedCache returns a cache seeded with root hints, as the traverser will // always provide. func rootedCache() *InfoCache { c := NewInfoCache(nil) c.AddHints("", []StartServer{{Name: "a.root-servers.net", IPs: []string{"198.41.0.4"}}}) return c } func TestServerResponseReferralNotLame(t *testing.T) { // Root server refers com query to the gtld servers: "" → "com" is deeper. msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Ns = append(msg.Ns, nsRR("com", "a.gtld-servers.net")) msg.Extra = append(msg.Extra, aRR("a.gtld-servers.net", "192.5.6.30")) dq := decode(msg, "www.example.com", dns.TypeA, "") r, err := NewServerResponse(dq, "a.root-servers.net", "", rootedCache()) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if r.Status != StatusReferral { t.Fatalf("status = %s, want referral", r.Status) } if r.StartersBailiwick != "com" { t.Errorf("starters bailiwick = %q, want com", r.StartersBailiwick) } if len(r.Starters) != 1 || r.Starters[0].Name != "a.gtld-servers.net" { t.Errorf("starters = %v", r.Starters) } if len(r.Starters[0].IPs) != 1 || r.Starters[0].IPs[0] != "192.5.6.30" { t.Errorf("starter IPs = %v", r.Starters[0].IPs) } if len(dq.Warnings) != 0 { t.Errorf("unexpected warnings: %v", dq.Warnings) } } func TestServerResponseGluelessStarterHasNilIPs(t *testing.T) { msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Ns = append(msg.Ns, nsRR("example.com", "ns1.example.com")) dq := decode(msg, "www.example.com", dns.TypeA, "com") r, err := NewServerResponse(dq, "a.gtld-servers.net", "", rootedCache()) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if r.Status != StatusReferral { t.Fatalf("status = %s, want referral", r.Status) } if r.Starters[0].IPs != nil { t.Errorf("glueless starter should have nil IPs, got %v", r.Starters[0].IPs) } } func TestServerResponseLameReferral(t *testing.T) { // A com server "refers" us to an example.org zone: the NS records are // out-of-bailiwick so they are discarded, the cache walk falls back to // the root NS, and "" is not strictly deeper than "com" → lame. msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Ns = append(msg.Ns, nsRR("example.org", "ns1.example.org")) dq := decode(msg, "www.example.com", dns.TypeA, "com") if dq.Status != StatusReferral { t.Fatalf("decoded status = %s, want referral", dq.Status) } r, err := NewServerResponse(dq, "a.gtld-servers.net", "192.5.6.30", rootedCache()) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if r.Status != StatusReferralLame { t.Fatalf("status = %s, want referral_lame", r.Status) } if r.StartersBailiwick != "" { t.Errorf("starters bailiwick = %q, want \"\" (root fallback)", r.StartersBailiwick) } found := false for _, w := range dq.Warnings { if w == "Referred authority names do not match query cache expectations" { found = true } } if !found { t.Errorf("expected mismatch warning, got %v", dq.Warnings) } want := "key:referral_lame:192.0.2.1:a.gtld-servers.net:www.example.com:IN:A:192.5.6.30" if got := r.StatsKey(); got != want { t.Errorf("stats key = %q, want %q", got, want) } } func TestServerResponseEqualZoneReferralIsLame(t *testing.T) { // Referral back into the SAME zone (com → com) is lame: not strictly deeper. msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Ns = append(msg.Ns, nsRR("com", "b.gtld-servers.net")) dq := decode(msg, "www.example.com", dns.TypeA, "com") r, err := NewServerResponse(dq, "a.gtld-servers.net", "192.5.6.30", rootedCache()) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if r.Status != StatusReferralLame { t.Fatalf("status = %s, want referral_lame", r.Status) } } func TestServerResponseRestartStarters(t *testing.T) { // A CNAME out of the bailiwick restarts; starters come from the deepest // cached zone for the new target (root here). msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Answer = append(msg.Answer, cnameRR("www.example.com", "cdn.example.org")) dq := decode(msg, "www.example.com", dns.TypeA, "example.com") if dq.Status != StatusRestart { t.Fatalf("decoded status = %s, want restart", dq.Status) } parent := rootedCache() parent.Add([]dns.RR{nsRR("example.org", "ns1.example.org"), aRR("ns1.example.org", "9.9.9.9")}) r, err := NewServerResponse(dq, "ns1.example.com", "", parent) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if r.Status != StatusRestart { t.Fatalf("status = %s, want restart", r.Status) } if r.StartersBailiwick != "example.org" { t.Errorf("starters bailiwick = %q, want example.org", r.StartersBailiwick) } if len(r.Starters) != 1 || r.Starters[0].Name != "ns1.example.org" { t.Errorf("starters = %v", r.Starters) } } func TestServerResponseCachesGoodRecordsInChildCache(t *testing.T) { parent := rootedCache() msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Ns = append(msg.Ns, nsRR("example.com", "ns1.example.com")) msg.Extra = append(msg.Extra, aRR("ns1.example.com", "1.2.3.4"), aRR("ns1.example.org", "5.6.7.8"), // out of bailiwick — discarded ) dq := decode(msg, "www.example.com", dns.TypeA, "com") r, err := NewServerResponse(dq, "a.gtld-servers.net", "", parent) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if got := r.Cache.Get("ns1.example.com", dns.ClassINET, dns.TypeA); len(got) != 1 { t.Errorf("in-bailiwick glue should be cached, got %v", got) } if got := r.Cache.Get("ns1.example.org", dns.ClassINET, dns.TypeA); got != nil { t.Errorf("out-of-bailiwick record must be discarded, got %v", got) } // The parent cache stays clean — records live in the response's child. if got := parent.Get("example.com", dns.ClassINET, dns.TypeNS); got != nil { t.Errorf("parent cache polluted: %v", got) } } func TestServerResponseExceptionDoesNotCache(t *testing.T) { dq := NewDecodedQuery(nil, errTimeout{}, "www.example.com", dns.ClassINET, dns.TypeA, "192.0.2.1", "com") r, err := NewServerResponse(dq, "a.gtld-servers.net", "", rootedCache()) if err != nil { t.Fatalf("NewServerResponse: %v", err) } if r.Status != StatusException { t.Fatalf("status = %s, want exception", r.Status) } want := "key:exception:192.0.2.1:a.gtld-servers.net:www.example.com:IN:A:query timed out" if got := r.StatsKey(); got != want { t.Errorf("stats key = %q, want %q", got, want) } } type errTimeout struct{} func (errTimeout) Error() string { return "query timed out" } func TestServerResponseAnsweredStatsKey(t *testing.T) { msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Answer = append(msg.Answer, aRR("www.example.com", "93.184.216.34")) dq := decode(msg, "www.example.com", dns.TypeA, "example.com") r, err := NewServerResponse(dq, "NS1.Example.Com", "", rootedCache()) if err != nil { t.Fatalf("NewServerResponse: %v", err) } want := "key:answered:192.0.2.1:ns1.example.com:www.example.com:IN:A" if got := r.StatsKey(); got != want { t.Errorf("stats key = %q, want %q", got, want) } } func TestNoGlueResponse(t *testing.T) { r := NewNoGlueResponse("www.example.com", dns.ClassINET, dns.TypeA, "192.5.6.30", "ns1.example.com", "example.com") if r.Status != StatusNoGlue { t.Fatalf("status = %s, want noglue", r.Status) } // NoGlue/Loop use their own field order: ip, qname, qclass, qtype, server, bailiwick. want := "key:noglue:192.5.6.30:www.example.com:IN:A:ns1.example.com:example.com" if got := r.StatsKey(); got != want { t.Errorf("stats key = %q, want %q", got, want) } } func TestLoopResponse(t *testing.T) { r := NewLoopResponse("www.example.com", dns.ClassINET, dns.TypeA, "192.5.6.30", "ns1.example.com", "example.com") if r.Status != StatusLoop { t.Fatalf("status = %s, want loop", r.Status) } want := "key:loop:192.5.6.30:www.example.com:IN:A:ns1.example.com:example.com" if got := r.StatsKey(); got != want { t.Errorf("stats key = %q, want %q", got, want) } } func TestServerResponseReferralNoRootHintsErrors(t *testing.T) { // A lame referral with a completely empty cache chain cannot compute // starters; the constructor surfaces the "no root hints" error. msg := newMsg("www.example.com", dns.TypeA, dns.RcodeSuccess) msg.Ns = append(msg.Ns, nsRR("example.org", "ns1.example.org")) dq := decode(msg, "www.example.com", dns.TypeA, "com") if _, err := NewServerResponse(dq, "a.gtld-servers.net", "", NewInfoCache(nil)); err == nil { t.Fatal("expected error when no NS reachable in cache chain") } }