package siit import ( "context" "fmt" "net" "reflect" "testing" "github.com/coredns/coredns/plugin/metrics" "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/request" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus/testutil" ) // TestToUnmappedAAAA reproduces the empty-RDATA-A-record bug: an AAAA // address that matches neither an EAM entry nor ipv6_prefix must not // produce an A record at all. func TestToUnmappedAAAA(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") eam := map[string]net.IP{} unrelated := net.ParseIP("2001:db8::1") a, mapped := to4(eam, prefix, unrelated) if mapped { t.Fatalf("expected no mapping for unrelated AAAA address, got %v", a) } } // TestToEamPrecedence reproduces the reversed-lookup-order bug: when an // address matches both ipv6_prefix and an explicit eam entry, the eam // entry must win. func TestToEamPrecedence(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") eamAddr := net.ParseIP("64:ff9b::192.0.2.1") eam := make(map[string]net.IP) eam[eamAddr.String()] = net.ParseIP("203.0.113.9") a, mapped := to4(eam, prefix, eamAddr) if !mapped { t.Fatalf("expected eam mapping to be found") } want := net.ParseIP("203.0.113.9").To4() if !a.Equal(want) { t.Errorf("expected eam-mapped address %v, got %v (algorithmic translation was used instead)", want, a) } } // TestToAlgorithmicFallback verifies RFC 6052 translation still applies // when no eam entry matches. func TestToAlgorithmicFallback(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") eam := map[string]net.IP{} addr := net.ParseIP("64:ff9b::192.0.2.42") a, mapped := to4(eam, prefix, addr) if !mapped { t.Fatalf("expected algorithmic translation to apply") } want := net.ParseIP("192.0.2.42").To4() if !a.Equal(want) { t.Errorf("expected %v, got %v", want, a) } } func TestSIIT(t *testing.T) { var cases = []struct { // a brief summary of the test case name string // the request req *dns.Msg // the initial response from the "downstream" server initResp *dns.Msg // A response to provide aResp *dns.Msg // the expected ultimate result resp *dns.Msg }{ { // no A record, yes AAAA record. Do SIIT name: "standard flow", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 70 IN SOA foo bar 1 1 1 1 1")}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), test.AAAA("example.com. 5000 IN AAAA 64:ff9b::192.0.2.43"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 192.0.2.42"), // override RR ttl to SOA ttl, since it's lower test.A("example.com. 70 IN A 192.0.2.43"), }, }, }, { // name exists, but has neither A nor AAAA record name: "aaaa empty", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, }, { // name exists, but AAAA record is not synthesized name: "aaaa empty", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 2001:db8::1"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, }, { // name exists, but AAAA records are a mix of synthesized and not name: "aaaa empty", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 2001:db8::1"), test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 192.0.2.42"), }, }, }, { // Query error other than NameError name: "non-nxdomain error", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ // failure MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeRefused, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), test.AAAA("example.com. 5000 IN AAAA 64:ff9b::192.0.2.43"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 192.0.2.42"), test.A("example.com. 600 IN A 192.0.2.43"), }, }, }, { // nxdomain (NameError): don't even try an AAAA request. name: "nxdomain", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ // failure MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeNameError, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeNameError, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, }, { // A record exists name: "A record", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 127.0.0.1"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 127.0.0.1"), }, }, }, { // no A records, AAAA record response truncated. name: "truncated AAAA response", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 70 IN SOA foo bar 1 1 1 1 1")}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Truncated: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), test.AAAA("example.com. 5000 IN AAAA 64:ff9b::192.0.2.43"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Truncated: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 192.0.2.42"), // override RR ttl to SOA ttl, since it's lower test.A("example.com. 70 IN A 192.0.2.43"), }, }, }, { // no A records, AAAA record response via eam. name: "eam AAAA response", req: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, RecursionDesired: true, Opcode: dns.OpcodeQuery, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 70 IN SOA foo bar 1 1 1 1 1")}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 43, Opcode: dns.OpcodeQuery, RecursionDesired: true, Truncated: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 64:dead::1"), test.AAAA("example.com. 5000 IN AAAA 64:dead::2"), }, }, resp: &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Truncated: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 10.0.0.1"), // override RR ttl to SOA ttl, since it's lower test.A("example.com. 70 IN A 10.0.0.2"), }, }, }, } _, pfx, _ := net.ParseCIDR("64:ff9b::/96") eam4 := make(map[string]net.IP) eam4["64:dead::1"] = net.ParseIP("10.0.0.1") eam4["64:dead::2"] = net.ParseIP("10.0.0.2") for idx, tc := range cases { t.Run(fmt.Sprintf("%d_%s", idx, tc.name), func(t *testing.T) { d := SIIT{ Next: &fakeHandler{t, tc.initResp}, IPv6Prefix: pfx, Eam4: eam4, Upstream: &fakeUpstream{t, tc.req.Question[0].Name, tc.aResp}, } rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) rc, err := d.ServeDNS(context.Background(), rec, tc.req) if err != nil { t.Fatal(err) } actual := rec.Msg if actual.Rcode != rc { t.Fatalf("ServeDNS should return real result code %q != %q", actual.Rcode, rc) } if !reflect.DeepEqual(actual, tc.resp) { t.Fatalf("Final answer should match expected %q != %q", actual, tc.resp) } }) } } type fakeHandler struct { t *testing.T reply *dns.Msg } func (fh *fakeHandler) ServeDNS(_ context.Context, w dns.ResponseWriter, _ *dns.Msg) (int, error) { if fh.reply == nil { panic("fakeHandler ServeDNS with nil reply") } w.WriteMsg(fh.reply) return fh.reply.Rcode, nil } func (fh *fakeHandler) Name() string { return "fake" } type fakeUpstream struct { t *testing.T qname string resp *dns.Msg } func (fu *fakeUpstream) Lookup(_ context.Context, _ request.Request, name string, typ uint16) (*dns.Msg, error) { if fu.qname == "" { fu.t.Fatalf("Unexpected A lookup for %s", name) } if name != fu.qname { fu.t.Fatalf("Wrong A lookup for %s, expected %s", name, fu.qname) } if typ != dns.TypeA && typ != dns.TypeAAAA { fu.t.Fatalf("Wrong lookup type %d, expected %d or %d", typ, dns.TypeA, dns.TypeAAAA) } return fu.resp, nil } func TestDoSIITNegativeResponse(t *testing.T) { origResponse := new(dns.Msg) origResponse.SetQuestion("example.org.", dns.TypeA) origResponse.Rcode = dns.RcodeSuccess // the client's original A answer aaaaFailure := new(dns.Msg) aaaaFailure.SetQuestion("example.org.", dns.TypeAAAA) // internal AAAA question aaaaFailure.Rcode = dns.RcodeServerFailure // upstream AAAA lookup SERVFAILs aaaaFailure.RecursionAvailable = true d := &SIIT{ Upstream: &fakeUpstream{ t: t, qname: "example.org.", resp: aaaaFailure, }, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) r.Id = 42 w := &test.ResponseWriter{} out, synthesized, err := d.DoSIIT(context.Background(), w, r, origResponse) if err != nil { t.Fatalf("unexpected error: %v", err) } if synthesized { t.Errorf("expected synthesized=false, no A record was actually produced") } if out == aaaaFailure { t.Fatalf("expected DoSIIT to build a new response addressed to the client, not return the internal AAAA lookup response as-is") } if out.Rcode != aaaaFailure.Rcode { t.Errorf("expected Rcode %v to be carried over from the lookup, got %v", aaaaFailure.Rcode, out.Rcode) } if out.Id != r.Id { t.Errorf("expected response ID %v to match the client's request, got %v", r.Id, out.Id) } if len(out.Question) != 1 || out.Question[0].Qtype != dns.TypeA || out.Question[0].Name != "example.org." { t.Fatalf("expected the original A question to be preserved in the response, got %+v", out.Question) } if !out.RecursionAvailable { t.Errorf("expected RecursionAvailable to be carried over from the lookup response") } } func TestDoSIITNXDOMAIN(t *testing.T) { origResponse := new(dns.Msg) origResponse.SetQuestion("example.org.", dns.TypeA) origResponse.Rcode = dns.RcodeSuccess soa := test.SOA("example.org. 3600 IN SOA foo bar 1 7200 900 1209600 86400") aaaaNX := new(dns.Msg) aaaaNX.SetQuestion("example.org.", dns.TypeAAAA) aaaaNX.Rcode = dns.RcodeNameError aaaaNX.Authoritative = true aaaaNX.Ns = []dns.RR{soa} d := &SIIT{ Upstream: &fakeUpstream{ t: t, qname: "example.org.", resp: aaaaNX, }, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) r.Id = 43 w := &test.ResponseWriter{} out, synthesized, err := d.DoSIIT(context.Background(), w, r, origResponse) if err != nil { t.Fatalf("unexpected error: %v", err) } if synthesized { t.Errorf("expected synthesized=false, no A record was actually produced") } if out == aaaaNX { t.Fatalf("expected DoSIIT to build a new response addressed to the client, not return the internal AAAA lookup response as-is") } if out.Rcode != aaaaNX.Rcode { t.Errorf("expected Rcode %v from lookup, got %v", aaaaNX.Rcode, out.Rcode) } if out.Id != r.Id { t.Errorf("expected response ID %v to match the client's request, got %v", r.Id, out.Id) } if len(out.Question) != 1 || out.Question[0].Qtype != dns.TypeA || out.Question[0].Name != "example.org." { t.Fatalf("expected the original A question to be preserved in the response, got %+v", out.Question) } if !out.Authoritative { t.Errorf("expected Authoritative to be carried over from the lookup response") } if !reflect.DeepEqual(out.Ns, aaaaNX.Ns) { t.Errorf("expected authority section (SOA) to be carried over, got %+v", out.Ns) } } // TestDoSIITNilResponse covers Upstream.Lookup returning (nil, nil): DoSIIT // must surface this as an error rather than pass a nil message on to a // caller that will dereference it. func TestDoSIITNilResponse(t *testing.T) { origResponse := new(dns.Msg) origResponse.SetQuestion("example.org.", dns.TypeA) origResponse.Rcode = dns.RcodeSuccess d := &SIIT{ Upstream: &fakeUpstream{ t: t, qname: "example.org.", resp: nil, // simulate a well-behaved Upstream returning (nil, nil) }, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) w := &test.ResponseWriter{} out, synthesized, err := d.DoSIIT(context.Background(), w, r, origResponse) if err == nil { t.Fatal("expected an error for a nil upstream response, got nil") } if synthesized { t.Errorf("expected synthesized=false, no A record was actually produced") } if out != nil { t.Errorf("expected a nil message alongside the error, got %+v", out) } } func TestServeDNSUpstreamSERVFAIL(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") initResp := &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, } aaaaFailure := new(dns.Msg) aaaaFailure.SetQuestion("example.org.", dns.TypeAAAA) aaaaFailure.Rcode = dns.RcodeServerFailure aaaaFailure.RecursionAvailable = true d := &SIIT{ Next: &fakeHandler{t, initResp}, IPv6Prefix: prefix, Upstream: &fakeUpstream{ t: t, qname: "example.org.", resp: aaaaFailure, }, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) r.Id = 42 r.RecursionDesired = true rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) rc, err := d.ServeDNS(context.Background(), rec, r) if err != nil { t.Fatalf("unexpected error: %v", err) } if rc != dns.RcodeServerFailure { t.Fatalf("expected SERVFAIL rcode, got %v", rc) } if rec.Msg == nil { t.Fatal("expected a response to be written") } if rec.Msg.Id != r.Id { t.Errorf("expected response ID %v to match the client's request, got %v", r.Id, rec.Msg.Id) } if len(rec.Msg.Question) != 1 || rec.Msg.Question[0].Qtype != dns.TypeA || rec.Msg.Question[0].Name != "example.org." { t.Fatalf("expected the original A question to be preserved in the response, got %+v", rec.Msg.Question) } if !rec.Msg.RecursionAvailable { t.Errorf("expected RecursionAvailable to be carried over from the internal AAAA lookup") } } func TestServeDNSUpstreamNXDOMAIN(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") initResp := &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 7, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, } soa := test.SOA("example.org. 3600 IN SOA foo bar 1 7200 900 1209600 86400") aaaaNX := new(dns.Msg) aaaaNX.SetQuestion("example.org.", dns.TypeAAAA) aaaaNX.Rcode = dns.RcodeNameError aaaaNX.Authoritative = true aaaaNX.Ns = []dns.RR{soa} d := &SIIT{ Next: &fakeHandler{t, initResp}, IPv6Prefix: prefix, Upstream: &fakeUpstream{ t: t, qname: "example.org.", resp: aaaaNX, }, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) r.Id = 7 r.RecursionDesired = true rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) rc, err := d.ServeDNS(context.Background(), rec, r) if err != nil { t.Fatalf("unexpected error: %v", err) } if rc != dns.RcodeNameError { t.Fatalf("expected NXDOMAIN rcode, got %v", rc) } if len(rec.Msg.Question) != 1 || rec.Msg.Question[0].Qtype != dns.TypeA { t.Fatalf("expected the original A question to be preserved, got %+v", rec.Msg.Question) } if !rec.Msg.Authoritative { t.Errorf("expected Authoritative to be carried over from the internal AAAA lookup") } if !reflect.DeepEqual(rec.Msg.Ns, aaaaNX.Ns) { t.Errorf("expected authority (SOA) to be carried over, got %+v", rec.Msg.Ns) } } func TestServeDNSUpstreamNilResponse(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") initResp := &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 99, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, } d := &SIIT{ Next: &fakeHandler{t, initResp}, IPv6Prefix: prefix, Upstream: &fakeUpstream{ t: t, qname: "example.org.", resp: nil, // simulate a well-behaved Upstream returning (nil, nil) }, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) r.Id = 99 r.RecursionDesired = true rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) rc, err := d.ServeDNS(context.Background(), rec, r) if err == nil { t.Fatal("expected an error from ServeDNS for a nil upstream response") } if rc != dns.RcodeServerFailure { t.Fatalf("expected SERVFAIL rcode, got %v", rc) } } // TestSynthesizePreservesAuthoritativeAndRecursive verifies RFC 6147 §5.4: // the synthesized response's AA/RA flags must reflect the secondary AAAA // response (where the data actually came from), not be unconditionally // cleared by SetReply. func TestSynthesizePreservesAuthoritativeAndRecursive(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") cases := []struct { name string authoritative bool recursive bool }{ {"authoritative only", true, false}, {"recursive only", false, true}, {"both", true, true}, {"neither", false, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { d := &SIIT{IPv6Prefix: prefix} origReq := new(dns.Msg) origReq.SetQuestion("example.org.", dns.TypeA) origReq.Id = 55 origResponse := new(dns.Msg) origResponse.SetReply(origReq) resp := new(dns.Msg) resp.SetQuestion("example.org.", dns.TypeAAAA) resp.Rcode = dns.RcodeSuccess resp.Authoritative = tc.authoritative resp.RecursionAvailable = tc.recursive resp.AuthenticatedData = true // must NOT survive synthesis resp.Answer = []dns.RR{ test.AAAA("example.org. 60 IN AAAA 64:ff9b::192.0.2.1"), } out, _ := d.Synthesize(origReq, origResponse, resp) if out.Authoritative != tc.authoritative { t.Errorf("Authoritative: expected %v, got %v", tc.authoritative, out.Authoritative) } if out.RecursionAvailable != tc.recursive { t.Errorf("RecursionAvailable: expected %v, got %v", tc.recursive, out.RecursionAvailable) } if out.AuthenticatedData { t.Errorf("expected AuthenticatedData to be cleared on a synthesized response, got true") } if out.Id != origReq.Id { t.Errorf("expected ID %v to be preserved from the original request, got %v", origReq.Id, out.Id) } if len(out.Question) != 1 || out.Question[0].Qtype != dns.TypeA { t.Fatalf("expected original A question to be preserved, got %+v", out.Question) } }) } } // TestServeDNSTruncatedInitialResponseNotSynthesized reproduces the // hidden-truncation bug: an initial A response with TC=1 and no visible // Answer must not be treated as NODATA and synthesized from a mapped AAAA // record. Doing so would return TC=0 to the client, who would then never // retry over TCP to recover the real A RR the original truncation hid. func TestServeDNSTruncatedInitialResponseNotSynthesized(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") initResp := &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 42, Opcode: dns.OpcodeQuery, RecursionDesired: true, Truncated: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, // no Answer -- looks like NODATA, but TC=1 means it's incomplete } d := &SIIT{ Next: &fakeHandler{t, initResp}, IPv6Prefix: prefix, // qname left empty: any Lookup call fails the test outright, since // a truncated initial response must short-circuit before ever // issuing the secondary AAAA lookup. Upstream: &fakeUpstream{t: t}, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) r.Id = 42 r.RecursionDesired = true rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) rc, err := d.ServeDNS(context.Background(), rec, r) if err != nil { t.Fatalf("unexpected error: %v", err) } if rc != dns.RcodeSuccess { t.Fatalf("unexpected rcode: %v", rc) } if rec.Msg == nil { t.Fatal("expected a response to be written") } if !rec.Msg.Truncated { t.Fatalf("expected TC=1 to be preserved so the client retries over TCP, got TC=%v", rec.Msg.Truncated) } if len(rec.Msg.Answer) != 0 { t.Fatalf("expected no synthesized answer for a truncated initial response, got %+v", rec.Msg.Answer) } } // TestServeDNSMetricCounting verifies RequestsTranslatedCount is // incremented exactly when a synthetic A record was actually produced, and // left untouched for the secondary-failure and unmapped-only cases that // previously overcounted. func TestServeDNSMetricCounting(t *testing.T) { _, prefix, _ := net.ParseCIDR("64:ff9b::/96") emptyAResp := &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: 1, Opcode: dns.OpcodeQuery, RecursionDesired: true, Rcode: dns.RcodeSuccess, Response: true, }, Question: []dns.Question{{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, } cases := []struct { name string aaaaResp *dns.Msg wantIncrease float64 }{ { name: "successful synthesis increments", aaaaResp: func() *dns.Msg { m := new(dns.Msg) m.SetQuestion("example.org.", dns.TypeAAAA) m.Rcode = dns.RcodeSuccess m.Answer = []dns.RR{test.AAAA("example.org. 60 IN AAAA 64:ff9b::192.0.2.1")} return m }(), wantIncrease: 1, }, { name: "secondary SERVFAIL does not increment", aaaaResp: func() *dns.Msg { m := new(dns.Msg) m.SetQuestion("example.org.", dns.TypeAAAA) m.Rcode = dns.RcodeServerFailure return m }(), wantIncrease: 0, }, { name: "unmapped-only AAAA does not increment", aaaaResp: func() *dns.Msg { m := new(dns.Msg) m.SetQuestion("example.org.", dns.TypeAAAA) m.Rcode = dns.RcodeSuccess m.Answer = []dns.RR{test.AAAA("example.org. 60 IN AAAA 2001:db8::1")} // outside prefix, no eam return m }(), wantIncrease: 0, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { d := &SIIT{ Next: &fakeHandler{t, emptyAResp}, IPv6Prefix: prefix, Upstream: &fakeUpstream{t, "example.org.", tc.aaaaResp}, } r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) ctx := context.Background() label := metrics.WithServer(ctx) before := testutil.ToFloat64(RequestsTranslatedCount.WithLabelValues(label)) rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) if _, err := d.ServeDNS(ctx, rec, r); err != nil { t.Fatalf("unexpected error: %v", err) } after := testutil.ToFloat64(RequestsTranslatedCount.WithLabelValues(label)) if got := after - before; got != tc.wantIncrease { t.Errorf("expected counter to increase by %v, got %v (before=%v after=%v)", tc.wantIncrease, got, before, after) } }) } } // embedIPv4 mirrors the RFC 6052 §2.2 embedding algorithm — the exact // mirror image of extractIPv4 — so these tests can construct known-good // NAT64 addresses independently of production code. func embedIPv4(prefix net.IP, prefixLen int, v4 net.IP) net.IP { v4 = v4.To4() out := make([]byte, 16) copy(out, prefix.To16()[:prefixLen/8]) i, j := prefixLen/8, 0 for ; i < 8; i, j = i+1, j+1 { out[i] = v4[j] } if i == 8 { i++ // reserved "u" byte stays zero } for ; j < 4; i, j = i+1, j+1 { out[i] = v4[j] } return net.IP(out) } // TestExtractIPv4RFC6052Vectors uses the six literal example addresses from // RFC 6052 §2.2 Table 1 ("Text Representation of IPv4-Embedded IPv6 // Addresses Using Network-Specific Prefixes"), rather than round-tripping // through embedIPv4. embedIPv4 mirrors the same byte-placement algorithm as // extractIPv4, so a shared offset bug in both helpers could still round-trip // successfully and this test would never catch it. These fixtures are // copied verbatim from the RFC, independent of any code in this package. func TestExtractIPv4RFC6052Vectors(t *testing.T) { want := net.ParseIP("192.0.2.33").To4() cases := []struct { prefixCIDR string // Network-Specific Prefix column addr string // IPv4-embedded IPv6 address column }{ {"2001:db8::/32", "2001:db8:c000:221::"}, {"2001:db8:100::/40", "2001:db8:1c0:2:21::"}, {"2001:db8:122::/48", "2001:db8:122:c000:2:2100::"}, {"2001:db8:122:300::/56", "2001:db8:122:3c0:0:221::"}, {"2001:db8:122:344::/64", "2001:db8:122:344:c0:2:2100::"}, {"2001:db8:122:344::/96", "2001:db8:122:344::192.0.2.33"}, } for _, tc := range cases { t.Run(tc.prefixCIDR, func(t *testing.T) { _, prefix, err := net.ParseCIDR(tc.prefixCIDR) if err != nil { t.Fatalf("bad prefix fixture %q: %v", tc.prefixCIDR, err) } addr := net.ParseIP(tc.addr) if addr == nil { t.Fatalf("bad address fixture %q", tc.addr) } got := extractIPv4(addr, prefix) if !got.Equal(want) { t.Errorf("prefix %s, address %s: expected %v, got %v", tc.prefixCIDR, tc.addr, want, got) } }) } } // TestToRejectsNonzeroUOctet: an address // whose reserved "u" byte (byte 8) is nonzero must not be algorithmically // translated, even though it falls within the configured prefix. func TestToRejectsNonzeroUOctet(t *testing.T) { _, prefix, _ := net.ParseCIDR("2001:db8:122:344::/64") eam := map[string]net.IP{} // bits 64-71 (byte 8) = 0xff, nonzero -- must be rejected addr := net.ParseIP("2001:db8:122:344:ffc0:2:2100:0") a, mapped := to4(eam, prefix, addr) if mapped { t.Fatalf("expected nonzero-u address to be rejected, got mapped address %v", a) } } // TestToAcceptsZeroUOctet is the counterpart: a well-formed address with // u == 0 for the same /64 prefix must still translate correctly. func TestToAcceptsZeroUOctet(t *testing.T) { _, prefix, _ := net.ParseCIDR("2001:db8:122:344::/64") eam := map[string]net.IP{} v4 := net.ParseIP("192.0.2.33").To4() addr := embedIPv4(net.ParseIP("2001:db8:122:344::"), 64, v4) a, mapped := to4(eam, prefix, addr) if !mapped { t.Fatalf("expected zero-u address to translate") } if !a.Equal(v4) { t.Errorf("expected %v, got %v", v4, a) } }