ipn: fix netmap change tracking and dns map generation (#609)

Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:
Dmytro Shynkevych
2020-07-28 21:47:23 -04:00
committed by GitHub
parent 3e3c24b8f6
commit c7582dc234
10 changed files with 450 additions and 111 deletions

118
wgengine/tsdns/map.go Normal file
View File

@@ -0,0 +1,118 @@
// 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 tsdns
import (
"fmt"
"sort"
"strings"
"inet.af/netaddr"
)
// Map is all the data Resolver needs to resolve DNS queries within the Tailscale network.
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
// 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 {
names = append(names, name)
}
sort.Strings(names)
return &Map{
nameToIP: nameToIP,
names: names,
}
}
func printSingleNameIP(buf *strings.Builder, name string, ip netaddr.IP) {
// Output width is exactly 80 columns.
fmt.Fprintf(buf, "%s\t%s\n", name, ip)
}
func (m *Map) Pretty() string {
buf := new(strings.Builder)
for _, name := range m.names {
printSingleNameIP(buf, name, m.nameToIP[name])
}
return buf.String()
}
func (m *Map) PrettyDiffFrom(old *Map) string {
var (
oldNameToIP map[string]netaddr.IP
newNameToIP map[string]netaddr.IP
oldNames []string
newNames []string
)
if old != nil {
oldNameToIP = old.nameToIP
oldNames = old.names
}
if m != nil {
newNameToIP = m.nameToIP
newNames = m.names
}
buf := new(strings.Builder)
for len(oldNames) > 0 && len(newNames) > 0 {
var name string
newName, oldName := newNames[0], oldNames[0]
switch {
case oldName < newName:
name = oldName
oldNames = oldNames[1:]
case oldName > newName:
name = newName
newNames = newNames[1:]
case oldNames[0] == newNames[0]:
name = oldNames[0]
oldNames = oldNames[1:]
newNames = newNames[1:]
}
ipOld, inOld := oldNameToIP[name]
ipNew, inNew := newNameToIP[name]
switch {
case !inOld:
buf.WriteByte('+')
printSingleNameIP(buf, name, ipNew)
case !inNew:
buf.WriteByte('-')
printSingleNameIP(buf, name, ipOld)
case ipOld != ipNew:
buf.WriteByte('-')
printSingleNameIP(buf, name, ipOld)
buf.WriteByte('+')
printSingleNameIP(buf, name, ipNew)
}
}
for _, name := range oldNames {
if _, ok := newNameToIP[name]; !ok {
buf.WriteByte('-')
printSingleNameIP(buf, name, oldNameToIP[name])
}
}
for _, name := range newNames {
if _, ok := oldNameToIP[name]; !ok {
buf.WriteByte('+')
printSingleNameIP(buf, name, newNameToIP[name])
}
}
return buf.String()
}

138
wgengine/tsdns/map_test.go Normal file
View File

@@ -0,0 +1,138 @@
// 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 tsdns
import (
"testing"
"inet.af/netaddr"
)
func TestPretty(t *testing.T) {
tests := []struct {
name string
dmap *Map
want string
}{
{"empty", NewMap(nil), ""},
{
"single",
NewMap(map[string]netaddr.IP{
"hello.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
}),
"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\t100.101.102.103\ntest2.sub.domain\t100.99.9.1\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.dmap.Pretty()
if tt.want != got {
t.Errorf("want %v; got %v", tt.want, got)
}
})
}
}
func TestPrettyDiffFrom(t *testing.T) {
tests := []struct {
name string
map1 *Map
map2 *Map
want string
}{
{
"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\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),
}),
NewMap(map[string]netaddr.IP{
"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),
}),
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\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),
}),
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\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),
}),
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
}),
"-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),
}),
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),
}),
"-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",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.map2.PrettyDiffFrom(tt.map1)
if tt.want != got {
t.Errorf("want %v; got %v", tt.want, got)
}
})
}
}

View File

@@ -45,18 +45,6 @@ var (
errNotQuery = errors.New("not a DNS query")
)
// Map is all the data Resolver needs to resolve DNS queries within the Tailscale network.
type Map struct {
// domainToIP is a mapping of Tailscale domains to their IP addresses.
// For example, monitoring.tailscale.us -> 100.64.0.1.
domainToIP map[string]netaddr.IP
}
// NewMap returns a new Map with domain to address mapping given by domainToIP.
func NewMap(domainToIP map[string]netaddr.IP) *Map {
return &Map{domainToIP: domainToIP}
}
// Packet represents a DNS payload together with the address of its origin.
type Packet struct {
// Payload is the application layer DNS payload.
@@ -142,8 +130,10 @@ func (r *Resolver) Close() {
// SetMap sets the resolver's DNS map, taking ownership of it.
func (r *Resolver) SetMap(m *Map) {
r.mu.Lock()
oldMap := r.dnsMap
r.dnsMap = m
r.mu.Unlock()
r.logf("map diff:\n%s", m.PrettyDiffFrom(oldMap))
}
// SetUpstreamNameservers sets the addresses of the resolver's
@@ -189,7 +179,7 @@ func (r *Resolver) Resolve(domain string) (netaddr.IP, dns.RCode, error) {
r.mu.RUnlock()
return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet
}
addr, found := r.dnsMap.domainToIP[domain]
addr, found := r.dnsMap.nameToIP[domain]
r.mu.RUnlock()
if !found {

View File

@@ -23,7 +23,7 @@ var testipv6 = netaddr.IPv6Raw([16]byte{
})
var dnsMap = &Map{
domainToIP: map[string]netaddr.IP{
nameToIP: map[string]netaddr.IP{
"test1.ipn.dev": testipv4,
"test2.ipn.dev": testipv6,
},