mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 03:25:35 +00:00
8b11937eaf
This raises the maximum DNS response message size from 512 to 4095. This should be large enough for almost all situations that do not need TCP. We still do not recognize EDNS, so we will still forward requests that claim support for a larger response size than 4095 (that will be solved later). For now, when a response comes back that is too large to fit in our receive buffer, we now set the truncation flag in the DNS header, which is an improvement from before but will prompt attempts to use TCP which isn't supported yet. On Windows, WSARecvFrom into a buffer that's too small returns an error in addition to the data. On other OSes, the extra data is silently discarded. In this case, we prefer the latter so need to catch the error on Windows. Partially addresses #1123 Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
871 lines
24 KiB
Go
871 lines
24 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package resolver
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"math/rand"
|
|
"net"
|
|
"testing"
|
|
|
|
dns "golang.org/x/net/dns/dnsmessage"
|
|
"inet.af/netaddr"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/util/dnsname"
|
|
)
|
|
|
|
var testipv4 = netaddr.MustParseIP("1.2.3.4")
|
|
var testipv6 = netaddr.MustParseIP("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f")
|
|
|
|
var dnsCfg = Config{
|
|
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
|
"test1.ipn.dev.": []netaddr.IP{testipv4},
|
|
"test2.ipn.dev.": []netaddr.IP{testipv6},
|
|
},
|
|
LocalDomains: []dnsname.FQDN{"ipn.dev."},
|
|
}
|
|
|
|
func dnspacket(domain dnsname.FQDN, tp dns.Type) []byte {
|
|
var dnsHeader dns.Header
|
|
question := dns.Question{
|
|
Name: dns.MustNewName(domain.WithTrailingDot()),
|
|
Type: tp,
|
|
Class: dns.ClassINET,
|
|
}
|
|
|
|
builder := dns.NewBuilder(nil, dnsHeader)
|
|
builder.StartQuestions()
|
|
builder.Question(question)
|
|
payload, _ := builder.Finish()
|
|
|
|
return payload
|
|
}
|
|
|
|
type dnsResponse struct {
|
|
ip netaddr.IP
|
|
txt []string
|
|
name dnsname.FQDN
|
|
rcode dns.RCode
|
|
truncated bool
|
|
}
|
|
|
|
func unpackResponse(payload []byte) (dnsResponse, error) {
|
|
var response dnsResponse
|
|
var parser dns.Parser
|
|
|
|
h, err := parser.Start(payload)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
if !h.Response {
|
|
return response, errors.New("not a response")
|
|
}
|
|
|
|
response.rcode = h.RCode
|
|
if response.rcode != dns.RCodeSuccess {
|
|
return response, nil
|
|
}
|
|
|
|
response.truncated = h.Truncated
|
|
if response.truncated {
|
|
// TODO(#2067): Ideally, answer processing should still succeed when
|
|
// dealing with a truncated message, but currently when we truncate
|
|
// a packet, it's caused by the buffer being too small and usually that
|
|
// means the data runs out mid-record. dns.Parser does not like it when
|
|
// that happens. We can improve this by trimming off incomplete records.
|
|
return response, nil
|
|
}
|
|
|
|
err = parser.SkipAllQuestions()
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
ah, err := parser.AnswerHeader()
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
switch ah.Type {
|
|
case dns.TypeA:
|
|
res, err := parser.AResource()
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
response.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 response, err
|
|
}
|
|
response.ip = netaddr.IPv6Raw(res.AAAA)
|
|
case dns.TypeTXT:
|
|
res, err := parser.TXTResource()
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
response.txt = res.TXT
|
|
case dns.TypeNS:
|
|
res, err := parser.NSResource()
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
response.name, err = dnsname.ToFQDN(res.NS.String())
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
default:
|
|
return response, errors.New("type not in {A, AAAA, NS}")
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func syncRespond(r *Resolver, query []byte) ([]byte, error) {
|
|
r.EnqueueRequest(query, netaddr.IPPort{})
|
|
payload, _, err := r.NextResponse()
|
|
return payload, err
|
|
}
|
|
|
|
func mustIP(str string) netaddr.IP {
|
|
ip, err := netaddr.ParseIP(str)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ip
|
|
}
|
|
|
|
func TestRDNSNameToIPv4(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input dnsname.FQDN
|
|
wantIP netaddr.IP
|
|
wantOK bool
|
|
}{
|
|
{"valid", "4.123.24.1.in-addr.arpa.", netaddr.IPv4(1, 24, 123, 4), true},
|
|
{"double_dot", "1..2.3.in-addr.arpa.", netaddr.IP{}, false},
|
|
{"overflow", "1.256.3.4.in-addr.arpa.", netaddr.IP{}, false},
|
|
{"not_ip", "sub.do.ma.in.in-addr.arpa.", netaddr.IP{}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ip, ok := rdnsNameToIPv4(tt.input)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("ok = %v; want %v", ok, tt.wantOK)
|
|
} else if ok && ip != tt.wantIP {
|
|
t.Errorf("ip = %v; want %v", ip, tt.wantIP)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRDNSNameToIPv6(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input dnsname.FQDN
|
|
wantIP netaddr.IP
|
|
wantOK bool
|
|
}{
|
|
{
|
|
"valid",
|
|
"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
|
|
mustIP("2001:db8::567:89ab"),
|
|
true,
|
|
},
|
|
{
|
|
"double_dot",
|
|
"b..9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
|
|
netaddr.IP{},
|
|
false,
|
|
},
|
|
{
|
|
"double_hex",
|
|
"b.a.98.0.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
|
|
netaddr.IP{},
|
|
false,
|
|
},
|
|
{
|
|
"not_hex",
|
|
"b.a.g.0.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
|
|
netaddr.IP{},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ip, ok := rdnsNameToIPv6(tt.input)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("ok = %v; want %v", ok, tt.wantOK)
|
|
} else if ok && ip != tt.wantIP {
|
|
t.Errorf("ip = %v; want %v", ip, tt.wantIP)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveLocal(t *testing.T) {
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
|
|
r.SetConfig(dnsCfg)
|
|
|
|
tests := []struct {
|
|
name string
|
|
qname dnsname.FQDN
|
|
qtype dns.Type
|
|
ip netaddr.IP
|
|
code dns.RCode
|
|
}{
|
|
{"ipv4", "test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess},
|
|
{"ipv6", "test2.ipn.dev.", dns.TypeAAAA, testipv6, dns.RCodeSuccess},
|
|
{"no-ipv6", "test1.ipn.dev.", dns.TypeAAAA, netaddr.IP{}, dns.RCodeSuccess},
|
|
{"nxdomain", "test3.ipn.dev.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError},
|
|
{"foreign domain", "google.com.", dns.TypeA, netaddr.IP{}, dns.RCodeRefused},
|
|
{"all", "test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess},
|
|
{"mx-ipv4", "test1.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeSuccess},
|
|
{"mx-ipv6", "test2.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeSuccess},
|
|
{"mx-nxdomain", "test3.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeNameError},
|
|
{"ns-nxdomain", "test3.ipn.dev.", dns.TypeNS, netaddr.IP{}, dns.RCodeNameError},
|
|
{"onion-domain", "footest.onion.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ip, code := r.resolveLocal(tt.qname, tt.qtype)
|
|
if code != tt.code {
|
|
t.Errorf("code = %v; want %v", code, tt.code)
|
|
}
|
|
// Only check ip for non-err
|
|
if ip != tt.ip {
|
|
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveLocalReverse(t *testing.T) {
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
|
|
r.SetConfig(dnsCfg)
|
|
|
|
tests := []struct {
|
|
name string
|
|
ip netaddr.IP
|
|
want dnsname.FQDN
|
|
code dns.RCode
|
|
}{
|
|
{"ipv4", testipv4, "test1.ipn.dev.", dns.RCodeSuccess},
|
|
{"ipv6", testipv6, "test2.ipn.dev.", dns.RCodeSuccess},
|
|
{"nxdomain", netaddr.IPv4(4, 3, 2, 1), "", dns.RCodeNameError},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
name, code := r.resolveLocalReverse(tt.ip)
|
|
if code != tt.code {
|
|
t.Errorf("code = %v; want %v", code, tt.code)
|
|
}
|
|
if name != tt.want {
|
|
t.Errorf("ip = %v; want %v", name, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func ipv6Works() bool {
|
|
c, err := net.Listen("tcp", "[::1]:0")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
c.Close()
|
|
return true
|
|
}
|
|
|
|
func generateTXT(size int, source rand.Source) []string {
|
|
const sizePerTXT = 120
|
|
|
|
if size%2 != 0 {
|
|
panic("even lengths only")
|
|
}
|
|
|
|
rng := rand.New(source)
|
|
|
|
txts := make([]string, 0, size/sizePerTXT+1)
|
|
|
|
raw := make([]byte, sizePerTXT/2)
|
|
|
|
rem := size
|
|
for ; rem > sizePerTXT; rem -= sizePerTXT {
|
|
rng.Read(raw)
|
|
txts = append(txts, hex.EncodeToString(raw))
|
|
}
|
|
if rem > 0 {
|
|
rng.Read(raw[:rem/2])
|
|
txts = append(txts, hex.EncodeToString(raw[:rem/2]))
|
|
}
|
|
|
|
return txts
|
|
}
|
|
|
|
func TestDelegate(t *testing.T) {
|
|
tstest.ResourceCheck(t)
|
|
|
|
if !ipv6Works() {
|
|
t.Skip("skipping test that requires localhost IPv6")
|
|
}
|
|
|
|
randSource := rand.NewSource(4)
|
|
|
|
// smallTXT does not require EDNS
|
|
smallTXT := generateTXT(300, randSource)
|
|
|
|
// medTXT and largeTXT are responses that require EDNS but we would like to
|
|
// support these sizes of response without truncation because they are
|
|
// moderately common.
|
|
medTXT := generateTXT(1200, randSource)
|
|
largeTXT := generateTXT(4000, randSource)
|
|
|
|
// xlargeTXT is slightly above the maximum response size that we support,
|
|
// so there should be truncation.
|
|
xlargeTXT := generateTXT(5000, randSource)
|
|
|
|
// hugeTXT is significantly larger than any typical MTU and will require
|
|
// significant fragmentation. For buffer management reasons, we do not
|
|
// intend to handle responses this large, so there should be truncation.
|
|
hugeTXT := generateTXT(64000, randSource)
|
|
|
|
v4server := serveDNS(t, "127.0.0.1:0",
|
|
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."),
|
|
"nxdomain.site.", resolveToNXDOMAIN,
|
|
"small.txt.", resolveToTXT(smallTXT),
|
|
"med.txt.", resolveToTXT(medTXT),
|
|
"large.txt.", resolveToTXT(largeTXT),
|
|
"xlarge.txt.", resolveToTXT(xlargeTXT),
|
|
"huge.txt.", resolveToTXT(hugeTXT))
|
|
defer v4server.Shutdown()
|
|
v6server := serveDNS(t, "[::1]:0",
|
|
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."),
|
|
"nxdomain.site.", resolveToNXDOMAIN,
|
|
"small.txt.", resolveToTXT(smallTXT),
|
|
"med.txt.", resolveToTXT(medTXT),
|
|
"large.txt.", resolveToTXT(largeTXT),
|
|
"xlarge.txt.", resolveToTXT(xlargeTXT),
|
|
"huge.txt.", resolveToTXT(hugeTXT))
|
|
defer v6server.Shutdown()
|
|
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
|
|
cfg := dnsCfg
|
|
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
|
|
".": {
|
|
netaddr.MustParseIPPort(v4server.PacketConn.LocalAddr().String()),
|
|
netaddr.MustParseIPPort(v6server.PacketConn.LocalAddr().String()),
|
|
},
|
|
}
|
|
r.SetConfig(cfg)
|
|
|
|
tests := []struct {
|
|
title string
|
|
query []byte
|
|
response dnsResponse
|
|
}{
|
|
{
|
|
"ipv4",
|
|
dnspacket("test.site.", dns.TypeA),
|
|
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"ipv6",
|
|
dnspacket("test.site.", dns.TypeAAAA),
|
|
dnsResponse{ip: testipv6, rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"ns",
|
|
dnspacket("test.site.", dns.TypeNS),
|
|
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"nxdomain",
|
|
dnspacket("nxdomain.site.", dns.TypeA),
|
|
dnsResponse{rcode: dns.RCodeNameError},
|
|
},
|
|
{
|
|
"smalltxt",
|
|
dnspacket("small.txt.", dns.TypeTXT),
|
|
dnsResponse{txt: smallTXT, rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"medtxt",
|
|
dnspacket("med.txt.", dns.TypeTXT),
|
|
dnsResponse{txt: medTXT, rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"largetxt",
|
|
dnspacket("large.txt.", dns.TypeTXT),
|
|
dnsResponse{txt: largeTXT, rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"xlargetxt",
|
|
dnspacket("xlarge.txt.", dns.TypeTXT),
|
|
dnsResponse{rcode: dns.RCodeSuccess, truncated: true},
|
|
},
|
|
{
|
|
"hugetxt",
|
|
dnspacket("huge.txt.", dns.TypeTXT),
|
|
dnsResponse{rcode: dns.RCodeSuccess, truncated: true},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.title, func(t *testing.T) {
|
|
payload, err := syncRespond(r, tt.query)
|
|
if err != nil {
|
|
t.Errorf("err = %v; want nil", err)
|
|
return
|
|
}
|
|
response, err := unpackResponse(payload)
|
|
if err != nil {
|
|
t.Errorf("extract: err = %v; want nil (in %x)", err, payload)
|
|
return
|
|
}
|
|
if response.rcode != tt.response.rcode {
|
|
t.Errorf("rcode = %v; want %v", response.rcode, tt.response.rcode)
|
|
}
|
|
if response.ip != tt.response.ip {
|
|
t.Errorf("ip = %v; want %v", response.ip, tt.response.ip)
|
|
}
|
|
if response.name != tt.response.name {
|
|
t.Errorf("name = %v; want %v", response.name, tt.response.name)
|
|
}
|
|
if len(response.txt) != len(tt.response.txt) {
|
|
t.Errorf("%v txt records, want %v txt records", len(response.txt), len(tt.response.txt))
|
|
} else {
|
|
for i := range response.txt {
|
|
if response.txt[i] != tt.response.txt[i] {
|
|
t.Errorf("txt record %v is %s, want %s", i, response.txt[i], tt.response.txt[i])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDelegateSplitRoute(t *testing.T) {
|
|
test4 := netaddr.MustParseIP("2.3.4.5")
|
|
test6 := netaddr.MustParseIP("ff::1")
|
|
|
|
server1 := serveDNS(t, "127.0.0.1:0",
|
|
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
|
defer server1.Shutdown()
|
|
server2 := serveDNS(t, "127.0.0.1:0",
|
|
"test.other.", resolveToIP(test4, test6, "dns.other."))
|
|
defer server2.Shutdown()
|
|
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
|
|
cfg := dnsCfg
|
|
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
|
|
".": {netaddr.MustParseIPPort(server1.PacketConn.LocalAddr().String())},
|
|
"other.": {netaddr.MustParseIPPort(server2.PacketConn.LocalAddr().String())},
|
|
}
|
|
r.SetConfig(cfg)
|
|
|
|
tests := []struct {
|
|
title string
|
|
query []byte
|
|
response dnsResponse
|
|
}{
|
|
{
|
|
"general",
|
|
dnspacket("test.site.", dns.TypeA),
|
|
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
|
|
},
|
|
{
|
|
"override",
|
|
dnspacket("test.other.", dns.TypeA),
|
|
dnsResponse{ip: test4, rcode: dns.RCodeSuccess},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.title, func(t *testing.T) {
|
|
payload, err := syncRespond(r, tt.query)
|
|
if err != nil {
|
|
t.Errorf("err = %v; want nil", err)
|
|
return
|
|
}
|
|
response, err := unpackResponse(payload)
|
|
if err != nil {
|
|
t.Errorf("extract: err = %v; want nil (in %x)", err, payload)
|
|
return
|
|
}
|
|
if response.rcode != tt.response.rcode {
|
|
t.Errorf("rcode = %v; want %v", response.rcode, tt.response.rcode)
|
|
}
|
|
if response.ip != tt.response.ip {
|
|
t.Errorf("ip = %v; want %v", response.ip, tt.response.ip)
|
|
}
|
|
if response.name != tt.response.name {
|
|
t.Errorf("name = %v; want %v", response.name, tt.response.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDelegateCollision(t *testing.T) {
|
|
server := serveDNS(t, "127.0.0.1:0",
|
|
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
|
defer server.Shutdown()
|
|
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
|
|
cfg := dnsCfg
|
|
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
|
|
".": {
|
|
netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()),
|
|
},
|
|
}
|
|
r.SetConfig(cfg)
|
|
|
|
packets := []struct {
|
|
qname dnsname.FQDN
|
|
qtype dns.Type
|
|
addr netaddr.IPPort
|
|
}{
|
|
{"test.site.", dns.TypeA, netaddr.IPPortFrom(netaddr.IPv4(1, 1, 1, 1), 1001)},
|
|
{"test.site.", dns.TypeAAAA, netaddr.IPPortFrom(netaddr.IPv4(1, 1, 1, 1), 1002)},
|
|
}
|
|
|
|
// packets will have the same dns txid.
|
|
for _, p := range packets {
|
|
payload := dnspacket(p.qname, p.qtype)
|
|
err := r.EnqueueRequest(payload, p.addr)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
// Despite the txid collision, the answer(s) should still match the query.
|
|
resp, addr, err := r.NextResponse()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
var p dns.Parser
|
|
_, err = p.Start(resp)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
err = p.SkipAllQuestions()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
ans, err := p.AllAnswers()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
var wantType dns.Type
|
|
switch ans[0].Body.(type) {
|
|
case *dns.AResource:
|
|
wantType = dns.TypeA
|
|
case *dns.AAAAResource:
|
|
wantType = dns.TypeAAAA
|
|
default:
|
|
t.Errorf("unexpected answer type: %T", ans[0].Body)
|
|
}
|
|
|
|
for _, p := range packets {
|
|
if p.qtype == wantType && p.addr != addr {
|
|
t.Errorf("addr = %v; want %v", addr, p.addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
var allResponse = []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, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
|
0x00, 0xff, 0x00, 0x01, // type ALL, class IN
|
|
// Answer:
|
|
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
|
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
|
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
|
0x00, 0x04, // length: 4 bytes
|
|
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
|
}
|
|
|
|
var ipv4Response = []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, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
|
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
|
// Answer:
|
|
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
|
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
|
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
|
0x00, 0x04, // length: 4 bytes
|
|
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
|
}
|
|
|
|
var ipv6Response = []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 ipv4UppercaseResponse = []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, 0x54, 0x45, 0x53, 0x54, 0x31, 0x03, 0x49, 0x50, 0x4e, 0x03, 0x44, 0x45, 0x56, 0x00, // name
|
|
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
|
// Answer:
|
|
0x05, 0x54, 0x45, 0x53, 0x54, 0x31, 0x03, 0x49, 0x50, 0x4e, 0x03, 0x44, 0x45, 0x56, 0x00, // name
|
|
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
|
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
|
0x00, 0x04, // length: 4 bytes
|
|
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
|
}
|
|
|
|
var ptrResponse = []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: 4.3.2.1.in-addr.arpa
|
|
0x01, 0x34, 0x01, 0x33, 0x01, 0x32, 0x01, 0x31, 0x07,
|
|
0x69, 0x6e, 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00,
|
|
0x00, 0x0c, 0x00, 0x01, // type PTR, class IN
|
|
// Answer: 4.3.2.1.in-addr.arpa
|
|
0x01, 0x34, 0x01, 0x33, 0x01, 0x32, 0x01, 0x31, 0x07,
|
|
0x69, 0x6e, 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00,
|
|
0x00, 0x0c, 0x00, 0x01, // type PTR, class IN
|
|
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
|
0x00, 0x0f, // length: 15 bytes
|
|
// PTR: test1.ipn.dev
|
|
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00,
|
|
}
|
|
|
|
var ptrResponse6 = []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: f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa
|
|
0x01, 0x66, 0x01, 0x30, 0x01, 0x65, 0x01, 0x30,
|
|
0x01, 0x64, 0x01, 0x30, 0x01, 0x63, 0x01, 0x30,
|
|
0x01, 0x62, 0x01, 0x30, 0x01, 0x61, 0x01, 0x30,
|
|
0x01, 0x39, 0x01, 0x30, 0x01, 0x38, 0x01, 0x30,
|
|
0x01, 0x37, 0x01, 0x30, 0x01, 0x36, 0x01, 0x30,
|
|
0x01, 0x35, 0x01, 0x30, 0x01, 0x34, 0x01, 0x30,
|
|
0x01, 0x33, 0x01, 0x30, 0x01, 0x32, 0x01, 0x30,
|
|
0x01, 0x31, 0x01, 0x30, 0x01, 0x30, 0x01, 0x30,
|
|
0x03, 0x69, 0x70, 0x36,
|
|
0x04, 0x61, 0x72, 0x70, 0x61, 0x00,
|
|
0x00, 0x0c, 0x00, 0x01, // type PTR, class IN6
|
|
// Answer: f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa
|
|
0x01, 0x66, 0x01, 0x30, 0x01, 0x65, 0x01, 0x30,
|
|
0x01, 0x64, 0x01, 0x30, 0x01, 0x63, 0x01, 0x30,
|
|
0x01, 0x62, 0x01, 0x30, 0x01, 0x61, 0x01, 0x30,
|
|
0x01, 0x39, 0x01, 0x30, 0x01, 0x38, 0x01, 0x30,
|
|
0x01, 0x37, 0x01, 0x30, 0x01, 0x36, 0x01, 0x30,
|
|
0x01, 0x35, 0x01, 0x30, 0x01, 0x34, 0x01, 0x30,
|
|
0x01, 0x33, 0x01, 0x30, 0x01, 0x32, 0x01, 0x30,
|
|
0x01, 0x31, 0x01, 0x30, 0x01, 0x30, 0x01, 0x30,
|
|
0x03, 0x69, 0x70, 0x36,
|
|
0x04, 0x61, 0x72, 0x70, 0x61, 0x00,
|
|
0x00, 0x0c, 0x00, 0x01, // type PTR, class IN
|
|
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
|
0x00, 0x0f, // length: 15 bytes
|
|
// PTR: test2.ipn.dev
|
|
0x05, 0x74, 0x65, 0x73, 0x74, 0x32, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00,
|
|
}
|
|
|
|
var nxdomainResponse = []byte{
|
|
0x00, 0x00, // transaction id: 0
|
|
0x84, 0x03, // flags: response, authoritative, error: nxdomain
|
|
0x00, 0x01, // one question
|
|
0x00, 0x00, // no answers
|
|
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
|
// Question:
|
|
0x05, 0x74, 0x65, 0x73, 0x74, 0x33, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
|
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
|
}
|
|
|
|
var emptyResponse = []byte{
|
|
0x00, 0x00, // transaction id: 0
|
|
0x84, 0x00, // flags: response, authoritative, no error
|
|
0x00, 0x01, // one question
|
|
0x00, 0x00, // no answers
|
|
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
|
// Question:
|
|
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
|
0x00, 0x1c, 0x00, 0x01, // type AAAA, class IN
|
|
}
|
|
|
|
func TestFull(t *testing.T) {
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
|
|
r.SetConfig(dnsCfg)
|
|
|
|
// One full packet and one error packet
|
|
tests := []struct {
|
|
name string
|
|
request []byte
|
|
response []byte
|
|
}{
|
|
{"all", dnspacket("test1.ipn.dev.", dns.TypeALL), allResponse},
|
|
{"ipv4", dnspacket("test1.ipn.dev.", dns.TypeA), ipv4Response},
|
|
{"ipv6", dnspacket("test2.ipn.dev.", dns.TypeAAAA), ipv6Response},
|
|
{"no-ipv6", dnspacket("test1.ipn.dev.", dns.TypeAAAA), emptyResponse},
|
|
{"upper", dnspacket("TEST1.IPN.DEV.", dns.TypeA), ipv4UppercaseResponse},
|
|
{"ptr4", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), ptrResponse},
|
|
{"ptr6", dnspacket("f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa.",
|
|
dns.TypePTR), ptrResponse6},
|
|
{"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA), nxdomainResponse},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
response, err := syncRespond(r, tt.request)
|
|
if err != nil {
|
|
t.Errorf("err = %v; want nil", err)
|
|
}
|
|
if !bytes.Equal(response, tt.response) {
|
|
t.Errorf("response = %x; want %x", response, tt.response)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAllocs(t *testing.T) {
|
|
r := New(t.Logf, nil)
|
|
defer r.Close()
|
|
r.SetConfig(dnsCfg)
|
|
|
|
// It is seemingly pointless to test allocs in the delegate path,
|
|
// as dialer.Dial -> Read -> Write alone comprise 12 allocs.
|
|
tests := []struct {
|
|
name string
|
|
query []byte
|
|
want int
|
|
}{
|
|
// Name lowercasing and response slice created by dns.NewBuilder.
|
|
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA), 2},
|
|
// 3 extra allocs in rdnsNameToIPv4 and one in marshalPTRRecord (dns.NewName).
|
|
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), 5},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
allocs := testing.AllocsPerRun(100, func() {
|
|
syncRespond(r, tt.query)
|
|
})
|
|
if int(allocs) > tt.want {
|
|
t.Errorf("%s: allocs = %v; want %v", tt.name, allocs, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrimRDNSBonjourPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
in dnsname.FQDN
|
|
want bool
|
|
}{
|
|
{"b._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
|
{"db._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
|
{"r._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
|
{"dr._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
|
{"lb._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
|
{"qq._dns-sd._udp.0.10.20.172.in-addr.arpa.", false},
|
|
{"0.10.20.172.in-addr.arpa.", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
got := hasRDNSBonjourPrefix(test.in)
|
|
if got != test.want {
|
|
t.Errorf("trimRDNSBonjourPrefix(%q) = %v, want %v", test.in, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkFull(b *testing.B) {
|
|
server := serveDNS(b, "127.0.0.1:0",
|
|
"test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
|
defer server.Shutdown()
|
|
|
|
r := New(b.Logf, nil)
|
|
defer r.Close()
|
|
|
|
cfg := dnsCfg
|
|
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
|
|
".": {
|
|
netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
request []byte
|
|
}{
|
|
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA)},
|
|
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR)},
|
|
{"delegated", dnspacket("test.site.", dns.TypeA)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
b.Run(tt.name, func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
syncRespond(r, tt.request)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMarshalResponseFormatError(t *testing.T) {
|
|
resp := new(response)
|
|
resp.Header.RCode = dns.RCodeFormatError
|
|
v, err := marshalResponse(resp)
|
|
if err != nil {
|
|
t.Errorf("marshal error: %v", err)
|
|
}
|
|
t.Logf("response: %q", v)
|
|
}
|