mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-09 02:53:09 -05:00
Add an ipn.Prefs.Tailnet field, passed through controlclient.Options into tailcfg.RegisterRequest.Tailnet, so a client can recommend or require a tailnet at registration (e.g. to skip the tailnet picker when the intent is already known) without impersonating MDM. Previously this value could only come from the "Tailnet" syspolicy setting. That setting still takes precedence as an administrative override; the prefs value is used only when no policy is set. Updates #22093 Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
1299 lines
28 KiB
Go
1299 lines
28 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipn
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"go4.org/mem"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/net/netaddr"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/opt"
|
|
"tailscale.com/types/persist"
|
|
"tailscale.com/types/preftype"
|
|
"tailscale.com/util/syspolicy/policyclient"
|
|
)
|
|
|
|
func fieldsOf(t reflect.Type) (fields []string) {
|
|
for field := range t.Fields() {
|
|
fields = append(fields, field.Name)
|
|
}
|
|
return
|
|
}
|
|
|
|
func TestPrefsEqual(t *testing.T) {
|
|
tstest.PanicOnLog()
|
|
|
|
prefsHandles := []string{
|
|
"ControlURL",
|
|
"RouteAll",
|
|
"ExitNodeID",
|
|
"ExitNodeIP",
|
|
"AutoExitNode",
|
|
"InternalExitNodePrior",
|
|
"ExitNodeAllowLANAccess",
|
|
"CorpDNS",
|
|
"RunSSH",
|
|
"RunWebClient",
|
|
"WantRunning",
|
|
"LoggedOut",
|
|
"ShieldsUp",
|
|
"AdvertiseTags",
|
|
"Hostname",
|
|
"NotepadURLs",
|
|
"ForceDaemon",
|
|
"Egg",
|
|
"AdvertiseRoutes",
|
|
"AdvertiseServices",
|
|
"Sync",
|
|
"NoSNAT",
|
|
"NoStatefulFiltering",
|
|
"NetfilterMode",
|
|
"OperatorUser",
|
|
"ProfileName",
|
|
"AutoUpdate",
|
|
"AppConnector",
|
|
"PostureChecking",
|
|
"NetfilterKind",
|
|
"Tailnet",
|
|
"RemoteConfig",
|
|
"DriveShares",
|
|
"RelayServerPort",
|
|
"RelayServerStaticEndpoints",
|
|
"Persist",
|
|
}
|
|
if have := fieldsOf(reflect.TypeFor[Prefs]()); !reflect.DeepEqual(have, prefsHandles) {
|
|
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
|
have, prefsHandles)
|
|
}
|
|
|
|
relayServerPort := func(port uint16) *uint16 {
|
|
return &port
|
|
}
|
|
nets := func(strs ...string) (ns []netip.Prefix) {
|
|
for _, s := range strs {
|
|
n, err := netip.ParsePrefix(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ns = append(ns, n)
|
|
}
|
|
return ns
|
|
}
|
|
aps := func(strs ...string) (ret []netip.AddrPort) {
|
|
for _, s := range strs {
|
|
n, err := netip.ParseAddrPort(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ret = append(ret, n)
|
|
}
|
|
return ret
|
|
}
|
|
tests := []struct {
|
|
a, b *Prefs
|
|
want bool
|
|
}{
|
|
{
|
|
&Prefs{},
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
nil,
|
|
&Prefs{},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{},
|
|
&Prefs{},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{ControlURL: "https://controlplane.tailscale.com"},
|
|
&Prefs{ControlURL: "https://login.private.co"},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ControlURL: "https://controlplane.tailscale.com"},
|
|
&Prefs{ControlURL: "https://controlplane.tailscale.com"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{RouteAll: true},
|
|
&Prefs{RouteAll: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{RouteAll: true},
|
|
&Prefs{RouteAll: true},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{ExitNodeID: "n1234"},
|
|
&Prefs{},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ExitNodeID: "n1234"},
|
|
&Prefs{ExitNodeID: "n1234"},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{ExitNodeIP: netip.MustParseAddr("1.2.3.4")},
|
|
&Prefs{},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{ExitNodeIP: netip.MustParseAddr("1.2.3.4")},
|
|
&Prefs{ExitNodeIP: netip.MustParseAddr("1.2.3.4")},
|
|
true,
|
|
},
|
|
|
|
{
|
|
&Prefs{AutoExitNode: ""},
|
|
&Prefs{AutoExitNode: "auto:any"},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AutoExitNode: "auto:any"},
|
|
&Prefs{AutoExitNode: "auto:any"},
|
|
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: []netip.Prefix{}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseRoutes: []netip.Prefix{}},
|
|
&Prefs{AdvertiseRoutes: []netip.Prefix{}},
|
|
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{
|
|
UserProfile: tailcfg.UserProfile{LoginName: "dave"},
|
|
}},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{Persist: &persist.Persist{
|
|
UserProfile: tailcfg.UserProfile{LoginName: "dave"},
|
|
}},
|
|
&Prefs{Persist: &persist.Persist{
|
|
UserProfile: tailcfg.UserProfile{LoginName: "dave"},
|
|
}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{ProfileName: "work"},
|
|
&Prefs{ProfileName: "work"},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{ProfileName: "work"},
|
|
&Prefs{ProfileName: "home"},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
|
|
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: false, Apply: opt.NewBool(false)}},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)}},
|
|
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
|
|
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{AppConnector: AppConnectorPrefs{Advertise: true}},
|
|
&Prefs{AppConnector: AppConnectorPrefs{Advertise: true}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{AppConnector: AppConnectorPrefs{Advertise: true}},
|
|
&Prefs{AppConnector: AppConnectorPrefs{Advertise: false}},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{PostureChecking: true},
|
|
&Prefs{PostureChecking: true},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{PostureChecking: true},
|
|
&Prefs{PostureChecking: false},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{NetfilterKind: "iptables"},
|
|
&Prefs{NetfilterKind: "iptables"},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{NetfilterKind: "nftables"},
|
|
&Prefs{NetfilterKind: ""},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{Tailnet: "required:tn123"},
|
|
&Prefs{Tailnet: "required:tn123"},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{Tailnet: "required:tn123"},
|
|
&Prefs{Tailnet: "tn123"},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:amelie"}},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{RelayServerPort: relayServerPort(0)},
|
|
&Prefs{RelayServerPort: nil},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{RelayServerPort: relayServerPort(0)},
|
|
&Prefs{RelayServerPort: relayServerPort(1)},
|
|
false,
|
|
},
|
|
{
|
|
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.1:40000")},
|
|
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.1:40000")},
|
|
true,
|
|
},
|
|
{
|
|
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.2:40000")},
|
|
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.1:40000")},
|
|
false,
|
|
},
|
|
}
|
|
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 = new(Prefs)
|
|
err = PrefsFromBytes(p2.ToBytes(), p2b)
|
|
if err != nil {
|
|
t.Fatalf("PrefsFromBytes(p2) failed: bytes=%q; err=%v\n", p2.ToBytes(), err)
|
|
}
|
|
p2b.normalizeOptBools()
|
|
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")
|
|
}
|
|
}
|
|
|
|
// PrefsFromBytes documents that it preserves fields unset in the JSON.
|
|
// This verifies that stays true.
|
|
func TestPrefsFromBytesPreservesOldValues(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
old Prefs
|
|
json []byte
|
|
want Prefs
|
|
}{
|
|
{
|
|
name: "preserve-control-url",
|
|
old: Prefs{ControlURL: "https://foo"},
|
|
json: []byte(`{"RouteAll": true}`),
|
|
want: Prefs{ControlURL: "https://foo", RouteAll: true},
|
|
},
|
|
{
|
|
name: "opt-Bool", // test that we don't normalize it early
|
|
old: Prefs{Sync: "unset"},
|
|
json: []byte(`{}`),
|
|
want: Prefs{Sync: "unset"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
old := tt.old // shallow
|
|
err := PrefsFromBytes(tt.json, &old)
|
|
if err != nil {
|
|
t.Fatalf("PrefsFromBytes failed: %v", err)
|
|
}
|
|
if !old.Equals(&tt.want) {
|
|
t.Fatalf("got %+v; want %+v", old, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBasicPrefs(t *testing.T) {
|
|
tstest.PanicOnLog()
|
|
|
|
p := Prefs{
|
|
ControlURL: "https://controlplane.tailscale.com",
|
|
}
|
|
checkPrefs(t, p)
|
|
}
|
|
|
|
func TestPrefsPersist(t *testing.T) {
|
|
tstest.PanicOnLog()
|
|
|
|
c := persist.Persist{
|
|
UserProfile: tailcfg.UserProfile{
|
|
LoginName: "test@example.com",
|
|
},
|
|
}
|
|
p := Prefs{
|
|
ControlURL: "https://controlplane.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 dns=false want=false routes=[] nf=off update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{ShieldsUp: true},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false shields=true update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{RemoteConfig: true},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false remoteconfig=true update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{
|
|
NotepadURLs: true,
|
|
},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=false notepad=true update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{
|
|
WantRunning: true,
|
|
ForceDaemon: true, // server mode
|
|
},
|
|
"windows",
|
|
"Prefs{ra=false dns=false want=true server=true update=off Persist=nil}",
|
|
},
|
|
{
|
|
Prefs{
|
|
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" update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
Persist: &persist.Persist{
|
|
PrivateNodeKey: key.NodePrivateFromRaw32(mem.B([]byte{1: 1, 31: 0})),
|
|
},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u="" ak=-}}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeIP: netip.MustParseAddr("1.2.3.4"),
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
|
ExitNodeAllowLANAccess: true,
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
ExitNodeAllowLANAccess: true,
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
Hostname: "foo",
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off host="foo" update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
AutoUpdate: AutoUpdatePrefs{
|
|
Check: true,
|
|
Apply: opt.NewBool(false),
|
|
},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=check Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
AutoUpdate: AutoUpdatePrefs{
|
|
Check: true,
|
|
Apply: opt.NewBool(true),
|
|
},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=on Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
AppConnector: AppConnectorPrefs{
|
|
Advertise: true,
|
|
},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off appconnector=advertise Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
AppConnector: AppConnectorPrefs{
|
|
Advertise: false,
|
|
},
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
NetfilterKind: "iptables",
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off netfilterKind=iptables update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{
|
|
NetfilterKind: "",
|
|
},
|
|
"linux",
|
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
|
},
|
|
{
|
|
Prefs{Sync: "false"},
|
|
"linux",
|
|
"Prefs{ra=false dns=false want=false sync=false routes=[] nf=off update=off 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 := LoadPrefsWindows(bogusFile)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// expected.
|
|
return
|
|
}
|
|
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
|
}
|
|
|
|
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs handles corrupted input files.
|
|
// See issue #954 for details.
|
|
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
|
f, err := os.CreateTemp("", "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 := LoadPrefsWindows(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// expected.
|
|
return
|
|
}
|
|
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
|
}
|
|
|
|
func TestMaskedPrefsSetsInternal(t *testing.T) {
|
|
for _, f := range fieldsOf(reflect.TypeFor[MaskedPrefs]()) {
|
|
if !strings.HasSuffix(f, "Set") || !strings.HasPrefix(f, "Internal") {
|
|
continue
|
|
}
|
|
mp := new(MaskedPrefs)
|
|
reflect.ValueOf(mp).Elem().FieldByName(f).SetBool(true)
|
|
if !mp.SetsInternal() {
|
|
t.Errorf("MaskedPrefs.%sSet=true but SetsInternal=false", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMaskedPrefsFields(t *testing.T) {
|
|
have := map[string]bool{}
|
|
for _, f := range fieldsOf(reflect.TypeFor[Prefs]()) {
|
|
switch f {
|
|
case "Persist":
|
|
// These can't be edited.
|
|
continue
|
|
}
|
|
have[f] = true
|
|
}
|
|
for _, f := range fieldsOf(reflect.TypeFor[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.TypeFor[Prefs]()
|
|
mt := reflect.TypeFor[MaskedPrefs]()
|
|
for i := range mt.NumField() {
|
|
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",
|
|
OperatorUser: "ignore-this", // not set
|
|
},
|
|
HostnameSet: true,
|
|
},
|
|
want: &Prefs{
|
|
Hostname: "bar",
|
|
},
|
|
},
|
|
{
|
|
name: "set_several",
|
|
prefs: &Prefs{},
|
|
edit: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
Hostname: "bar",
|
|
OperatorUser: "galaxybrain",
|
|
},
|
|
HostnameSet: true,
|
|
OperatorUserSet: true,
|
|
},
|
|
want: &Prefs{
|
|
Hostname: "bar",
|
|
OperatorUser: "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",
|
|
OperatorUser: "galaxybrain",
|
|
RouteAll: false,
|
|
ExitNodeID: "foo",
|
|
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
|
NetfilterMode: preftype.NetfilterNoDivert,
|
|
},
|
|
RouteAllSet: true,
|
|
HostnameSet: true,
|
|
OperatorUserSet: true,
|
|
ExitNodeIDSet: true,
|
|
AdvertiseTagsSet: true,
|
|
NetfilterModeSet: true,
|
|
},
|
|
want: `MaskedPrefs{RouteAll=false ExitNodeID="foo" AdvertiseTags=["tag:foo" "tag:bar"] Hostname="bar" NetfilterMode=nodivert OperatorUser="galaxybrain"}`,
|
|
},
|
|
{
|
|
m: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
ExitNodeIP: netaddr.IPv4(100, 102, 104, 105),
|
|
},
|
|
ExitNodeIPSet: true,
|
|
},
|
|
want: `MaskedPrefs{ExitNodeIP=100.102.104.105}`,
|
|
},
|
|
{
|
|
m: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)},
|
|
},
|
|
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: false},
|
|
},
|
|
want: `MaskedPrefs{AutoUpdate={Check=true}}`,
|
|
},
|
|
{
|
|
m: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)},
|
|
},
|
|
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: true},
|
|
},
|
|
want: `MaskedPrefs{AutoUpdate={Check=true Apply=true}}`,
|
|
},
|
|
{
|
|
m: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)},
|
|
},
|
|
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: true},
|
|
},
|
|
want: `MaskedPrefs{AutoUpdate={Apply=false}}`,
|
|
},
|
|
{
|
|
m: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)},
|
|
},
|
|
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: false},
|
|
},
|
|
want: `MaskedPrefs{}`,
|
|
},
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPrefsExitNode(t *testing.T) {
|
|
var p *Prefs
|
|
if p.AdvertisesExitNode() {
|
|
t.Errorf("nil shouldn't advertise exit node")
|
|
}
|
|
p = NewPrefs()
|
|
if p.AdvertisesExitNode() {
|
|
t.Errorf("default shouldn't advertise exit node")
|
|
}
|
|
p.AdvertiseRoutes = []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.0.0/16"),
|
|
}
|
|
p.SetAdvertiseExitNode(true)
|
|
if got, want := len(p.AdvertiseRoutes), 3; got != want {
|
|
t.Errorf("routes = %d; want %d", got, want)
|
|
}
|
|
p.SetAdvertiseExitNode(true)
|
|
if got, want := len(p.AdvertiseRoutes), 3; got != want {
|
|
t.Errorf("routes = %d; want %d", got, want)
|
|
}
|
|
if !p.AdvertisesExitNode() {
|
|
t.Errorf("not advertising after enable")
|
|
}
|
|
p.SetAdvertiseExitNode(false)
|
|
if p.AdvertisesExitNode() {
|
|
t.Errorf("advertising after disable")
|
|
}
|
|
if got, want := len(p.AdvertiseRoutes), 1; got != want {
|
|
t.Errorf("routes = %d; want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestExitNodeIPOfArg(t *testing.T) {
|
|
mustIP := netip.MustParseAddr
|
|
tests := []struct {
|
|
name string
|
|
arg string
|
|
st *ipnstate.Status
|
|
want netip.Addr
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "ip_while_stopped_okay",
|
|
arg: "1.2.3.4",
|
|
st: &ipnstate.Status{
|
|
BackendState: "Stopped",
|
|
},
|
|
want: mustIP("1.2.3.4"),
|
|
},
|
|
{
|
|
name: "ip_not_found",
|
|
arg: "1.2.3.4",
|
|
st: &ipnstate.Status{
|
|
BackendState: "Running",
|
|
},
|
|
wantErr: `no node found in netmap with IP 1.2.3.4`,
|
|
},
|
|
{
|
|
name: "ip_is_self",
|
|
arg: "1.2.3.4",
|
|
st: &ipnstate.Status{
|
|
TailscaleIPs: []netip.Addr{mustIP("1.2.3.4")},
|
|
},
|
|
wantErr: "cannot use 1.2.3.4 as an exit node as it is a local IP address to this machine",
|
|
},
|
|
{
|
|
name: "ip_is_self_when_backend_running",
|
|
arg: "1.2.3.4",
|
|
st: &ipnstate.Status{
|
|
BackendState: "Running",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.2.3.4")},
|
|
},
|
|
wantErr: "cannot use 1.2.3.4 as an exit node as it is a local IP address to this machine",
|
|
},
|
|
{
|
|
name: "ip_not_exit",
|
|
arg: "1.2.3.4",
|
|
st: &ipnstate.Status{
|
|
BackendState: "Running",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
TailscaleIPs: []netip.Addr{mustIP("1.2.3.4")},
|
|
},
|
|
},
|
|
},
|
|
wantErr: `node 1.2.3.4 is not advertising an exit node`,
|
|
},
|
|
{
|
|
name: "ip",
|
|
arg: "1.2.3.4",
|
|
st: &ipnstate.Status{
|
|
BackendState: "Running",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
TailscaleIPs: []netip.Addr{mustIP("1.2.3.4")},
|
|
ExitNodeOption: true,
|
|
},
|
|
},
|
|
},
|
|
want: mustIP("1.2.3.4"),
|
|
},
|
|
{
|
|
name: "no_match",
|
|
arg: "unknown",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
ExitNodeOption: true,
|
|
},
|
|
},
|
|
},
|
|
wantErr: `invalid value "unknown" for --exit-node; must be IP or peer hostname`,
|
|
},
|
|
{
|
|
name: "name",
|
|
arg: "skippy",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
ExitNodeOption: true,
|
|
},
|
|
},
|
|
},
|
|
want: mustIP("1.0.0.2"),
|
|
},
|
|
{
|
|
name: "name_fqdn",
|
|
arg: "skippy.foo.",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
ExitNodeOption: true,
|
|
},
|
|
},
|
|
},
|
|
want: mustIP("1.0.0.2"),
|
|
},
|
|
{
|
|
name: "name_fqdn_sans_dot",
|
|
arg: "skippy.foo",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
ExitNodeOption: true,
|
|
},
|
|
},
|
|
},
|
|
want: mustIP("1.0.0.2"),
|
|
},
|
|
{
|
|
name: "hostname_no_peer",
|
|
arg: "skippy.foo",
|
|
st: &ipnstate.Status{},
|
|
wantErr: `cannot resolve exit node by hostname while Tailscale is starting up; please use its Tailscale IP address instead`,
|
|
},
|
|
{
|
|
name: "name_not_exit",
|
|
arg: "skippy",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
},
|
|
},
|
|
},
|
|
wantErr: `node "skippy" is not advertising an exit node`,
|
|
},
|
|
{
|
|
name: "name_wrong_fqdn",
|
|
arg: "skippy.bar.",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
},
|
|
},
|
|
},
|
|
wantErr: `invalid value "skippy.bar." for --exit-node; must be IP or peer hostname`,
|
|
},
|
|
{
|
|
name: "ambiguous",
|
|
arg: "skippy",
|
|
st: &ipnstate.Status{
|
|
MagicDNSSuffix: ".foo",
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
|
key.NewNode().Public(): {
|
|
DNSName: "skippy.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
ExitNodeOption: true,
|
|
},
|
|
key.NewNode().Public(): {
|
|
DNSName: "SKIPPY.foo.",
|
|
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
|
ExitNodeOption: true,
|
|
},
|
|
},
|
|
},
|
|
wantErr: `ambiguous exit node name "skippy"`,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := exitNodeIPOfArg(tt.arg, tt.st)
|
|
if err != nil {
|
|
if err.Error() == tt.wantErr {
|
|
return
|
|
}
|
|
if tt.wantErr == "" {
|
|
t.Fatal(err)
|
|
}
|
|
t.Fatalf("error = %#q; want %#q", err, tt.wantErr)
|
|
}
|
|
if tt.wantErr != "" {
|
|
t.Fatalf("got %v; want error %#q", got, tt.wantErr)
|
|
}
|
|
if got != tt.want {
|
|
t.Fatalf("got %v; want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestControlURLOrDefault(t *testing.T) {
|
|
var p Prefs
|
|
polc := policyclient.NoPolicyClient{}
|
|
if got, want := p.ControlURLOrDefault(polc), DefaultControlURL; got != want {
|
|
t.Errorf("got %q; want %q", got, want)
|
|
}
|
|
p.ControlURL = "http://foo.bar"
|
|
if got, want := p.ControlURLOrDefault(polc), "http://foo.bar"; got != want {
|
|
t.Errorf("got %q; want %q", got, want)
|
|
}
|
|
p.ControlURL = "https://login.tailscale.com"
|
|
if got, want := p.ControlURLOrDefault(polc), DefaultControlURL; got != want {
|
|
t.Errorf("got %q; want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestMaskedPrefsIsEmpty(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mp *MaskedPrefs
|
|
wantEmpty bool
|
|
}{
|
|
{
|
|
name: "nil",
|
|
wantEmpty: true,
|
|
},
|
|
{
|
|
name: "empty",
|
|
wantEmpty: true,
|
|
mp: &MaskedPrefs{},
|
|
},
|
|
{
|
|
name: "no-masks",
|
|
wantEmpty: true,
|
|
mp: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
WantRunning: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "with-mask",
|
|
wantEmpty: false,
|
|
mp: &MaskedPrefs{
|
|
Prefs: Prefs{
|
|
WantRunning: true,
|
|
},
|
|
WantRunningSet: true,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := tc.mp.IsEmpty()
|
|
if got != tc.wantEmpty {
|
|
t.Fatalf("mp.IsEmpty = %t; want %t", got, tc.wantEmpty)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNotifyPrefsJSONRoundtrip(t *testing.T) {
|
|
var n Notify
|
|
if n.Prefs != nil && n.Prefs.Valid() {
|
|
t.Fatal("Prefs should not be valid at start")
|
|
}
|
|
b, err := json.Marshal(n)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var n2 Notify
|
|
if err := json.Unmarshal(b, &n2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n2.Prefs != nil && n2.Prefs.Valid() {
|
|
t.Fatal("Prefs should not be valid after deserialization")
|
|
}
|
|
}
|
|
|
|
func TestParseAutoExitNodeString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
exitNodeID string
|
|
wantOk bool
|
|
wantExpr ExitNodeExpression
|
|
}{
|
|
{
|
|
name: "empty-expr",
|
|
exitNodeID: "",
|
|
wantOk: false,
|
|
wantExpr: "",
|
|
},
|
|
{
|
|
name: "no-auto-prefix",
|
|
exitNodeID: "foo",
|
|
wantOk: false,
|
|
wantExpr: "",
|
|
},
|
|
{
|
|
name: "auto:any",
|
|
exitNodeID: "auto:any",
|
|
wantOk: true,
|
|
wantExpr: AnyExitNode,
|
|
},
|
|
{
|
|
name: "auto:foo",
|
|
exitNodeID: "auto:foo",
|
|
wantOk: true,
|
|
wantExpr: "foo",
|
|
},
|
|
{
|
|
name: "auto-prefix-empty-suffix",
|
|
exitNodeID: "auto:",
|
|
wantOk: false,
|
|
wantExpr: "",
|
|
},
|
|
{
|
|
name: "auto-prefix-no-colon",
|
|
exitNodeID: "auto",
|
|
wantOk: false,
|
|
wantExpr: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotExpr, gotOk := ParseAutoExitNodeString(tt.exitNodeID)
|
|
if gotOk != tt.wantOk || gotExpr != tt.wantExpr {
|
|
if tt.wantOk {
|
|
t.Fatalf("got %v (%q); want %v (%q)", gotOk, gotExpr, tt.wantOk, tt.wantExpr)
|
|
} else {
|
|
t.Fatalf("got %v (%q); want false", gotOk, gotExpr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|