@ -6,113 +6,173 @@ package tsdns
import (
import (
"bytes"
"bytes"
"errors"
"sync"
"sync"
"testing"
"testing"
dns "golang.org/x/net/dns/dnsmessage"
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"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 {
var dnsMap = & Map {
domainToIP : map [ string ] netaddr . IP {
domainToIP : map [ string ] netaddr . IP {
"test1.ipn.dev" : netaddr . IPv4 ( 1 , 2 , 3 , 4 ) ,
"test1.ipn.dev" : netaddr . IPv4 ( 1 , 2 , 3 , 4 ) ,
"test2.ipn.dev" : netaddr . IPv4 ( 5 , 6 , 7 , 8 ) ,
"test2.ipn.dev" : netaddr . IPv 6Raw( test2bytes ) ,
} ,
} ,
}
}
func dnspacket ( srcip, dstip packet . IP , domain string , tp dns . Type , response bool ) * packet . ParsedPacket {
func dnspacket ( domain string , tp dns . Type ) [ ] byte {
dnsHeader := dns . Header { Response : response }
var dnsHeader dns . Header
question := dns . Question {
question := dns . Question {
Name : dns . MustNewName ( domain ) ,
Name : dns . MustNewName ( domain ) ,
Type : tp ,
Type : tp ,
Class : dns . ClassINET ,
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 := dns . NewBuilder ( nil , dnsHeader )
builder . StartQuestions ( )
builder . StartQuestions ( )
builder . Question ( question )
builder . Question ( question )
payload , _ := builder . Finish ( )
payload , _ := builder . Finish ( )
buf := packet . Generate ( udpHeader , payload )
return payload
}
pp := new ( packet . ParsedPacket )
func extractipcode ( response [ ] byte ) ( netaddr . IP , dns . RCode , error ) {
pp . Decode ( buf )
var ip netaddr . IP
var parser dns . Parser
return pp
h , err := parser . Start ( response )
}
if err != nil {
return ip , 0 , err
}
func TestAcceptsPacket ( t * testing . T ) {
if ! h . Response {
r := NewResolver ( t . Logf )
return ip , 0 , errors . New ( "not a response" )
r . SetMap ( dnsMap )
}
if h . RCode != dns . RCodeSuccess {
return ip , h . RCode , nil
}
src := packet . IP ( 0x64656667 ) // 100.101.102.103
err = parser . SkipAllQuestions ( )
dst := packet . IP ( 0x64646464 ) // 100.100.100.100
if err != nil {
tests := [ ] struct {
return ip , 0 , err
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 } ,
}
}
for _ , tt := range tests {
ah , err := parser . AnswerHeader ( )
t . Run ( tt . name , func ( t * testing . T ) {
if err != nil {
accepts := r . AcceptsPacket ( tt . request )
return ip , 0 , err
if accepts != tt . want {
t . Errorf ( "accepts = %v; want %v" , accepts , tt . want )
}
} )
}
}
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 ) {
func TestResolve ( t * testing . T ) {
r := NewResolver ( t . Logf )
r := NewResolver ( t . Logf , "ipn.dev" )
r . SetMap ( dnsMap )
r . SetMap ( dnsMap )
r . Start ( )
tests := [ ] struct {
tests := [ ] struct {
name string
name string
domain string
domain string
ip netaddr . IP
ip netaddr . IP
code dns . RCode
code dns . RCode
iserr bool
} {
} {
{ "valid" , "test1.ipn.dev" , netaddr . IPv4 ( 1 , 2 , 3 , 4 ) , dns . RCodeSuccess , false } ,
{ "ipv4" , "test1.ipn.dev" , netaddr . IPv4 ( 1 , 2 , 3 , 4 ) , dns . RCodeSuccess } ,
{ "nxdomain" , "test3.ipn.dev" , netaddr . IP { } , dns . RCodeNameError , true } ,
{ "ipv6" , "test2.ipn.dev" , netaddr . IPv6Raw ( test2bytes ) , dns . RCodeSuccess } ,
{ "not our domain" , "google.com" , netaddr . IP { } , dns . RCodeRefused , true } ,
{ "nxdomain" , "test3.ipn.dev" , netaddr . IP { } , dns . RCodeNameError } ,
{ "foreign domain" , "google.com" , netaddr . IP { } , dns . RCodeNameError } ,
}
}
for _ , tt := range tests {
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
t . Run ( tt . name , func ( t * testing . T ) {
ip , code , err := r . Resolve ( tt . domain )
ip , code , err := r . Resolve ( tt . domain )
if err != nil && ! tt . iserr {
if err != nil {
t . Errorf ( "err = %v; want nil" , err )
t . Errorf ( "err = %v; want nil" , err )
} else if err == nil && tt . iserr {
t . Errorf ( "err = nil; want non-nil" )
}
}
if code != tt . code {
if code != tt . code {
t . Errorf ( "code = %v; want %v" , code , tt . code )
t . Errorf ( "code = %v; want %v" , code , tt . code )
}
}
// Only check ip for non-err
// 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 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 )
t . Errorf ( "ip = %v; want %v" , ip , tt . ip )
}
}
} )
} )
}
}
}
}
func TestConcurrentSet ( t * testing . T ) {
func TestConcurrentSetMap ( t * testing . T ) {
r := NewResolver ( t . Logf )
r := NewResolver ( t . Logf , "ipn.dev" )
r . Start ( )
// This is purely to ensure that Resolve does not race with SetMap.
// This is purely to ensure that Resolve does not race with SetMap.
var wg sync . WaitGroup
var wg sync . WaitGroup
@ -128,16 +188,26 @@ func TestConcurrentSet(t *testing.T) {
wg . Wait ( )
wg . Wait ( )
}
}
var validResponse = [ ] byte {
func TestConcurrentSetNameservers ( t * testing . T ) {
// IP header
r := NewResolver ( t . Logf , "ipn.dev" )
0x45 , 0x00 , 0x00 , 0x58 , 0xff , 0xff , 0x00 , 0x00 , 0x40 , 0x11 , 0xe7 , 0x00 ,
r . Start ( )
// Source IP
packet := dnspacket ( "google.com." , dns . TypeA )
0x64 , 0x64 , 0x64 , 0x64 ,
// Destination IP
// This is purely to ensure that delegation does not race with SetNameservers.
0x64 , 0x65 , 0x66 , 0x67 ,
var wg sync . WaitGroup
// UDP header
wg . Add ( 2 )
0x00 , 0x35 , 0x04 , 0xd2 , 0x00 , 0x44 , 0x53 , 0xdd ,
go func ( ) {
// DNS payload
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
0x00 , 0x00 , // transaction id: 0
0x84 , 0x00 , // flags: response, authoritative, no error
0x84 , 0x00 , // flags: response, authoritative, no error
0x00 , 0x01 , // one question
0x00 , 0x01 , // one question
@ -154,16 +224,25 @@ var validResponse = []byte{
0x01 , 0x02 , 0x03 , 0x04 , // A: 1.2.3.4
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 {
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
0x00 , 0x00 , // transaction id: 0
0x84 , 0x03 , // flags: response, authoritative, error: nxdomain
0x84 , 0x03 , // flags: response, authoritative, error: nxdomain
0x00 , 0x01 , // one question
0x00 , 0x01 , // one question
@ -175,25 +254,24 @@ var nxdomainResponse = []byte{
}
}
func TestFull ( t * testing . T ) {
func TestFull ( t * testing . T ) {
r := NewResolver ( t . Logf )
r := NewResolver ( t . Logf , "ipn.dev" )
r . SetMap ( dnsMap )
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
// One full packet and one error packet
tests := [ ] struct {
tests := [ ] struct {
name string
name string
request * packet . ParsedPacket
request [ ] byte
response [ ] byte
response [ ] byte
} {
} {
{ "valid" , dnspacket ( src , dst , "test1.ipn.dev." , dns . TypeA , false ) , validResponse } ,
{ "ipv4" , dnspacket ( "test1.ipn.dev." , dns . TypeA ) , validIPv4Response } ,
{ "error" , dnspacket ( src , dst , "test3.ipn.dev." , dns . TypeA , false ) , nxdomainResponse } ,
{ "ipv6" , dnspacket ( "test2.ipn.dev." , dns . TypeAAAA ) , validIPv6Response } ,
{ "error" , dnspacket ( "test3.ipn.dev." , dns . TypeA ) , nxdomainResponse } ,
}
}
for _ , tt := range tests {
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
t . Run ( tt . name , func ( t * testing . T ) {
buf := make ( [ ] byte , 512 )
response , err := syncRespond ( r , tt . request )
response , err := r . Respond ( tt . request , buf )
if err != nil {
if err != nil {
t . Errorf ( "err = %v; want nil" , err )
t . Errorf ( "err = %v; want nil" , err )
}
}
@ -205,43 +283,41 @@ func TestFull(t *testing.T) {
}
}
func TestAllocs ( t * testing . T ) {
func TestAllocs ( t * testing . T ) {
r := NewResolver ( t . Logf )
r := NewResolver ( t . Logf , "ipn.dev" )
r . SetMap ( dnsMap )
r . SetMap ( dnsMap )
r . Start ( )
src := packet . IP ( 0x64656667 ) // 100.101.102.103
// It is seemingly pointless to test allocs in the delegate path,
dst := packet . IP ( 0x64646464 ) // 100.100.100.100
// as dialer.Dial -> Read -> Write alone comprise 12 allocs.
query := dnspacket ( src , dst , "test1.ipn.dev." , dns . TypeA , false )
query := dnspacket ( "test1.ipn.dev." , dns . TypeA )
buf := make ( [ ] byte , 512 )
allocs := testing . AllocsPerRun ( 100 , func ( ) {
allocs := testing . AllocsPerRun ( 100 , func ( ) {
r . Respond ( query , buf )
syncRespond ( r , query )
} )
} )
if allocs > 0 {
if allocs > 1 {
t . Errorf ( "allocs = %v; want 0 ", allocs )
t . Errorf ( "allocs = %v; want 1 ", allocs )
}
}
}
}
func BenchmarkFull ( b * testing . B ) {
func BenchmarkFull ( b * testing . B ) {
r := NewResolver ( b . Logf )
r := NewResolver ( b . Logf , "ipn.dev" )
r . SetMap ( dnsMap )
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
// One full packet and one error packet
tests := [ ] struct {
tests := [ ] struct {
name string
name string
request * packet . ParsedPacket
request [ ] byte
} {
} {
{ "valid" , dnspacket ( src , dst , "test1.ipn.dev." , dns . TypeA , false ) } ,
{ "valid" , dnspacket ( "test1.ipn.dev." , dns . TypeA ) } ,
{ "nxdomain" , dnspacket ( src , dst , "test3.ipn.dev." , dns . TypeA , false ) } ,
{ "nxdomain" , dnspacket ( "test3.ipn.dev." , dns . TypeA ) } ,
}
}
buf := make ( [ ] byte , 512 )
for _ , tt := range tests {
for _ , tt := range tests {
b . Run ( tt . name , func ( b * testing . B ) {
b . Run ( tt . name , func ( b * testing . B ) {
for i := 0 ; i < b . N ; i ++ {
for i := 0 ; i < b . N ; i ++ {
r . Respond( tt. request , buf )
sync Respond( r, tt. request )
}
}
} )
} )
}
}