tsdns: dual resolution mode, IPv6 support (#526)
This change adds to tsdns the ability to delegate lookups to upstream nameservers. This is crucial for setting Magic DNS as the system resolver. Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:
committed by
GitHub
parent
ce1b52bb71
commit
67ebba90e1
+168
-92
@@ -6,113 +6,173 @@ package tsdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/packet"
|
||||
)
|
||||
|
||||
var test2bytes = [16]byte{
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f,
|
||||
}
|
||||
|
||||
var dnsMap = &Map{
|
||||
domainToIP: map[string]netaddr.IP{
|
||||
"test1.ipn.dev": netaddr.IPv4(1, 2, 3, 4),
|
||||
"test2.ipn.dev": netaddr.IPv4(5, 6, 7, 8),
|
||||
"test2.ipn.dev": netaddr.IPv6Raw(test2bytes),
|
||||
},
|
||||
}
|
||||
|
||||
func dnspacket(srcip, dstip packet.IP, domain string, tp dns.Type, response bool) *packet.ParsedPacket {
|
||||
dnsHeader := dns.Header{Response: response}
|
||||
func dnspacket(domain string, tp dns.Type) []byte {
|
||||
var dnsHeader dns.Header
|
||||
question := dns.Question{
|
||||
Name: dns.MustNewName(domain),
|
||||
Type: tp,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
udpHeader := &packet.UDPHeader{
|
||||
IPHeader: packet.IPHeader{
|
||||
SrcIP: srcip,
|
||||
DstIP: dstip,
|
||||
IPProto: packet.UDP,
|
||||
},
|
||||
SrcPort: 1234,
|
||||
DstPort: 53,
|
||||
}
|
||||
|
||||
builder := dns.NewBuilder(nil, dnsHeader)
|
||||
builder.StartQuestions()
|
||||
builder.Question(question)
|
||||
payload, _ := builder.Finish()
|
||||
|
||||
buf := packet.Generate(udpHeader, payload)
|
||||
|
||||
pp := new(packet.ParsedPacket)
|
||||
pp.Decode(buf)
|
||||
|
||||
return pp
|
||||
return payload
|
||||
}
|
||||
|
||||
func TestAcceptsPacket(t *testing.T) {
|
||||
r := NewResolver(t.Logf)
|
||||
r.SetMap(dnsMap)
|
||||
func extractipcode(response []byte) (netaddr.IP, dns.RCode, error) {
|
||||
var ip netaddr.IP
|
||||
var parser dns.Parser
|
||||
|
||||
src := packet.IP(0x64656667) // 100.101.102.103
|
||||
dst := packet.IP(0x64646464) // 100.100.100.100
|
||||
tests := []struct {
|
||||
name string
|
||||
request *packet.ParsedPacket
|
||||
want bool
|
||||
}{
|
||||
{"valid", dnspacket(src, dst, "test1.ipn.dev.", dns.TypeA, false), true},
|
||||
{"invalid", dnspacket(dst, src, "test1.ipn.dev.", dns.TypeA, false), false},
|
||||
h, err := parser.Start(response)
|
||||
if err != nil {
|
||||
return ip, 0, err
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
accepts := r.AcceptsPacket(tt.request)
|
||||
if accepts != tt.want {
|
||||
t.Errorf("accepts = %v; want %v", accepts, tt.want)
|
||||
}
|
||||
})
|
||||
if !h.Response {
|
||||
return ip, 0, errors.New("not a response")
|
||||
}
|
||||
if h.RCode != dns.RCodeSuccess {
|
||||
return ip, h.RCode, nil
|
||||
}
|
||||
|
||||
err = parser.SkipAllQuestions()
|
||||
if err != nil {
|
||||
return ip, 0, err
|
||||
}
|
||||
|
||||
ah, err := parser.AnswerHeader()
|
||||
if err != nil {
|
||||
return ip, 0, err
|
||||
}
|
||||
switch ah.Type {
|
||||
case dns.TypeA:
|
||||
res, err := parser.AResource()
|
||||
if err != nil {
|
||||
return ip, 0, err
|
||||
}
|
||||
ip = netaddr.IPv4(res.A[0], res.A[1], res.A[2], res.A[3])
|
||||
case dns.TypeAAAA:
|
||||
res, err := parser.AAAAResource()
|
||||
if err != nil {
|
||||
return ip, 0, err
|
||||
}
|
||||
ip = netaddr.IPv6Raw(res.AAAA)
|
||||
default:
|
||||
return ip, 0, errors.New("type not in {A, AAAA}")
|
||||
}
|
||||
|
||||
return ip, h.RCode, nil
|
||||
}
|
||||
|
||||
func syncRespond(r *Resolver, query []byte) ([]byte, error) {
|
||||
request := Packet{Payload: query}
|
||||
r.EnqueueRequest(request)
|
||||
resp, err := r.NextResponse()
|
||||
return resp.Payload, err
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
r := NewResolver(t.Logf)
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetMap(dnsMap)
|
||||
r.Start()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
ip netaddr.IP
|
||||
code dns.RCode
|
||||
iserr bool
|
||||
}{
|
||||
{"valid", "test1.ipn.dev", netaddr.IPv4(1, 2, 3, 4), dns.RCodeSuccess, false},
|
||||
{"nxdomain", "test3.ipn.dev", netaddr.IP{}, dns.RCodeNameError, true},
|
||||
{"not our domain", "google.com", netaddr.IP{}, dns.RCodeRefused, true},
|
||||
{"ipv4", "test1.ipn.dev", netaddr.IPv4(1, 2, 3, 4), dns.RCodeSuccess},
|
||||
{"ipv6", "test2.ipn.dev", netaddr.IPv6Raw(test2bytes), dns.RCodeSuccess},
|
||||
{"nxdomain", "test3.ipn.dev", netaddr.IP{}, dns.RCodeNameError},
|
||||
{"foreign domain", "google.com", netaddr.IP{}, dns.RCodeNameError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, code, err := r.Resolve(tt.domain)
|
||||
if err != nil && !tt.iserr {
|
||||
if err != nil {
|
||||
t.Errorf("err = %v; want nil", err)
|
||||
} else if err == nil && tt.iserr {
|
||||
t.Errorf("err = nil; want non-nil")
|
||||
}
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
// Only check ip for non-err
|
||||
if !tt.iserr && ip != tt.ip {
|
||||
if ip != tt.ip {
|
||||
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentSet(t *testing.T) {
|
||||
r := NewResolver(t.Logf)
|
||||
func TestDelegate(t *testing.T) {
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetNameservers([]string{"9.9.9.9:53", "[2620:fe::fe]:53"})
|
||||
r.Start()
|
||||
|
||||
localhostv4, _ := netaddr.ParseIP("127.0.0.1")
|
||||
localhostv6, _ := netaddr.ParseIP("::1")
|
||||
tests := []struct {
|
||||
name string
|
||||
query []byte
|
||||
ip netaddr.IP
|
||||
code dns.RCode
|
||||
}{
|
||||
{"ipv4", dnspacket("localhost.", dns.TypeA), localhostv4, dns.RCodeSuccess},
|
||||
{"ipv6", dnspacket("localhost.", dns.TypeAAAA), localhostv6, dns.RCodeSuccess},
|
||||
{"nxdomain", dnspacket("invalid.invalid.", dns.TypeA), netaddr.IP{}, dns.RCodeNameError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp, err := syncRespond(r, tt.query)
|
||||
if err != nil {
|
||||
t.Errorf("err = %v; want nil", err)
|
||||
return
|
||||
}
|
||||
ip, code, err := extractipcode(resp)
|
||||
if err != nil {
|
||||
t.Errorf("extract: err = %v; want nil (in %x)", err, resp)
|
||||
return
|
||||
}
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if ip != tt.ip {
|
||||
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentSetMap(t *testing.T) {
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.Start()
|
||||
|
||||
// This is purely to ensure that Resolve does not race with SetMap.
|
||||
var wg sync.WaitGroup
|
||||
@@ -128,16 +188,26 @@ func TestConcurrentSet(t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var validResponse = []byte{
|
||||
// IP header
|
||||
0x45, 0x00, 0x00, 0x58, 0xff, 0xff, 0x00, 0x00, 0x40, 0x11, 0xe7, 0x00,
|
||||
// Source IP
|
||||
0x64, 0x64, 0x64, 0x64,
|
||||
// Destination IP
|
||||
0x64, 0x65, 0x66, 0x67,
|
||||
// UDP header
|
||||
0x00, 0x35, 0x04, 0xd2, 0x00, 0x44, 0x53, 0xdd,
|
||||
// DNS payload
|
||||
func TestConcurrentSetNameservers(t *testing.T) {
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.Start()
|
||||
packet := dnspacket("google.com.", dns.TypeA)
|
||||
|
||||
// This is purely to ensure that delegation does not race with SetNameservers.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
r.SetNameservers([]string{"9.9.9.9:53"})
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
syncRespond(r, packet)
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var validIPv4Response = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
@@ -154,16 +224,25 @@ var validResponse = []byte{
|
||||
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
||||
}
|
||||
|
||||
var validIPv6Response = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
0x00, 0x01, // one answer
|
||||
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
||||
// Question:
|
||||
0x05, 0x74, 0x65, 0x73, 0x74, 0x32, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
||||
0x00, 0x1c, 0x00, 0x01, // type AAAA, class IN
|
||||
// Answer:
|
||||
0x05, 0x74, 0x65, 0x73, 0x74, 0x32, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
||||
0x00, 0x1c, 0x00, 0x01, // type AAAA, class IN
|
||||
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
||||
0x00, 0x10, // length: 16 bytes
|
||||
// AAAA: 0001:0203:0405:0607:0809:0A0B:0C0D:0E0F
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xb, 0xc, 0xd, 0xe, 0xf,
|
||||
}
|
||||
|
||||
var nxdomainResponse = []byte{
|
||||
// IP header
|
||||
0x45, 0x00, 0x00, 0x3b, 0xff, 0xff, 0x00, 0x00, 0x40, 0x11, 0xe7, 0x1d,
|
||||
// Source IP
|
||||
0x64, 0x64, 0x64, 0x64,
|
||||
// Destination IP
|
||||
0x64, 0x65, 0x66, 0x67,
|
||||
// UDP header
|
||||
0x00, 0x35, 0x04, 0xd2, 0x00, 0x27, 0x25, 0x33,
|
||||
// DNS payload
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x03, // flags: response, authoritative, error: nxdomain
|
||||
0x00, 0x01, // one question
|
||||
@@ -175,25 +254,24 @@ var nxdomainResponse = []byte{
|
||||
}
|
||||
|
||||
func TestFull(t *testing.T) {
|
||||
r := NewResolver(t.Logf)
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetMap(dnsMap)
|
||||
r.Start()
|
||||
|
||||
src := packet.IP(0x64656667) // 100.101.102.103
|
||||
dst := packet.IP(0x64646464) // 100.100.100.100
|
||||
// One full packet and one error packet
|
||||
tests := []struct {
|
||||
name string
|
||||
request *packet.ParsedPacket
|
||||
request []byte
|
||||
response []byte
|
||||
}{
|
||||
{"valid", dnspacket(src, dst, "test1.ipn.dev.", dns.TypeA, false), validResponse},
|
||||
{"error", dnspacket(src, dst, "test3.ipn.dev.", dns.TypeA, false), nxdomainResponse},
|
||||
{"ipv4", dnspacket("test1.ipn.dev.", dns.TypeA), validIPv4Response},
|
||||
{"ipv6", dnspacket("test2.ipn.dev.", dns.TypeAAAA), validIPv6Response},
|
||||
{"error", dnspacket("test3.ipn.dev.", dns.TypeA), nxdomainResponse},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := make([]byte, 512)
|
||||
response, err := r.Respond(tt.request, buf)
|
||||
response, err := syncRespond(r, tt.request)
|
||||
if err != nil {
|
||||
t.Errorf("err = %v; want nil", err)
|
||||
}
|
||||
@@ -205,43 +283,41 @@ func TestFull(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAllocs(t *testing.T) {
|
||||
r := NewResolver(t.Logf)
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetMap(dnsMap)
|
||||
r.Start()
|
||||
|
||||
src := packet.IP(0x64656667) // 100.101.102.103
|
||||
dst := packet.IP(0x64646464) // 100.100.100.100
|
||||
query := dnspacket(src, dst, "test1.ipn.dev.", dns.TypeA, false)
|
||||
// It is seemingly pointless to test allocs in the delegate path,
|
||||
// as dialer.Dial -> Read -> Write alone comprise 12 allocs.
|
||||
query := dnspacket("test1.ipn.dev.", dns.TypeA)
|
||||
|
||||
buf := make([]byte, 512)
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
r.Respond(query, buf)
|
||||
syncRespond(r, query)
|
||||
})
|
||||
|
||||
if allocs > 0 {
|
||||
t.Errorf("allocs = %v; want 0", allocs)
|
||||
if allocs > 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFull(b *testing.B) {
|
||||
r := NewResolver(b.Logf)
|
||||
r := NewResolver(b.Logf, "ipn.dev")
|
||||
r.SetMap(dnsMap)
|
||||
r.Start()
|
||||
|
||||
src := packet.IP(0x64656667) // 100.101.102.103
|
||||
dst := packet.IP(0x64646464) // 100.100.100.100
|
||||
// One full packet and one error packet
|
||||
tests := []struct {
|
||||
name string
|
||||
request *packet.ParsedPacket
|
||||
request []byte
|
||||
}{
|
||||
{"valid", dnspacket(src, dst, "test1.ipn.dev.", dns.TypeA, false)},
|
||||
{"nxdomain", dnspacket(src, dst, "test3.ipn.dev.", dns.TypeA, false)},
|
||||
{"valid", dnspacket("test1.ipn.dev.", dns.TypeA)},
|
||||
{"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA)},
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Respond(tt.request, buf)
|
||||
syncRespond(r, tt.request)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user