diff --git a/wgengine/tsdns/map.go b/wgengine/tsdns/map.go index 985f47693..65c77175b 100644 --- a/wgengine/tsdns/map.go +++ b/wgengine/tsdns/map.go @@ -17,20 +17,37 @@ type Map struct { // nameToIP is a mapping of Tailscale domain names to their IP addresses. // For example, monitoring.tailscale.us -> 100.64.0.1. nameToIP map[string]netaddr.IP + // ipToName is the inverse of nameToIP. + ipToName map[netaddr.IP]string // names are the keys of nameToIP in sorted order. names []string } // NewMap returns a new Map with name to address mapping given by nameToIP. -func NewMap(nameToIP map[string]netaddr.IP) *Map { - names := make([]string, 0, len(nameToIP)) - for name := range nameToIP { +func NewMap(initNameToIP map[string]netaddr.IP) *Map { + // TODO(dmytro): we have to allocate names and ipToName, but nameToIP can be avoided. + // It is here because control sends us names not in canonical form. Change this. + names := make([]string, 0, len(initNameToIP)) + nameToIP := make(map[string]netaddr.IP, len(initNameToIP)) + ipToName := make(map[netaddr.IP]string, len(initNameToIP)) + + for name, ip := range initNameToIP { + if len(name) == 0 { + // Nothing useful can be done with empty names. + continue + } + if name[len(name)-1] != '.' { + name += "." + } names = append(names, name) + nameToIP[name] = ip + ipToName[ip] = name } sort.Strings(names) return &Map{ nameToIP: nameToIP, + ipToName: ipToName, names: names, } } diff --git a/wgengine/tsdns/map_test.go b/wgengine/tsdns/map_test.go index 5aae62774..d1e79b663 100644 --- a/wgengine/tsdns/map_test.go +++ b/wgengine/tsdns/map_test.go @@ -20,17 +20,17 @@ func TestPretty(t *testing.T) { { "single", NewMap(map[string]netaddr.IP{ - "hello.ipn.dev": netaddr.IPv4(100, 101, 102, 103), + "hello.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), }), - "hello.ipn.dev\t100.101.102.103\n", + "hello.ipn.dev.\t100.101.102.103\n", }, { "multiple", NewMap(map[string]netaddr.IP{ - "test1.domain": netaddr.IPv4(100, 101, 102, 103), - "test2.sub.domain": netaddr.IPv4(100, 99, 9, 1), + "test1.domain.": netaddr.IPv4(100, 101, 102, 103), + "test2.sub.domain.": netaddr.IPv4(100, 99, 9, 1), }), - "test1.domain\t100.101.102.103\ntest2.sub.domain\t100.99.9.1\n", + "test1.domain.\t100.101.102.103\ntest2.sub.domain.\t100.99.9.1\n", }, } @@ -55,75 +55,75 @@ func TestPrettyDiffFrom(t *testing.T) { "from_empty", nil, NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), }), - "+test1.ipn.dev\t100.101.102.103\n+test2.ipn.dev\t100.103.102.101\n", + "+test1.ipn.dev.\t100.101.102.103\n+test2.ipn.dev.\t100.103.102.101\n", }, { "equal", NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), }), NewMap(map[string]netaddr.IP{ - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), }), "", }, { "changed_ip", NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), }), NewMap(map[string]netaddr.IP{ - "test2.ipn.dev": netaddr.IPv4(100, 104, 102, 101), - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 104, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), }), - "-test2.ipn.dev\t100.103.102.101\n+test2.ipn.dev\t100.104.102.101\n", + "-test2.ipn.dev.\t100.103.102.101\n+test2.ipn.dev.\t100.104.102.101\n", }, { "new_domain", NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), }), NewMap(map[string]netaddr.IP{ - "test3.ipn.dev": netaddr.IPv4(100, 105, 106, 107), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), + "test3.ipn.dev.": netaddr.IPv4(100, 105, 106, 107), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), }), - "+test3.ipn.dev\t100.105.106.107\n", + "+test3.ipn.dev.\t100.105.106.107\n", }, { "gone_domain", NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), }), NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), }), - "-test2.ipn.dev\t100.103.102.101\n", + "-test2.ipn.dev.\t100.103.102.101\n", }, { "mixed", NewMap(map[string]netaddr.IP{ - "test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103), - "test4.ipn.dev": netaddr.IPv4(100, 107, 106, 105), - "test5.ipn.dev": netaddr.IPv4(100, 64, 1, 1), - "test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103), + "test4.ipn.dev.": netaddr.IPv4(100, 107, 106, 105), + "test5.ipn.dev.": netaddr.IPv4(100, 64, 1, 1), + "test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101), }), NewMap(map[string]netaddr.IP{ - "test2.ipn.dev": netaddr.IPv4(100, 104, 102, 101), - "test1.ipn.dev": netaddr.IPv4(100, 100, 101, 102), - "test3.ipn.dev": netaddr.IPv4(100, 64, 1, 1), + "test2.ipn.dev.": netaddr.IPv4(100, 104, 102, 101), + "test1.ipn.dev.": netaddr.IPv4(100, 100, 101, 102), + "test3.ipn.dev.": netaddr.IPv4(100, 64, 1, 1), }), - "-test1.ipn.dev\t100.101.102.103\n+test1.ipn.dev\t100.100.101.102\n" + - "-test2.ipn.dev\t100.103.102.101\n+test2.ipn.dev\t100.104.102.101\n" + - "+test3.ipn.dev\t100.64.1.1\n-test4.ipn.dev\t100.107.106.105\n-test5.ipn.dev\t100.64.1.1\n", + "-test1.ipn.dev.\t100.101.102.103\n+test1.ipn.dev.\t100.100.101.102\n" + + "-test2.ipn.dev.\t100.103.102.101\n+test2.ipn.dev.\t100.104.102.101\n" + + "+test3.ipn.dev.\t100.64.1.1\n-test4.ipn.dev.\t100.107.106.105\n-test5.ipn.dev.\t100.64.1.1\n", }, } diff --git a/wgengine/tsdns/tsdns.go b/wgengine/tsdns/tsdns.go index d2aa08109..54ed11b4d 100644 --- a/wgengine/tsdns/tsdns.go +++ b/wgengine/tsdns/tsdns.go @@ -9,6 +9,7 @@ package tsdns import ( "bytes" "context" + "encoding/hex" "errors" "sync" "time" @@ -84,7 +85,7 @@ type Resolver struct { dialer netns.Dialer // mu guards the following fields from being updated while used. - mu sync.RWMutex + mu sync.Mutex // dnsMap is the map most recently received from the control server. dnsMap *Map // nameservers is the list of nameserver addresses that should be used @@ -94,15 +95,15 @@ type Resolver struct { } // NewResolver constructs a resolver associated with the given root domain. +// The root domain must be in canonical form (with a trailing period). func NewResolver(logf logger.Logf, rootDomain string) *Resolver { r := &Resolver{ - logf: logger.WithPrefix(logf, "tsdns: "), - queue: make(chan Packet, queueSize), - responses: make(chan Packet), - errors: make(chan error), - closed: make(chan struct{}), - // Conform to the name format dnsmessage uses (trailing period, bytes). - rootDomain: []byte(rootDomain + "."), + logf: logger.WithPrefix(logf, "tsdns: "), + queue: make(chan Packet, queueSize), + responses: make(chan Packet), + errors: make(chan error), + closed: make(chan struct{}), + rootDomain: []byte(rootDomain), dialer: netns.NewDialer(), } @@ -173,22 +174,40 @@ func (r *Resolver) NextResponse() (Packet, error) { } // Resolve maps a given domain name to the IP address of the host that owns it. -// The domain name must not have a trailing period. +// The domain name must be in canonical form (with a trailing period). func (r *Resolver) Resolve(domain string) (netaddr.IP, dns.RCode, error) { - r.mu.RLock() - if r.dnsMap == nil { - r.mu.RUnlock() + r.mu.Lock() + dnsMap := r.dnsMap + r.mu.Unlock() + + if dnsMap == nil { return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet } - addr, found := r.dnsMap.nameToIP[domain] - r.mu.RUnlock() + addr, found := dnsMap.nameToIP[domain] if !found { return netaddr.IP{}, dns.RCodeNameError, nil } return addr, dns.RCodeSuccess, nil } +// ResolveReverse returns the unique domain name that maps to the given address. +// The returned domain name is in canonical form (with a trailing period). +func (r *Resolver) ResolveReverse(ip netaddr.IP) (string, dns.RCode, error) { + r.mu.Lock() + dnsMap := r.dnsMap + r.mu.Unlock() + + if dnsMap == nil { + return "", dns.RCodeServerFailure, errMapNotSet + } + name, found := dnsMap.ipToName[ip] + if !found { + return "", dns.RCodeNameError, nil + } + return name, dns.RCodeSuccess, nil +} + func (r *Resolver) poll() { defer r.pollGroup.Done() @@ -253,9 +272,9 @@ func (r *Resolver) queryServer(ctx context.Context, server string, query []byte) // delegate forwards the query to all upstream nameservers and returns the first response. func (r *Resolver) delegate(query []byte) ([]byte, error) { - r.mu.RLock() + r.mu.Lock() nameservers := r.nameservers - r.mu.RUnlock() + r.mu.Unlock() if len(nameservers) == 0 { return nil, errNoNameservers @@ -301,8 +320,10 @@ func (r *Resolver) delegate(query []byte) ([]byte, error) { type response struct { Header dns.Header Question dns.Question - Name string - IP netaddr.IP + // Name is the response to a PTR query. + Name string + // IP is the response to an A, AAAA, or ANY query. + IP netaddr.IP } // parseQuery parses the query in given packet into a response struct. @@ -359,6 +380,25 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error return builder.AAAAResource(answerHeader, answer) } +// marshalPTRRecord serializes a PTR record into an active builder. +// The caller may continue using the builder following the call. +func marshalPTRRecord(queryName dns.Name, name string, builder *dns.Builder) error { + var answer dns.PTRResource + var err error + + answerHeader := dns.ResourceHeader{ + Name: queryName, + Type: dns.TypePTR, + Class: dns.ClassINET, + TTL: uint32(defaultTTL / time.Second), + } + answer.PTR, err = dns.NewName(name) + if err != nil { + return err + } + return builder.PTRResource(answerHeader, answer) +} + // marshalResponse serializes the DNS response into a new buffer. func marshalResponse(resp *response) ([]byte, error) { resp.Header.Response = true @@ -389,10 +429,15 @@ func marshalResponse(resp *response) ([]byte, error) { return nil, err } - if resp.IP.Is4() { - err = marshalARecord(resp.Question.Name, resp.IP, &builder) - } else { - err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder) + switch resp.Question.Type { + case dns.TypeA, dns.TypeAAAA, dns.TypeALL: + if resp.IP.Is4() { + err = marshalARecord(resp.Question.Name, resp.IP, &builder) + } else { + err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder) + } + case dns.TypePTR: + err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder) } if err != nil { return nil, err @@ -401,6 +446,120 @@ func marshalResponse(resp *response) ([]byte, error) { return builder.Finish() } +var ( + rdnsv4Suffix = []byte(".in-addr.arpa.") + rdnsv6Suffix = []byte(".ip6.arpa.") +) + +// ptrNameToIPv4 transforms a PTR name representing an IPv4 address to said address. +// Such names are IPv4 labels in reverse order followed by .in-addr.arpa. +// For example, +// 4.3.2.1.in-addr.arpa +// is transformed to +// 1.2.3.4 +func rdnsNameToIPv4(name []byte) (ip netaddr.IP, ok bool) { + name = bytes.TrimSuffix(name, rdnsv4Suffix) + ip, err := netaddr.ParseIP(string(name)) + if err != nil { + return netaddr.IP{}, false + } + if !ip.Is4() { + return netaddr.IP{}, false + } + b := ip.As4() + return netaddr.IPv4(b[3], b[2], b[1], b[0]), true +} + +// ptrNameToIPv6 transforms a PTR name representing an IPv6 address to said address. +// Such names are dot-separated nibbles in reverse order followed by .ip6.arpa. +// For example, +// 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. +// is transformed to +// 2001:db8::567:89ab +func rdnsNameToIPv6(name []byte) (ip netaddr.IP, ok bool) { + var b [32]byte + var ipb [16]byte + + name = bytes.TrimSuffix(name, rdnsv6Suffix) + // 32 nibbles and 31 dots between them. + if len(name) != 63 { + return netaddr.IP{}, false + } + + // Dots and hex digits alternate. + prevDot := true + // i ranges over name backward; j ranges over b forward. + for i, j := len(name)-1, 0; i >= 0; i-- { + thisDot := (name[i] == '.') + if prevDot == thisDot { + return netaddr.IP{}, false + } + prevDot = thisDot + + if !thisDot { + // This is safe assuming alternation. + // We do not check that non-dots are hex digits: hex.Decode below will do that. + b[j] = name[i] + j++ + } + } + + _, err := hex.Decode(ipb[:], b[:]) + if err != nil { + return netaddr.IP{}, false + } + + return netaddr.IPFrom16(ipb), true +} + +// respondReverse returns a DNS response to a PTR query. +// It is assumed that resp.Question is populated by respond before this is called. +func (r *Resolver) respondReverse(query []byte, resp *response) ([]byte, error) { + name := resp.Question.Name.Data[:resp.Question.Name.Length] + + shouldDelegate := false + + var ip netaddr.IP + var ok bool + var err error + switch { + case bytes.HasSuffix(name, rdnsv4Suffix): + ip, ok = rdnsNameToIPv4(name) + case bytes.HasSuffix(name, rdnsv6Suffix): + ip, ok = rdnsNameToIPv6(name) + default: + shouldDelegate = true + } + + // It is more likely that we failed in parsing the name than that it is actually malformed. + // To avoid frustrating users, just log and delegate. + if !ok { + // Without this conversion, escape analysis rules that resp escapes. + r.logf("parsing rdns: malformed name: %s", resp.Question.Name.String()) + shouldDelegate = true + } + + if !shouldDelegate { + resp.Name, resp.Header.RCode, err = r.ResolveReverse(ip) + if err != nil { + r.logf("resolving rdns: %v", ip, err) + } + shouldDelegate = (resp.Header.RCode == dns.RCodeNameError) + } + + if shouldDelegate { + out, err := r.delegate(query) + if err != nil { + r.logf("delegating rdns: %v", err) + resp.Header.RCode = dns.RCodeServerFailure + return marshalResponse(resp) + } + return out, nil + } + + return marshalResponse(resp) +} + // respond returns a DNS response to query. func (r *Resolver) respond(query []byte) ([]byte, error) { resp := new(response) @@ -416,7 +575,14 @@ func (r *Resolver) respond(query []byte) ([]byte, error) { return marshalResponse(resp) } - // Delegate only when not a subdomain of rootDomain. + // Always try to handle reverse lookups; delegate inside when not found. + // This way, queries for exitent nodes do not leak, + // but we behave gracefully if non-Tailscale nodes exist in CGNATRange. + if resp.Question.Type == dns.TypePTR { + return r.respondReverse(query, resp) + } + + // Delegate forward lookups when not a subdomain of rootDomain. // We do this on bytes because Name.String() allocates. rawName := resp.Question.Name.Data[:resp.Question.Name.Length] if !bytes.HasSuffix(rawName, r.rootDomain) { @@ -431,11 +597,8 @@ func (r *Resolver) respond(query []byte) ([]byte, error) { switch resp.Question.Type { case dns.TypeA, dns.TypeAAAA, dns.TypeALL: - domain := resp.Question.Name.String() - // Strip off the trailing period. - // This is safe: Name is guaranteed to have a trailing period by construction. - domain = domain[:len(domain)-1] - resp.IP, resp.Header.RCode, err = r.Resolve(domain) + name := resp.Question.Name.String() + resp.IP, resp.Header.RCode, err = r.Resolve(name) default: resp.Header.RCode = dns.RCodeNotImplemented err = errNotImplemented diff --git a/wgengine/tsdns/tsdns_test.go b/wgengine/tsdns/tsdns_test.go index d2194d809..6844f78a4 100644 --- a/wgengine/tsdns/tsdns_test.go +++ b/wgengine/tsdns/tsdns_test.go @@ -22,12 +22,12 @@ var testipv6 = netaddr.IPv6Raw([16]byte{ 0x0c, 0x0d, 0x0e, 0x0f, }) -var dnsMap = &Map{ - nameToIP: map[string]netaddr.IP{ +var dnsMap = NewMap( + map[string]netaddr.IP{ "test1.ipn.dev": testipv4, "test2.ipn.dev": testipv6, }, -} +) func dnspacket(domain string, tp dns.Type) []byte { var dnsHeader dns.Header @@ -97,6 +97,86 @@ func syncRespond(r *Resolver, query []byte) ([]byte, error) { return resp.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 string + 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) { + name := []byte(tt.input) + ip, ok := rdnsNameToIPv4(name) + 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 string + 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) { + name := []byte(tt.input) + ip, ok := rdnsNameToIPv6(name) + 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 TestResolve(t *testing.T) { r := NewResolver(t.Logf, "ipn.dev") r.SetMap(dnsMap) @@ -108,10 +188,10 @@ func TestResolve(t *testing.T) { ip netaddr.IP code dns.RCode }{ - {"ipv4", "test1.ipn.dev", testipv4, dns.RCodeSuccess}, - {"ipv6", "test2.ipn.dev", testipv6, dns.RCodeSuccess}, - {"nxdomain", "test3.ipn.dev", netaddr.IP{}, dns.RCodeNameError}, - {"foreign domain", "google.com", netaddr.IP{}, dns.RCodeNameError}, + {"ipv4", "test1.ipn.dev.", testipv4, dns.RCodeSuccess}, + {"ipv6", "test2.ipn.dev.", testipv6, dns.RCodeSuccess}, + {"nxdomain", "test3.ipn.dev.", netaddr.IP{}, dns.RCodeNameError}, + {"foreign domain", "google.com.", netaddr.IP{}, dns.RCodeNameError}, } for _, tt := range tests { @@ -131,6 +211,38 @@ func TestResolve(t *testing.T) { } } +func TestResolveReverse(t *testing.T) { + r := NewResolver(t.Logf, "ipn.dev") + r.SetMap(dnsMap) + r.Start() + + tests := []struct { + name string + ip netaddr.IP + want string + 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, err := r.ResolveReverse(tt.ip) + if err != nil { + t.Errorf("err = %v; want nil", err) + } + 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 TestDelegate(t *testing.T) { dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6)) dnsHandleFunc("nxdomain.site.", resolveToNXDOMAIN) @@ -200,7 +312,7 @@ func TestDelegate(t *testing.T) { } func TestConcurrentSetMap(t *testing.T) { - r := NewResolver(t.Logf, "ipn.dev") + r := NewResolver(t.Logf, "ipn.dev.") r.Start() // This is purely to ensure that Resolve does not race with SetMap. @@ -218,7 +330,7 @@ func TestConcurrentSetMap(t *testing.T) { } func TestConcurrentSetNameservers(t *testing.T) { - r := NewResolver(t.Logf, "ipn.dev") + r := NewResolver(t.Logf, "ipn.dev.") r.Start() packet := dnspacket("google.com.", dns.TypeA) @@ -271,6 +383,26 @@ var validIPv6Response = []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xb, 0xc, 0xd, 0xe, 0xf, } +var validPTRResponse = []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 nxdomainResponse = []byte{ 0x00, 0x00, // transaction id: 0 0x84, 0x03, // flags: response, authoritative, error: nxdomain @@ -283,7 +415,7 @@ var nxdomainResponse = []byte{ } func TestFull(t *testing.T) { - r := NewResolver(t.Logf, "ipn.dev") + r := NewResolver(t.Logf, "ipn.dev.") r.SetMap(dnsMap) r.Start() @@ -295,6 +427,7 @@ func TestFull(t *testing.T) { }{ {"ipv4", dnspacket("test1.ipn.dev.", dns.TypeA), validIPv4Response}, {"ipv6", dnspacket("test2.ipn.dev.", dns.TypeAAAA), validIPv6Response}, + {"ptr", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), validPTRResponse}, {"error", dnspacket("test3.ipn.dev.", dns.TypeA), nxdomainResponse}, } @@ -312,34 +445,44 @@ func TestFull(t *testing.T) { } func TestAllocs(t *testing.T) { - r := NewResolver(t.Logf, "ipn.dev") + r := NewResolver(t.Logf, "ipn.dev.") r.SetMap(dnsMap) r.Start() // 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) + tests := []struct { + name string + query []byte + want int + }{ + // The only alloc is the slice created by dns.NewBuilder. + {"forward", dnspacket("test1.ipn.dev.", dns.TypeA), 1}, + // 3 extra allocs in rdnsNameToIPv4 and one in marshalPTRRecord (dns.NewName). + {"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), 5}, + } - allocs := testing.AllocsPerRun(100, func() { - syncRespond(r, query) - }) - - if allocs > 1 { - t.Errorf("allocs = %v; want 1", allocs) + 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 BenchmarkFull(b *testing.B) { - r := NewResolver(b.Logf, "ipn.dev") + r := NewResolver(b.Logf, "ipn.dev.") r.SetMap(dnsMap) r.Start() - // One full packet and one error packet tests := []struct { name string request []byte }{ - {"valid", dnspacket("test1.ipn.dev.", dns.TypeA)}, + {"forward", dnspacket("test1.ipn.dev.", dns.TypeA)}, + {"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR)}, {"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA)}, } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index c10bb04ad..a6117eefd 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -62,7 +62,7 @@ const ( ) // magicDNSDomain is the parent domain for Tailscale nodes. -const magicDNSDomain = "b.tailscale.net" +const magicDNSDomain = "b.tailscale.net." // Lazy wireguard-go configuration parameters. const (