mirror of
https://github.com/coredns/coredns.git
synced 2026-08-27 15:17:06 -04:00
* feat(siit): Initial version Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * cleaner readme Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * linting and generating Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * improving README and removing a useless case Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * improvements Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * improvements Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * linting Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * New fixes Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * improvements Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> * improvements Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com> --------- Signed-off-by: Michée Lengronne <michee.lengronne@coppint.com>
1165 lines
35 KiB
Go
1165 lines
35 KiB
Go
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)
|
|
}
|
|
}
|