mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-28 20:12:27 +00:00

After some analysis, stateful filtering is only necessary in tailnets that use `autogroup:danger-all` in `src` in ACLs. And in those cases users explicitly specify that hosts outside of the tailnet should be able to reach their nodes. To fix local DNS breakage in containers, we disable stateful filtering by default. Updates #12108 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
1479 lines
45 KiB
Go
1479 lines
45 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
stdcmp "cmp"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"net/netip"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/health/healthmsg"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tka"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/opt"
|
|
"tailscale.com/types/persist"
|
|
"tailscale.com/types/preftype"
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
|
|
envknob.PanicIfAnyEnvCheckedInInit()
|
|
}
|
|
|
|
func TestShortUsage(t *testing.T) {
|
|
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
|
if !envknob.UseWIPCode() {
|
|
t.Fatal("expected envknob.UseWIPCode() to be true")
|
|
}
|
|
|
|
walkCommands(newRootCmd(), func(w cmdWalk) bool {
|
|
c, parents := w.Command, w.parents
|
|
|
|
// Words that we expect to be in the usage.
|
|
words := make([]string, len(parents)+1)
|
|
for i, parent := range parents {
|
|
words[i] = parent.Name
|
|
}
|
|
words[len(parents)] = c.Name
|
|
|
|
// Check the ShortHelp starts with a capital letter.
|
|
if prefix, help := trimPrefixes(c.ShortHelp, "HIDDEN: ", "[ALPHA] ", "[BETA] "); help != "" {
|
|
if 'a' <= help[0] && help[0] <= 'z' {
|
|
if len(help) > 20 {
|
|
help = help[:20] + "…"
|
|
}
|
|
caphelp := string(help[0]-'a'+'A') + help[1:]
|
|
t.Errorf("command: %s: ShortHelp %q should start with a capital letter %q", strings.Join(words, " "), prefix+help, prefix+caphelp)
|
|
}
|
|
}
|
|
|
|
// Check all words appear in the usage.
|
|
usage := c.ShortUsage
|
|
for _, word := range words {
|
|
var ok bool
|
|
usage, ok = cutWord(usage, word)
|
|
if !ok {
|
|
full := strings.Join(words, " ")
|
|
t.Errorf("command: %s: usage %q should contain the full path %q", full, c.ShortUsage, full)
|
|
return true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func trimPrefixes(full string, prefixes ...string) (trimmed, remaining string) {
|
|
s := full
|
|
start:
|
|
for _, p := range prefixes {
|
|
var ok bool
|
|
s, ok = strings.CutPrefix(s, p)
|
|
if ok {
|
|
goto start
|
|
}
|
|
}
|
|
return full[:len(full)-len(s)], s
|
|
}
|
|
|
|
// cutWord("tailscale debug scale 123", "scale") returns (" 123", true).
|
|
func cutWord(s, w string) (after string, ok bool) {
|
|
var p string
|
|
for {
|
|
p, s, ok = strings.Cut(s, w)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
if p != "" && isWordChar(p[len(p)-1]) {
|
|
continue
|
|
}
|
|
if s != "" && isWordChar(s[0]) {
|
|
continue
|
|
}
|
|
return s, true
|
|
}
|
|
}
|
|
|
|
func isWordChar(r byte) bool {
|
|
return r == '_' ||
|
|
('0' <= r && r <= '9') ||
|
|
('A' <= r && r <= 'Z') ||
|
|
('a' <= r && r <= 'z')
|
|
}
|
|
|
|
func TestCutWord(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
word string
|
|
out string
|
|
ok bool
|
|
}{
|
|
{"tailscale debug", "debug", "", true},
|
|
{"tailscale debug", "bug", "", false},
|
|
{"tailscale debug", "tail", "", false},
|
|
{"tailscale debug scaley scale 123", "scale", " 123", true},
|
|
}
|
|
for _, test := range tests {
|
|
out, ok := cutWord(test.in, test.word)
|
|
if out != test.out || ok != test.ok {
|
|
t.Errorf("cutWord(%q, %q) = (%q, %t), wanted (%q, %t)", test.in, test.word, out, ok, test.out, test.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// geese is a collection of gooses. It need not be complete.
|
|
// But it should include anything handled specially (e.g. linux, windows)
|
|
// and at least one thing that's not (darwin, freebsd).
|
|
var geese = []string{"linux", "darwin", "windows", "freebsd"}
|
|
|
|
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
|
|
// all flags. This will panic if a new flag creeps in that's unhandled.
|
|
//
|
|
// Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
|
|
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
|
for _, goos := range geese {
|
|
var upArgs upArgsT
|
|
fs := newUpFlagSet(goos, &upArgs, "up")
|
|
fs.VisitAll(func(f *flag.Flag) {
|
|
mp := new(ipn.MaskedPrefs)
|
|
updateMaskedPrefsFromUpOrSetFlag(mp, f.Name)
|
|
got := mp.Pretty()
|
|
wantEmpty := preflessFlag(f.Name)
|
|
isEmpty := got == "MaskedPrefs{}"
|
|
if isEmpty != wantEmpty {
|
|
t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
flags []string // argv to be parsed by FlagSet
|
|
curPrefs *ipn.Prefs
|
|
|
|
curExitNodeIP netip.Addr
|
|
curUser string // os.Getenv("USER") on the client side
|
|
goos string // empty means "linux"
|
|
distro distro.Distro
|
|
|
|
want string
|
|
}{
|
|
{
|
|
name: "bare_up_means_up",
|
|
flags: []string{},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: false,
|
|
Hostname: "foo",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "losing_hostname",
|
|
flags: []string{"--accept-dns"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: false,
|
|
Hostname: "foo",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
|
|
},
|
|
{
|
|
name: "hostname_changing_explicitly",
|
|
flags: []string{"--hostname=bar"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
Hostname: "foo",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "hostname_changing_empty_explicitly",
|
|
flags: []string{"--hostname="},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
Hostname: "foo",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
// Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
|
|
// a fresh server's initial prefs.
|
|
name: "up_with_default_prefs",
|
|
flags: []string{"--authkey=foosdlkfjskdljf"},
|
|
curPrefs: ipn.NewPrefs(),
|
|
want: "",
|
|
},
|
|
{
|
|
name: "implicit_operator_change",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
OperatorUser: "alice",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
curUser: "eve",
|
|
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
|
|
},
|
|
{
|
|
name: "implicit_operator_matches_shell_user",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
OperatorUser: "alice",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
curUser: "alice",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "error_advertised_routes_exit_node_removed",
|
|
flags: []string{"--advertise-routes=10.0.42.0/24"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.42.0/24"),
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
|
|
},
|
|
{
|
|
name: "advertised_routes_exit_node_removed_explicit",
|
|
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.42.0/24"),
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
|
|
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.42.0/24"),
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "advertise_exit_node", // Issue 1859
|
|
flags: []string{"--advertise-exit-node"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "advertise_exit_node_over_existing_routes",
|
|
flags: []string{"--advertise-exit-node"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("1.2.0.0/16"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
|
},
|
|
{
|
|
name: "advertise_exit_node_over_existing_routes_and_exit_node",
|
|
flags: []string{"--advertise-exit-node"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
netip.MustParsePrefix("1.2.0.0/16"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
|
},
|
|
{
|
|
name: "exit_node_clearing", // Issue 1777
|
|
flags: []string{"--exit-node="},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
|
|
ExitNodeID: "fooID",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "remove_all_implicit",
|
|
flags: []string{"--force-reauth"},
|
|
curPrefs: &ipn.Prefs{
|
|
WantRunning: true,
|
|
ControlURL: ipn.DefaultControlURL,
|
|
RouteAll: true,
|
|
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
|
|
CorpDNS: false,
|
|
ShieldsUp: true,
|
|
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
|
Hostname: "myhostname",
|
|
ForceDaemon: true,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.0.0/16"),
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
},
|
|
NetfilterMode: preftype.NetfilterNoDivert,
|
|
OperatorUser: "alice",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
curUser: "eve",
|
|
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
|
|
},
|
|
{
|
|
name: "remove_all_implicit_except_hostname",
|
|
flags: []string{"--hostname=newhostname"},
|
|
curPrefs: &ipn.Prefs{
|
|
WantRunning: true,
|
|
ControlURL: ipn.DefaultControlURL,
|
|
RouteAll: true,
|
|
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
|
|
CorpDNS: false,
|
|
ShieldsUp: true,
|
|
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
|
Hostname: "myhostname",
|
|
ForceDaemon: true,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.0.0/16"),
|
|
},
|
|
NetfilterMode: preftype.NetfilterNoDivert,
|
|
OperatorUser: "alice",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
curUser: "eve",
|
|
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --netfilter-mode=nodivert --operator=alice --shields-up",
|
|
},
|
|
{
|
|
name: "loggedout_is_implicit",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
LoggedOut: true,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "", // not an error. LoggedOut is implicit.
|
|
},
|
|
{
|
|
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
|
|
// values is able to enable exit nodes without warnings.
|
|
name: "make_windows_exit_node",
|
|
flags: []string{"--advertise-exit-node"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
RouteAll: true,
|
|
|
|
// And assume this no-op accidental pre-1.8 value:
|
|
NoSNAT: true,
|
|
},
|
|
goos: "windows",
|
|
want: "", // not an error
|
|
},
|
|
{
|
|
name: "ignore_netfilter_change_non_linux",
|
|
flags: []string{"--accept-dns"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
|
|
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
|
},
|
|
goos: "openbsd",
|
|
want: "", // not an error
|
|
},
|
|
{
|
|
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
|
|
flags: []string{"--operator=expbits"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
netip.MustParsePrefix("1.2.0.0/16"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
|
},
|
|
{
|
|
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
|
|
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
netip.MustParsePrefix("1.2.0.0/16"),
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
|
|
},
|
|
{
|
|
name: "errors_preserve_explicit_flags",
|
|
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: false,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
|
|
Hostname: "foo",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --auth-key=secretrand --force-reauth=false --reset --hostname=foo",
|
|
},
|
|
{
|
|
name: "error_exit_node_omit_with_ip_pref",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
|
|
ExitNodeIP: netip.MustParseAddr("100.64.5.4"),
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
|
|
},
|
|
{
|
|
name: "error_exit_node_omit_with_id_pref",
|
|
flags: []string{"--hostname=foo"},
|
|
curExitNodeIP: netip.MustParseAddr("100.64.5.7"),
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
|
|
ExitNodeID: "some_stable_id",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
|
},
|
|
{
|
|
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Issue 3480
|
|
flags: []string{"--hostname=foo"},
|
|
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
|
|
ExitNodeAllowLANAccess: true,
|
|
ExitNodeID: "some_stable_id",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
|
|
},
|
|
{
|
|
name: "ignore_login_server_synonym",
|
|
flags: []string{"--login-server=https://controlplane.tailscale.com"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: "", // not an error
|
|
},
|
|
{
|
|
name: "ignore_login_server_synonym_on_other_change",
|
|
flags: []string{"--netfilter-mode=off"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: false,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
|
|
},
|
|
{
|
|
// Issue 3176: on Synology, don't require --accept-routes=false because user
|
|
// might've had an old install, and we don't support --accept-routes anyway.
|
|
name: "synology_permit_omit_accept_routes",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
RouteAll: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
goos: "linux",
|
|
distro: distro.Synology,
|
|
want: "",
|
|
},
|
|
{
|
|
// Same test case as "synology_permit_omit_accept_routes" above, but
|
|
// on non-Synology distro.
|
|
name: "not_synology_dont_permit_omit_accept_routes",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
RouteAll: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
goos: "linux",
|
|
distro: "", // not Synology
|
|
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
|
|
},
|
|
{
|
|
name: "profile_name_ignored_in_up",
|
|
flags: []string{"--hostname=foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
ProfileName: "foo",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
goos: "linux",
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
goos := "linux"
|
|
if tt.goos != "" {
|
|
goos = tt.goos
|
|
}
|
|
var upArgs upArgsT
|
|
flagSet := newUpFlagSet(goos, &upArgs, "up")
|
|
flags := CleanUpArgs(tt.flags)
|
|
flagSet.Parse(flags)
|
|
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
upEnv := upCheckEnv{
|
|
goos: goos,
|
|
flagSet: flagSet,
|
|
curExitNodeIP: tt.curExitNodeIP,
|
|
distro: tt.distro,
|
|
user: tt.curUser,
|
|
}
|
|
applyImplicitPrefs(newPrefs, tt.curPrefs, upEnv)
|
|
var got string
|
|
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upEnv); err != nil {
|
|
got = err.Error()
|
|
}
|
|
if strings.TrimSpace(got) != tt.want {
|
|
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
|
|
fs := newUpFlagSet(goos, &args, "up")
|
|
fs.Parse(flagArgs) // populates args
|
|
return
|
|
}
|
|
|
|
func TestPrefsFromUpArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args upArgsT
|
|
goos string // runtime.GOOS; empty means linux
|
|
st *ipnstate.Status // or nil
|
|
want *ipn.Prefs
|
|
wantErr string
|
|
wantWarn string
|
|
}{
|
|
{
|
|
name: "default_linux",
|
|
goos: "linux",
|
|
args: upArgsFromOSArgs("linux"),
|
|
want: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: true,
|
|
NoSNAT: false,
|
|
NoStatefulFiltering: "true",
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
CorpDNS: true,
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "default_windows",
|
|
goos: "windows",
|
|
args: upArgsFromOSArgs("windows"),
|
|
want: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: true,
|
|
CorpDNS: true,
|
|
RouteAll: true,
|
|
NoSNAT: false,
|
|
NoStatefulFiltering: "true",
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "advertise_default_route",
|
|
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
|
|
want: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: true,
|
|
CorpDNS: true,
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::/0"),
|
|
},
|
|
NoStatefulFiltering: "true",
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "error_advertise_route_invalid_ip",
|
|
args: upArgsT{
|
|
advertiseRoutes: "foo",
|
|
},
|
|
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
|
|
},
|
|
{
|
|
name: "error_advertise_route_unmasked_bits",
|
|
args: upArgsT{
|
|
advertiseRoutes: "1.2.3.4/16",
|
|
},
|
|
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
|
|
},
|
|
{
|
|
name: "error_exit_node_bad_ip",
|
|
args: upArgsT{
|
|
exitNodeIP: "foo",
|
|
},
|
|
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
|
|
},
|
|
{
|
|
name: "error_exit_node_allow_lan_without_exit_node",
|
|
args: upArgsT{
|
|
exitNodeAllowLANAccess: true,
|
|
},
|
|
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
|
|
},
|
|
{
|
|
name: "error_tag_prefix",
|
|
args: upArgsT{
|
|
advertiseTags: "foo",
|
|
},
|
|
wantErr: `tag: "foo": tags must start with 'tag:'`,
|
|
},
|
|
{
|
|
name: "error_long_hostname",
|
|
args: upArgsT{
|
|
hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
|
|
},
|
|
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is too long to be a DNS name`,
|
|
},
|
|
{
|
|
name: "error_long_label",
|
|
args: upArgsT{
|
|
hostname: strings.Repeat("a", 64) + ".example.com",
|
|
},
|
|
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not a valid DNS label`,
|
|
},
|
|
{
|
|
name: "error_linux_netfilter_empty",
|
|
args: upArgsT{
|
|
netfilterMode: "",
|
|
},
|
|
wantErr: `invalid value --netfilter-mode=""`,
|
|
},
|
|
{
|
|
name: "error_linux_netfilter_bogus",
|
|
args: upArgsT{
|
|
netfilterMode: "bogus",
|
|
},
|
|
wantErr: `invalid value --netfilter-mode="bogus"`,
|
|
},
|
|
{
|
|
name: "error_exit_node_ip_is_self_ip",
|
|
args: upArgsT{
|
|
exitNodeIP: "100.105.106.107",
|
|
},
|
|
st: &ipnstate.Status{
|
|
TailscaleIPs: []netip.Addr{netip.MustParseAddr("100.105.106.107")},
|
|
},
|
|
wantErr: `cannot use 100.105.106.107 as an exit node as it is a local IP address to this machine; did you mean --advertise-exit-node?`,
|
|
},
|
|
{
|
|
name: "warn_linux_netfilter_nodivert",
|
|
goos: "linux",
|
|
args: upArgsT{
|
|
netfilterMode: "nodivert",
|
|
},
|
|
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
|
want: &ipn.Prefs{
|
|
WantRunning: true,
|
|
NetfilterMode: preftype.NetfilterNoDivert,
|
|
NoSNAT: true,
|
|
NoStatefulFiltering: "true",
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "warn_linux_netfilter_off",
|
|
goos: "linux",
|
|
args: upArgsT{
|
|
netfilterMode: "off",
|
|
},
|
|
wantWarn: "netfilter=off; configure iptables yourself.",
|
|
want: &ipn.Prefs{
|
|
WantRunning: true,
|
|
NetfilterMode: preftype.NetfilterOff,
|
|
NoSNAT: true,
|
|
NoStatefulFiltering: "true",
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "via_route_good",
|
|
goos: "linux",
|
|
args: upArgsT{
|
|
advertiseRoutes: "fd7a:115c:a1e0:b1a::bb:10.0.0.0/112",
|
|
netfilterMode: "off",
|
|
},
|
|
want: &ipn.Prefs{
|
|
WantRunning: true,
|
|
NoSNAT: true,
|
|
NoStatefulFiltering: "true",
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
|
|
},
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "via_route_good_16_bit",
|
|
goos: "linux",
|
|
args: upArgsT{
|
|
advertiseRoutes: "fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112",
|
|
netfilterMode: "off",
|
|
},
|
|
want: &ipn.Prefs{
|
|
WantRunning: true,
|
|
NoSNAT: true,
|
|
NoStatefulFiltering: "true",
|
|
AdvertiseRoutes: []netip.Prefix{
|
|
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112"),
|
|
},
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "via_route_short_prefix",
|
|
goos: "linux",
|
|
args: upArgsT{
|
|
advertiseRoutes: "fd7a:115c:a1e0:b1a::/64",
|
|
netfilterMode: "off",
|
|
},
|
|
wantErr: "fd7a:115c:a1e0:b1a::/64 4-in-6 prefix must be at least a /96",
|
|
},
|
|
{
|
|
name: "via_route_short_reserved_siteid",
|
|
goos: "linux",
|
|
args: upArgsT{
|
|
advertiseRoutes: "fd7a:115c:a1e0:b1a:1234:5678::/112",
|
|
netfilterMode: "off",
|
|
},
|
|
wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xffff or less",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var warnBuf tstest.MemLogger
|
|
goos := stdcmp.Or(tt.goos, "linux")
|
|
st := tt.st
|
|
if st == nil {
|
|
st = new(ipnstate.Status)
|
|
}
|
|
got, err := prefsFromUpArgs(tt.args, warnBuf.Logf, st, goos)
|
|
gotErr := fmt.Sprint(err)
|
|
if tt.wantErr != "" {
|
|
if tt.wantErr != gotErr {
|
|
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tt.want == nil {
|
|
t.Fatal("tt.want is nil")
|
|
}
|
|
if !got.Equals(tt.want) {
|
|
jgot, _ := json.MarshalIndent(got, "", "\t")
|
|
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
|
|
if bytes.Equal(jgot, jwant) {
|
|
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
|
|
}
|
|
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
|
|
got.Pretty(), tt.want.Pretty(),
|
|
jgot, jwant,
|
|
)
|
|
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestPrefFlagMapping(t *testing.T) {
|
|
prefHasFlag := map[string]bool{}
|
|
for _, pv := range prefsOfFlag {
|
|
for _, pref := range pv {
|
|
prefHasFlag[strings.Split(pref, ".")[0]] = true
|
|
}
|
|
}
|
|
|
|
prefType := reflect.TypeFor[ipn.Prefs]()
|
|
for i := range prefType.NumField() {
|
|
prefName := prefType.Field(i).Name
|
|
if prefHasFlag[prefName] {
|
|
continue
|
|
}
|
|
switch prefName {
|
|
case "AllowSingleHosts":
|
|
// Fake pref for downgrade compat. See #12058.
|
|
continue
|
|
case "WantRunning", "Persist", "LoggedOut":
|
|
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
|
|
continue
|
|
case "OSVersion", "DeviceModel":
|
|
// Only used by Android, which doesn't have a CLI mode anyway, so
|
|
// fine to not map.
|
|
continue
|
|
case "NotepadURLs":
|
|
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
|
|
continue
|
|
case "Egg":
|
|
// Not applicable.
|
|
continue
|
|
case "RunWebClient":
|
|
// TODO(tailscale/corp#14335): Currently behind a feature flag.
|
|
continue
|
|
case "NetfilterKind":
|
|
// Handled by TS_DEBUG_FIREWALL_MODE env var, we don't want to have
|
|
// a CLI flag for this. The Pref is used by c2n.
|
|
continue
|
|
case "DriveShares":
|
|
// Handled by the tailscale share subcommand, we don't want a CLI
|
|
// flag for this.
|
|
continue
|
|
case "InternalExitNodePrior":
|
|
// Used internally by LocalBackend as part of exit node usage toggling.
|
|
// No CLI flag for this.
|
|
continue
|
|
}
|
|
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
|
}
|
|
}
|
|
|
|
func TestFlagAppliesToOS(t *testing.T) {
|
|
for _, goos := range geese {
|
|
var upArgs upArgsT
|
|
fs := newUpFlagSet(goos, &upArgs, "up")
|
|
fs.VisitAll(func(f *flag.Flag) {
|
|
if !flagAppliesToOS(f.Name, goos) {
|
|
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdatePrefs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
flags []string // argv to be parsed into env.flagSet and env.upArgs
|
|
curPrefs *ipn.Prefs
|
|
env upCheckEnv // empty goos means "linux"
|
|
|
|
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
|
|
// It is used to test the --accept-risks flag.
|
|
sshOverTailscale bool
|
|
|
|
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
|
|
// updatePrefs might've mutated them (from applyImplicitPrefs).
|
|
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
|
|
|
|
wantSimpleUp bool
|
|
wantJustEditMP *ipn.MaskedPrefs
|
|
wantErrSubtr string
|
|
}{
|
|
{
|
|
name: "bare_up_means_up",
|
|
flags: []string{},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
WantRunning: false,
|
|
Hostname: "foo",
|
|
},
|
|
},
|
|
{
|
|
name: "just_up",
|
|
flags: []string{},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
},
|
|
env: upCheckEnv{
|
|
backendState: "Stopped",
|
|
},
|
|
wantSimpleUp: true,
|
|
},
|
|
{
|
|
name: "just_edit",
|
|
flags: []string{},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantSimpleUp: true,
|
|
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
|
},
|
|
{
|
|
name: "just_edit_reset",
|
|
flags: []string{"--reset"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
AdvertiseRoutesSet: true,
|
|
AdvertiseTagsSet: true,
|
|
AppConnectorSet: true,
|
|
ControlURLSet: true,
|
|
CorpDNSSet: true,
|
|
ExitNodeAllowLANAccessSet: true,
|
|
ExitNodeIDSet: true,
|
|
ExitNodeIPSet: true,
|
|
HostnameSet: true,
|
|
NetfilterModeSet: true,
|
|
NoSNATSet: true,
|
|
NoStatefulFilteringSet: true,
|
|
OperatorUserSet: true,
|
|
RouteAllSet: true,
|
|
RunSSHSet: true,
|
|
ShieldsUpSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
},
|
|
{
|
|
name: "control_synonym",
|
|
flags: []string{},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantSimpleUp: true,
|
|
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
|
},
|
|
{
|
|
name: "change_login_server",
|
|
flags: []string{"--login-server=https://localhost:1000"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantSimpleUp: true,
|
|
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
|
wantErrSubtr: "can't change --login-server without --force-reauth",
|
|
},
|
|
{
|
|
name: "change_tags",
|
|
flags: []string{"--advertise-tags=tag:foo"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
},
|
|
{
|
|
// Issue 3808: explicitly empty --operator= should clear value.
|
|
name: "explicit_empty_operator",
|
|
flags: []string{"--operator="},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
OperatorUser: "somebody",
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
env: upCheckEnv{user: "somebody", backendState: "Running"},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
OperatorUserSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, prefs *ipn.Prefs) {
|
|
if prefs.OperatorUser != "" {
|
|
t.Errorf("operator sent to backend should be empty; got %q", prefs.OperatorUser)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "enable_ssh",
|
|
flags: []string{"--ssh"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
RunSSHSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if !newPrefs.RunSSH {
|
|
t.Errorf("RunSSH not set to true")
|
|
}
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
},
|
|
{
|
|
name: "disable_ssh",
|
|
flags: []string{"--ssh=false"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
RunSSH: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
RunSSHSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if newPrefs.RunSSH {
|
|
t.Errorf("RunSSH not set to false")
|
|
}
|
|
},
|
|
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
|
|
runSSH: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "disable_ssh_over_ssh_no_risk",
|
|
flags: []string{"--ssh=false"},
|
|
sshOverTailscale: true,
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
RunSSH: true,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
RunSSHSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if !newPrefs.RunSSH {
|
|
t.Errorf("RunSSH not set to true")
|
|
}
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantErrSubtr: "aborted, no changes made",
|
|
},
|
|
{
|
|
name: "enable_ssh_over_ssh_no_risk",
|
|
flags: []string{"--ssh=true"},
|
|
sshOverTailscale: true,
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
RunSSHSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if !newPrefs.RunSSH {
|
|
t.Errorf("RunSSH not set to true")
|
|
}
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantErrSubtr: "aborted, no changes made",
|
|
},
|
|
{
|
|
name: "enable_ssh_over_ssh",
|
|
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
|
|
sshOverTailscale: true,
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
RunSSHSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if !newPrefs.RunSSH {
|
|
t.Errorf("RunSSH not set to true")
|
|
}
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
},
|
|
{
|
|
name: "disable_ssh_over_ssh",
|
|
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
|
|
sshOverTailscale: true,
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
|
CorpDNS: true,
|
|
RunSSH: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
RunSSHSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if newPrefs.RunSSH {
|
|
t.Errorf("RunSSH not set to false")
|
|
}
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
},
|
|
{
|
|
name: "force_reauth_over_ssh_no_risk",
|
|
flags: []string{"--force-reauth"},
|
|
sshOverTailscale: true,
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
wantErrSubtr: "aborted, no changes made",
|
|
},
|
|
{
|
|
name: "force_reauth_over_ssh",
|
|
flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
|
|
sshOverTailscale: true,
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: "https://login.tailscale.com",
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: nil,
|
|
env: upCheckEnv{backendState: "Running"},
|
|
},
|
|
{
|
|
name: "advertise_connector",
|
|
flags: []string{"--advertise-connector"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
AppConnectorSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if !newPrefs.AppConnector.Advertise {
|
|
t.Errorf("prefs.AppConnector.Advertise not set")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "no_advertise_connector",
|
|
flags: []string{"--advertise-connector=false"},
|
|
curPrefs: &ipn.Prefs{
|
|
ControlURL: ipn.DefaultControlURL,
|
|
CorpDNS: true,
|
|
NetfilterMode: preftype.NetfilterOn,
|
|
AppConnector: ipn.AppConnectorPrefs{
|
|
Advertise: true,
|
|
},
|
|
NoStatefulFiltering: opt.NewBool(true),
|
|
},
|
|
wantJustEditMP: &ipn.MaskedPrefs{
|
|
AppConnectorSet: true,
|
|
WantRunningSet: true,
|
|
},
|
|
env: upCheckEnv{backendState: "Running"},
|
|
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
|
if newPrefs.AppConnector.Advertise {
|
|
t.Errorf("prefs.AppConnector.Advertise not unset")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.sshOverTailscale {
|
|
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "100.100.100.100 1 1" })
|
|
} else if isSSHOverTailscale() {
|
|
// The test is being executed over a "real" tailscale SSH
|
|
// session, but sshOverTailscale is unset. Make the test appear
|
|
// as if it's not over tailscale SSH.
|
|
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "" })
|
|
}
|
|
if tt.env.goos == "" {
|
|
tt.env.goos = "linux"
|
|
}
|
|
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs, "up")
|
|
flags := CleanUpArgs(tt.flags)
|
|
if err := tt.env.flagSet.Parse(flags); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
simpleUp, justEditMP, err := updatePrefs(newPrefs, tt.curPrefs, tt.env)
|
|
if err != nil {
|
|
if tt.wantErrSubtr != "" {
|
|
if !strings.Contains(err.Error(), tt.wantErrSubtr) {
|
|
t.Fatalf("want error %q, got: %v", tt.wantErrSubtr, err)
|
|
}
|
|
return
|
|
}
|
|
t.Fatal(err)
|
|
} else if tt.wantErrSubtr != "" {
|
|
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
|
|
}
|
|
if tt.checkUpdatePrefsMutations != nil {
|
|
tt.checkUpdatePrefsMutations(t, newPrefs)
|
|
}
|
|
if simpleUp != tt.wantSimpleUp {
|
|
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
|
|
}
|
|
var oldEditPrefs ipn.Prefs
|
|
if justEditMP != nil {
|
|
oldEditPrefs = justEditMP.Prefs
|
|
justEditMP.Prefs = ipn.Prefs{} // uninteresting
|
|
}
|
|
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
|
|
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", logger.AsJSON(oldEditPrefs))
|
|
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
|
|
return a == b
|
|
})
|
|
|
|
func TestCleanUpArgs(t *testing.T) {
|
|
c := qt.New(t)
|
|
tests := []struct {
|
|
in []string
|
|
want []string
|
|
}{
|
|
{in: []string{"something"}, want: []string{"something"}},
|
|
{in: []string{}, want: []string{}},
|
|
{in: []string{"--authkey=0"}, want: []string{"--auth-key=0"}},
|
|
{in: []string{"a", "--authkey=1", "b"}, want: []string{"a", "--auth-key=1", "b"}},
|
|
{in: []string{"a", "--auth-key=2", "b"}, want: []string{"a", "--auth-key=2", "b"}},
|
|
{in: []string{"a", "-authkey=3", "b"}, want: []string{"a", "--auth-key=3", "b"}},
|
|
{in: []string{"a", "-auth-key=4", "b"}, want: []string{"a", "-auth-key=4", "b"}},
|
|
{in: []string{"a", "--authkey", "5", "b"}, want: []string{"a", "--auth-key", "5", "b"}},
|
|
{in: []string{"a", "-authkey", "6", "b"}, want: []string{"a", "--auth-key", "6", "b"}},
|
|
{in: []string{"a", "authkey", "7", "b"}, want: []string{"a", "authkey", "7", "b"}},
|
|
{in: []string{"--authkeyexpiry", "8"}, want: []string{"--authkeyexpiry", "8"}},
|
|
{in: []string{"--auth-key-expiry", "9"}, want: []string{"--auth-key-expiry", "9"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := CleanUpArgs(tt.in)
|
|
c.Assert(got, qt.DeepEquals, tt.want)
|
|
}
|
|
}
|
|
|
|
func TestUpWorthWarning(t *testing.T) {
|
|
if !upWorthyWarning(healthmsg.WarnAcceptRoutesOff) {
|
|
t.Errorf("WarnAcceptRoutesOff of %q should be worth warning", healthmsg.WarnAcceptRoutesOff)
|
|
}
|
|
if !upWorthyWarning(healthmsg.TailscaleSSHOnBut + "some problem") {
|
|
t.Errorf("want true for SSH problems")
|
|
}
|
|
if upWorthyWarning("not in map poll") {
|
|
t.Errorf("want false for other misc errors")
|
|
}
|
|
}
|
|
|
|
func TestParseNLArgs(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
input []string
|
|
parseKeys bool
|
|
parseDisablements bool
|
|
|
|
wantErr error
|
|
wantKeys []tka.Key
|
|
wantDisablements [][]byte
|
|
}{
|
|
{
|
|
name: "empty",
|
|
input: nil,
|
|
parseKeys: true,
|
|
parseDisablements: true,
|
|
},
|
|
{
|
|
name: "key no votes",
|
|
input: []string{"nlpub:" + strings.Repeat("00", 32)},
|
|
parseKeys: true,
|
|
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 1, Public: bytes.Repeat([]byte{0}, 32)}},
|
|
},
|
|
{
|
|
name: "key with votes",
|
|
input: []string{"nlpub:" + strings.Repeat("01", 32) + "?5"},
|
|
parseKeys: true,
|
|
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 5, Public: bytes.Repeat([]byte{1}, 32)}},
|
|
},
|
|
{
|
|
name: "disablements",
|
|
input: []string{"disablement:" + strings.Repeat("02", 32), "disablement-secret:" + strings.Repeat("03", 32)},
|
|
parseDisablements: true,
|
|
wantDisablements: [][]byte{bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, 32)},
|
|
},
|
|
{
|
|
name: "disablements not allowed",
|
|
input: []string{"disablement:" + strings.Repeat("02", 32)},
|
|
parseKeys: true,
|
|
wantErr: fmt.Errorf("parsing key 1: key hex string doesn't have expected type prefix nlpub:"),
|
|
},
|
|
{
|
|
name: "keys not allowed",
|
|
input: []string{"nlpub:" + strings.Repeat("02", 32)},
|
|
parseDisablements: true,
|
|
wantErr: fmt.Errorf("parsing argument 1: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", "nlpub:0202020202020202020202020202020202020202020202020202020202020202"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
keys, disablements, err := parseNLArgs(tc.input, tc.parseKeys, tc.parseDisablements)
|
|
if (tc.wantErr == nil && err != nil) ||
|
|
(tc.wantErr != nil && err == nil) ||
|
|
(tc.wantErr != nil && err != nil && tc.wantErr.Error() != err.Error()) {
|
|
t.Fatalf("parseNLArgs(%v).err = %v, want %v", tc.input, err, tc.wantErr)
|
|
}
|
|
|
|
if !reflect.DeepEqual(keys, tc.wantKeys) {
|
|
t.Errorf("keys = %v, want %v", keys, tc.wantKeys)
|
|
}
|
|
if !reflect.DeepEqual(disablements, tc.wantDisablements) {
|
|
t.Errorf("disablements = %v, want %v", disablements, tc.wantDisablements)
|
|
}
|
|
})
|
|
}
|
|
}
|