mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
635 lines
12 KiB
Go
635 lines
12 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 ipn
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"inet.af/netaddr"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/types/persist"
|
|
"tailscale.com/types/preftype"
|
|
"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 TestPrefsEqual(t *testing.T) {
|
|
tstest.PanicOnLog()
|
|
|
|
prefsHandles := []string{
|
|
"ControlURL",
|
|
"RouteAll",
|
|
"AllowSingleHosts",
|
|
"ExitNodeID",
|
|
"ExitNodeIP",
|
|
"ExitNodeAllowLANAccess",
|
|
"CorpDNS",
|
|
"WantRunning",
|
|
"LoggedOut",
|
|
"ShieldsUp",
|
|
"AdvertiseTags",
|
|
"Hostname",
|
|
"OSVersion",
|
|
"DeviceModel",
|
|
"NotepadURLs",
|
|
"ForceDaemon",
|
|
"AdvertiseRoutes",
|
|
"NoSNAT",
|
|
"NetfilterMode",
|
|
"OperatorUser",
|
|
"Persist",
|
|
}
|
|
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
|
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
|
have, prefsHandles)
|
|
}
|
|
|
|
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 *Prefs
|
|
want bool
|
|
}{
|
|
{
|
|
&Prefs{},
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
nil,
|
|
&Prefs{},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{},
|
|
&Prefs{},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{ControlURL: "https://login.tailscale.com"},
|
|
&Prefs{ControlURL: "https://login.private.co"},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ControlURL: "https://login.tailscale.com"},
|
|
&Prefs{ControlURL: "https://login.tailscale.com"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{RouteAll: true},
|
|
&Prefs{RouteAll: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{RouteAll: true},
|
|
&Prefs{RouteAll: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{AllowSingleHosts: true},
|
|
&Prefs{AllowSingleHosts: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AllowSingleHosts: true},
|
|
&Prefs{AllowSingleHosts: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{ExitNodeID: "n1234"},
|
|
&Prefs{},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ExitNodeID: "n1234"},
|
|
&Prefs{ExitNodeID: "n1234"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
|
&Prefs{},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
|
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{},
|
|
&Prefs{ExitNodeAllowLANAccess: true},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ExitNodeAllowLANAccess: true},
|
|
&Prefs{ExitNodeAllowLANAccess: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{CorpDNS: true},
|
|
&Prefs{CorpDNS: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{CorpDNS: true},
|
|
&Prefs{CorpDNS: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{WantRunning: true},
|
|
&Prefs{WantRunning: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{WantRunning: true},
|
|
&Prefs{WantRunning: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{NoSNAT: true},
|
|
&Prefs{NoSNAT: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{NoSNAT: true},
|
|
&Prefs{NoSNAT: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{Hostname: "android-host01"},
|
|
&Prefs{Hostname: "android-host02"},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{Hostname: ""},
|
|
&Prefs{Hostname: ""},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{NotepadURLs: true},
|
|
&Prefs{NotepadURLs: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{NotepadURLs: true},
|
|
&Prefs{NotepadURLs: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{ShieldsUp: true},
|
|
&Prefs{ShieldsUp: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ShieldsUp: true},
|
|
&Prefs{ShieldsUp: true},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{AdvertiseRoutes: nil},
|
|
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
|
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseRoutes: nets("192.168.0.0/24", "10.1.0.0/16")},
|
|
&Prefs{AdvertiseRoutes: nets("192.168.1.0/24", "10.2.0.0/16")},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseRoutes: nets("192.168.0.0/24", "10.1.0.0/16")},
|
|
&Prefs{AdvertiseRoutes: nets("192.168.0.0/24", "10.2.0.0/16")},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseRoutes: nets("192.168.0.0/24", "10.1.0.0/16")},
|
|
&Prefs{AdvertiseRoutes: nets("192.168.0.0/24", "10.1.0.0/16")},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{NetfilterMode: preftype.NetfilterOff},
|
|
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
|
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{Persist: &persist.Persist{}},
|
|
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
|
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
|
true,
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.a.Equals(tt.b)
|
|
if got != tt.want {
|
|
t.Errorf("%d. Equal = %v; want %v", i, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkPrefs(t *testing.T, p Prefs) {
|
|
var err error
|
|
var p2, p2c *Prefs
|
|
var p2b *Prefs
|
|
|
|
pp := p.Pretty()
|
|
if pp == "" {
|
|
t.Fatalf("default p.Pretty() failed\n")
|
|
}
|
|
t.Logf("\npp: %#v\n", pp)
|
|
b := p.ToBytes()
|
|
if len(b) == 0 {
|
|
t.Fatalf("default p.ToBytes() failed\n")
|
|
}
|
|
if !p.Equals(&p) {
|
|
t.Fatalf("p != p\n")
|
|
}
|
|
p2 = p.Clone()
|
|
p2.RouteAll = true
|
|
if p.Equals(p2) {
|
|
t.Fatalf("p == p2\n")
|
|
}
|
|
p2b, err = PrefsFromBytes(p2.ToBytes(), false)
|
|
if err != nil {
|
|
t.Fatalf("PrefsFromBytes(p2) failed\n")
|
|
}
|
|
p2p := p2.Pretty()
|
|
p2bp := p2b.Pretty()
|
|
t.Logf("\np2p: %#v\np2bp: %#v\n", p2p, p2bp)
|
|
if p2p != p2bp {
|
|
t.Fatalf("p2p != p2bp\n%#v\n%#v\n", p2p, p2bp)
|
|
}
|
|
if !p2.Equals(p2b) {
|
|
t.Fatalf("p2 != p2b\n%#v\n%#v\n", p2, p2b)
|
|
}
|
|
p2c = p2.Clone()
|
|
if !p2b.Equals(p2c) {
|
|
t.Fatalf("p2b != p2c\n")
|
|
}
|
|
}
|
|
|
|
func TestBasicPrefs(t *testing.T) {
|
|
tstest.PanicOnLog()
|
|
|
|
p := Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
}
|
|
checkPrefs(t, p)
|
|
}
|
|
|
|
func TestPrefsPersist(t *testing.T) {
|
|
tstest.PanicOnLog()
|
|
|
|
c := persist.Persist{
|
|
LoginName: "test@example.com",
|
|
}
|
|
p := Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
Persist: &c,
|
|
}
|
|
checkPrefs(t, p)
|
|
}
|
|
|
|
func TestPrefsPretty(t *testing.T) {
|
|
tests := []struct {
|
|
p Prefs
|
|
os string
|
|
want string
|
|
}{
|
|
{
|
|
Prefs{},
|
|
"linux",
|
|
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{},
|
|
"windows",
|
|
"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{ShieldsUp: true},
|
|
"windows",
|
|
"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{AllowSingleHosts: true},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{
|
|
NotepadURLs: true,
|
|
AllowSingleHosts: true,
|
|
},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{
|
|
AllowSingleHosts: true,
|
|
WantRunning: true,
|
|
ForceDaemon: true, // server mode
|
|
},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=true server=true Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{
|
|
AllowSingleHosts: true,
|
|
WantRunning: true,
|
|
ControlURL: "http://localhost:1234",
|
|
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
|
},
|
|
"darwin",
|
|
`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
Persist: &persist.Persist{},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
Persist: &persist.Persist{
|
|
PrivateNodeKey: wgkey.Private{1: 1},
|
|
},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeIP: netaddr.MustParseIP("1.2.3.4"),
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
|
ExitNodeAllowLANAccess: true,
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeAllowLANAccess: true,
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
Hostname: "foo",
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" Persist=nil}`,
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.p.pretty(tt.os)
|
|
if got != tt.want {
|
|
t.Errorf("%d. wrong String:\n got: %s\nwant: %s\n", i, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadPrefsNotExist(t *testing.T) {
|
|
bogusFile := fmt.Sprintf("/tmp/not-exist-%d", time.Now().UnixNano())
|
|
|
|
p, err := LoadPrefs(bogusFile)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// expected.
|
|
return
|
|
}
|
|
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
|
}
|
|
|
|
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
|
|
// See issue #954 for details.
|
|
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
|
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := f.Name()
|
|
if _, err := f.Write(jsonEscapedZero); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.Close()
|
|
defer os.Remove(path)
|
|
|
|
p, err := LoadPrefs(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// expected.
|
|
return
|
|
}
|
|
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
|
}
|
|
|
|
func TestMaskedPrefsFields(t *testing.T) {
|
|
have := map[string]bool{}
|
|
for _, f := range fieldsOf(reflect.TypeOf(Prefs{})) {
|
|
if f == "Persist" {
|
|
// This one can't be edited.
|
|
continue
|
|
}
|
|
have[f] = true
|
|
}
|
|
for _, f := range fieldsOf(reflect.TypeOf(MaskedPrefs{})) {
|
|
if f == "Prefs" {
|
|
continue
|
|
}
|
|
if !strings.HasSuffix(f, "Set") {
|
|
t.Errorf("unexpected non-/Set$/ field %q", f)
|
|
continue
|
|
}
|
|
bare := strings.TrimSuffix(f, "Set")
|
|
_, ok := have[bare]
|
|
if !ok {
|
|
t.Errorf("no corresponding Prefs.%s field for MaskedPrefs.%s", bare, f)
|
|
continue
|
|
}
|
|
delete(have, bare)
|
|
}
|
|
for f := range have {
|
|
t.Errorf("missing MaskedPrefs.%sSet for Prefs.%s", f, f)
|
|
}
|
|
|
|
// And also make sure they line up in the right order, which
|
|
// ApplyEdits assumes.
|
|
pt := reflect.TypeOf(Prefs{})
|
|
mt := reflect.TypeOf(MaskedPrefs{})
|
|
for i := 0; i < mt.NumField(); i++ {
|
|
name := mt.Field(i).Name
|
|
if i == 0 {
|
|
if name != "Prefs" {
|
|
t.Errorf("first field of MaskedPrefs should be Prefs")
|
|
}
|
|
continue
|
|
}
|
|
prefName := pt.Field(i - 1).Name
|
|
if prefName+"Set" != name {
|
|
t.Errorf("MaskedField[%d] = %s; want %sSet", i-1, name, prefName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPrefsApplyEdits(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
prefs *Prefs
|
|
edit *MaskedPrefs
|
|
want *Prefs
|
|
}{
|
|
{
|
|
name: "no_change",
|
|
prefs: &Prefs{
|
|
Hostname: "foo",
|
|
},
|
|
edit: &MaskedPrefs{},
|
|
want: &Prefs{
|
|
Hostname: "foo",
|
|
},
|
|
},
|
|
{
|
|
name: "set1_decoy1",
|
|
prefs: &Prefs{
|
|
Hostname: "foo",
|
|
},
|
|
edit: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
Hostname: "bar",
|
|
DeviceModel: "ignore-this", // not set
|
|
},
|
|
HostnameSet: true,
|
|
},
|
|
want: &Prefs{
|
|
Hostname: "bar",
|
|
},
|
|
},
|
|
{
|
|
name: "set_several",
|
|
prefs: &Prefs{},
|
|
edit: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
Hostname: "bar",
|
|
DeviceModel: "galaxybrain",
|
|
},
|
|
HostnameSet: true,
|
|
DeviceModelSet: true,
|
|
},
|
|
want: &Prefs{
|
|
Hostname: "bar",
|
|
DeviceModel: "galaxybrain",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.prefs.Clone()
|
|
got.ApplyEdits(tt.edit)
|
|
if !got.Equals(tt.want) {
|
|
gotj, _ := json.Marshal(got)
|
|
wantj, _ := json.Marshal(tt.want)
|
|
t.Errorf("fail.\n got: %s\nwant: %s\n", gotj, wantj)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMaskedPrefsPretty(t *testing.T) {
|
|
tests := []struct {
|
|
m *MaskedPrefs
|
|
want string
|
|
}{
|
|
{
|
|
m: &MaskedPrefs{},
|
|
want: "MaskedPrefs{}",
|
|
},
|
|
{
|
|
m: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
Hostname: "bar",
|
|
DeviceModel: "galaxybrain",
|
|
AllowSingleHosts: true,
|
|
RouteAll: false,
|
|
},
|
|
RouteAllSet: true,
|
|
HostnameSet: true,
|
|
DeviceModelSet: true,
|
|
},
|
|
want: `MaskedPrefs{RouteAll=false Hostname="bar" DeviceModel="galaxybrain"}`,
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.m.Pretty()
|
|
if got != tt.want {
|
|
t.Errorf("%d.\n got: %#q\nwant: %#q\n", i, got, tt.want)
|
|
}
|
|
}
|
|
}
|