mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
34d2f5a3d9
Track endpoints internally with a new tailcfg.Endpoint type that includes a typed netaddr.IPPort (instead of just a string) and includes a type for how that endpoint was discovered (STUN, local, etc). Use []tailcfg.Endpoint instead of []string internally. At the last second, send it to the control server as the existing []string for endpoints, but also include a new parallel MapRequest.EndpointType []tailcfg.EndpointType, so the control server can start filtering out less-important endpoint changes from new-enough clients. Notably, STUN-discovered endpoints can be filtered out from 1.6+ clients, as they can discover them amongst each other via CallMeMaybe disco exchanges started over DERP. And STUN endpoints change a lot, causing a lot of MapResposne updates. But portmapped endpoints are worth keeping for now, as they they work right away without requiring the firewall traversal extra RTT dance. End result will be less control->client bandwidth. (despite negligible increase in client->control bandwidth) Updates tailscale/corp#1543 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
521 lines
9.7 KiB
Go
521 lines
9.7 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 tailcfg
|
|
|
|
import (
|
|
"encoding"
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"inet.af/netaddr"
|
|
"tailscale.com/types/wgkey"
|
|
)
|
|
|
|
func fieldsOf(t reflect.Type) (fields []string) {
|
|
for i := 0; i < t.NumField(); i++ {
|
|
fields = append(fields, t.Field(i).Name)
|
|
}
|
|
return
|
|
}
|
|
|
|
func TestHostinfoEqual(t *testing.T) {
|
|
hiHandles := []string{
|
|
"IPNVersion", "FrontendLogID", "BackendLogID",
|
|
"OS", "OSVersion", "Package", "DeviceModel", "Hostname",
|
|
"ShieldsUp", "ShareeNode",
|
|
"GoArch",
|
|
"RoutableIPs", "RequestTags",
|
|
"Services", "NetInfo",
|
|
}
|
|
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
|
|
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
|
have, hiHandles)
|
|
}
|
|
|
|
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
|
for _, s := range strs {
|
|
n, err := netaddr.ParseIPPrefix(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ns = append(ns, n)
|
|
}
|
|
return ns
|
|
}
|
|
tests := []struct {
|
|
a, b *Hostinfo
|
|
want bool
|
|
}{
|
|
{
|
|
nil,
|
|
nil,
|
|
true,
|
|
},
|
|
{
|
|
&Hostinfo{},
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
nil,
|
|
&Hostinfo{},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{},
|
|
&Hostinfo{},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{IPNVersion: "1"},
|
|
&Hostinfo{IPNVersion: "2"},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{IPNVersion: "2"},
|
|
&Hostinfo{IPNVersion: "2"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{FrontendLogID: "1"},
|
|
&Hostinfo{FrontendLogID: "2"},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{FrontendLogID: "2"},
|
|
&Hostinfo{FrontendLogID: "2"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{BackendLogID: "1"},
|
|
&Hostinfo{BackendLogID: "2"},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{BackendLogID: "2"},
|
|
&Hostinfo{BackendLogID: "2"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{OS: "windows"},
|
|
&Hostinfo{OS: "linux"},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{OS: "windows"},
|
|
&Hostinfo{OS: "windows"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{Hostname: "vega"},
|
|
&Hostinfo{Hostname: "iris"},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{Hostname: "vega"},
|
|
&Hostinfo{Hostname: "vega"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{RoutableIPs: nil},
|
|
&Hostinfo{RoutableIPs: nets("10.0.0.0/16")},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{RoutableIPs: nets("10.1.0.0/16", "192.168.1.0/24")},
|
|
&Hostinfo{RoutableIPs: nets("10.2.0.0/16", "192.168.2.0/24")},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{RoutableIPs: nets("10.1.0.0/16", "192.168.1.0/24")},
|
|
&Hostinfo{RoutableIPs: nets("10.1.0.0/16", "192.168.2.0/24")},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{RoutableIPs: nets("10.1.0.0/16", "192.168.1.0/24")},
|
|
&Hostinfo{RoutableIPs: nets("10.1.0.0/16", "192.168.1.0/24")},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{RequestTags: []string{"abc", "def"}},
|
|
&Hostinfo{RequestTags: []string{"abc", "def"}},
|
|
true,
|
|
},
|
|
{
|
|
&Hostinfo{RequestTags: []string{"abc", "def"}},
|
|
&Hostinfo{RequestTags: []string{"abc", "123"}},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{RequestTags: []string{}},
|
|
&Hostinfo{RequestTags: []string{"abc"}},
|
|
false,
|
|
},
|
|
|
|
{
|
|
&Hostinfo{Services: []Service{{Proto: TCP, Port: 1234, Description: "foo"}}},
|
|
&Hostinfo{Services: []Service{{Proto: UDP, Port: 2345, Description: "bar"}}},
|
|
false,
|
|
},
|
|
{
|
|
&Hostinfo{Services: []Service{{Proto: TCP, Port: 1234, Description: "foo"}}},
|
|
&Hostinfo{Services: []Service{{Proto: TCP, Port: 1234, Description: "foo"}}},
|
|
true,
|
|
},
|
|
{
|
|
&Hostinfo{ShareeNode: true},
|
|
&Hostinfo{},
|
|
false,
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.a.Equal(tt.b)
|
|
if got != tt.want {
|
|
t.Errorf("%d. Equal = %v; want %v", i, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodeEqual(t *testing.T) {
|
|
nodeHandles := []string{
|
|
"ID", "StableID", "Name", "User", "Sharer",
|
|
"Key", "KeyExpiry", "Machine", "DiscoKey",
|
|
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
|
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
|
|
"Capabilities",
|
|
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
|
}
|
|
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
|
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
|
have, nodeHandles)
|
|
}
|
|
|
|
newPublicKey := func(t *testing.T) wgkey.Key {
|
|
t.Helper()
|
|
k, err := wgkey.NewPrivate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return k.Public()
|
|
}
|
|
n1 := newPublicKey(t)
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
a, b *Node
|
|
want bool
|
|
}{
|
|
{
|
|
&Node{},
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
nil,
|
|
&Node{},
|
|
false,
|
|
},
|
|
{
|
|
&Node{},
|
|
&Node{},
|
|
true,
|
|
},
|
|
{
|
|
&Node{},
|
|
&Node{},
|
|
true,
|
|
},
|
|
{
|
|
&Node{ID: 1},
|
|
&Node{},
|
|
false,
|
|
},
|
|
{
|
|
&Node{ID: 1},
|
|
&Node{ID: 1},
|
|
true,
|
|
},
|
|
{
|
|
&Node{StableID: "node-abcd"},
|
|
&Node{},
|
|
false,
|
|
},
|
|
{
|
|
&Node{StableID: "node-abcd"},
|
|
&Node{StableID: "node-abcd"},
|
|
true,
|
|
},
|
|
{
|
|
&Node{User: 0},
|
|
&Node{User: 1},
|
|
false,
|
|
},
|
|
{
|
|
&Node{User: 1},
|
|
&Node{User: 1},
|
|
true,
|
|
},
|
|
{
|
|
&Node{Key: NodeKey(n1)},
|
|
&Node{Key: NodeKey(newPublicKey(t))},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Key: NodeKey(n1)},
|
|
&Node{Key: NodeKey(n1)},
|
|
true,
|
|
},
|
|
{
|
|
&Node{KeyExpiry: now},
|
|
&Node{KeyExpiry: now.Add(60 * time.Second)},
|
|
false,
|
|
},
|
|
{
|
|
&Node{KeyExpiry: now},
|
|
&Node{KeyExpiry: now},
|
|
true,
|
|
},
|
|
{
|
|
&Node{Machine: MachineKey(n1)},
|
|
&Node{Machine: MachineKey(newPublicKey(t))},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Machine: MachineKey(n1)},
|
|
&Node{Machine: MachineKey(n1)},
|
|
true,
|
|
},
|
|
{
|
|
&Node{Addresses: []netaddr.IPPrefix{}},
|
|
&Node{Addresses: nil},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Addresses: []netaddr.IPPrefix{}},
|
|
&Node{Addresses: []netaddr.IPPrefix{}},
|
|
true,
|
|
},
|
|
{
|
|
&Node{AllowedIPs: []netaddr.IPPrefix{}},
|
|
&Node{AllowedIPs: nil},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Addresses: []netaddr.IPPrefix{}},
|
|
&Node{Addresses: []netaddr.IPPrefix{}},
|
|
true,
|
|
},
|
|
{
|
|
&Node{Endpoints: []string{}},
|
|
&Node{Endpoints: nil},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Endpoints: []string{}},
|
|
&Node{Endpoints: []string{}},
|
|
true,
|
|
},
|
|
{
|
|
&Node{Hostinfo: Hostinfo{Hostname: "alice"}},
|
|
&Node{Hostinfo: Hostinfo{Hostname: "bob"}},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Hostinfo: Hostinfo{}},
|
|
&Node{Hostinfo: Hostinfo{}},
|
|
true,
|
|
},
|
|
{
|
|
&Node{Created: now},
|
|
&Node{Created: now.Add(60 * time.Second)},
|
|
false,
|
|
},
|
|
{
|
|
&Node{Created: now},
|
|
&Node{Created: now},
|
|
true,
|
|
},
|
|
{
|
|
&Node{LastSeen: &now},
|
|
&Node{LastSeen: nil},
|
|
false,
|
|
},
|
|
{
|
|
&Node{LastSeen: &now},
|
|
&Node{LastSeen: &now},
|
|
true,
|
|
},
|
|
{
|
|
&Node{DERP: "foo"},
|
|
&Node{DERP: "bar"},
|
|
false,
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.a.Equal(tt.b)
|
|
if got != tt.want {
|
|
t.Errorf("%d. Equal = %v; want %v", i, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNetInfoFields(t *testing.T) {
|
|
handled := []string{
|
|
"MappingVariesByDestIP",
|
|
"HairPinning",
|
|
"WorkingIPv6",
|
|
"WorkingUDP",
|
|
"HavePortMap",
|
|
"UPnP",
|
|
"PMP",
|
|
"PCP",
|
|
"PreferredDERP",
|
|
"LinkType",
|
|
"DERPLatency",
|
|
}
|
|
if have := fieldsOf(reflect.TypeOf(NetInfo{})); !reflect.DeepEqual(have, handled) {
|
|
t.Errorf("NetInfo.Clone/BasicallyEqually check might be out of sync\nfields: %q\nhandled: %q\n",
|
|
have, handled)
|
|
}
|
|
}
|
|
|
|
func TestMachineKeyMarshal(t *testing.T) {
|
|
var k1, k2 MachineKey
|
|
for i := range k1 {
|
|
k1[i] = byte(i)
|
|
}
|
|
testKey(t, "mkey:", k1, &k2)
|
|
}
|
|
|
|
func TestNodeKeyMarshal(t *testing.T) {
|
|
var k1, k2 NodeKey
|
|
for i := range k1 {
|
|
k1[i] = byte(i)
|
|
}
|
|
testKey(t, "nodekey:", k1, &k2)
|
|
}
|
|
|
|
func TestDiscoKeyMarshal(t *testing.T) {
|
|
var k1, k2 DiscoKey
|
|
for i := range k1 {
|
|
k1[i] = byte(i)
|
|
}
|
|
testKey(t, "discokey:", k1, &k2)
|
|
}
|
|
|
|
type keyIn interface {
|
|
String() string
|
|
MarshalText() ([]byte, error)
|
|
}
|
|
|
|
func testKey(t *testing.T, prefix string, in keyIn, out encoding.TextUnmarshaler) {
|
|
got, err := in.MarshalText()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := out.UnmarshalText(got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := in.String(); string(got) != s {
|
|
t.Errorf("MarshalText = %q != String %q", got, s)
|
|
}
|
|
if !strings.HasPrefix(string(got), prefix) {
|
|
t.Errorf("%q didn't start with prefix %q", got, prefix)
|
|
}
|
|
if reflect.ValueOf(out).Elem().Interface() != in {
|
|
t.Errorf("mismatch after unmarshal")
|
|
}
|
|
}
|
|
|
|
func TestCloneUser(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
u *User
|
|
}{
|
|
{"nil_logins", &User{}},
|
|
{"zero_logins", &User{Logins: make([]LoginID, 0)}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
u2 := tt.u.Clone()
|
|
if !reflect.DeepEqual(tt.u, u2) {
|
|
t.Errorf("not equal")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCloneNode(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
v *Node
|
|
}{
|
|
{"nil_fields", &Node{}},
|
|
{"zero_fields", &Node{
|
|
Addresses: make([]netaddr.IPPrefix, 0),
|
|
AllowedIPs: make([]netaddr.IPPrefix, 0),
|
|
Endpoints: make([]string, 0),
|
|
}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
v2 := tt.v.Clone()
|
|
if !reflect.DeepEqual(tt.v, v2) {
|
|
t.Errorf("not equal")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserProfileJSONMarshalForMac(t *testing.T) {
|
|
// Old macOS clients had a bug where they required
|
|
// UserProfile.Roles to be non-null. Lock that in
|
|
// 1.0.x/1.2.x clients are gone in the wild.
|
|
// See mac commit 0242c08a2ca496958027db1208f44251bff8488b (Sep 30).
|
|
// It was fixed in at least 1.4.x, and perhaps 1.2.x.
|
|
j, err := json.Marshal(UserProfile{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const wantSub = `"Roles":[]`
|
|
if !strings.Contains(string(j), wantSub) {
|
|
t.Fatalf("didn't contain %#q; got: %s", wantSub, j)
|
|
}
|
|
|
|
// And back:
|
|
var up UserProfile
|
|
if err := json.Unmarshal(j, &up); err != nil {
|
|
t.Fatalf("Unmarshal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEndpointTypeMarshal(t *testing.T) {
|
|
eps := []EndpointType{
|
|
EndpointUnknownType,
|
|
EndpointLocal,
|
|
EndpointSTUN,
|
|
EndpointPortmapped,
|
|
EndpointSTUN4LocalPort,
|
|
}
|
|
got, err := json.Marshal(eps)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const want = `[0,1,2,3,4]`
|
|
if string(got) != want {
|
|
t.Errorf("got %s; want %s", got, want)
|
|
}
|
|
}
|