summaryrefslogtreecommitdiffhomepage
path: root/cmd/tailscaled/cli/cli_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/tailscaled/cli/cli_test.go')
-rw-r--r--cmd/tailscaled/cli/cli_test.go668
1 files changed, 668 insertions, 0 deletions
diff --git a/cmd/tailscaled/cli/cli_test.go b/cmd/tailscaled/cli/cli_test.go
new file mode 100644
index 000000000..fecdb76b2
--- /dev/null
+++ b/cmd/tailscaled/cli/cli_test.go
@@ -0,0 +1,668 @@
+// Copyright (c) 2021 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 cli
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+
+ "inet.af/netaddr"
+ "tailscale.com/ipn"
+ "tailscale.com/ipn/ipnstate"
+ "tailscale.com/types/preftype"
+)
+
+// 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)
+ fs.VisitAll(func(f *flag.Flag) {
+ mp := new(ipn.MaskedPrefs)
+ updateMaskedPrefsFromUpFlag(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 netaddr.IP
+ curUser string // os.Getenv("USER") on the client side
+ goos string // empty means "linux"
+
+ want string
+ }{
+ {
+ name: "bare_up_means_up",
+ flags: []string{},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ WantRunning: false,
+ Hostname: "foo",
+ },
+ want: "",
+ },
+ {
+ name: "losing_hostname",
+ flags: []string{"--accept-dns"},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ WantRunning: false,
+ Hostname: "foo",
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AllowSingleHosts: 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,
+ AllowSingleHosts: true,
+ Hostname: "foo",
+ },
+ want: "",
+ },
+ {
+ name: "hostname_changing_empty_explicitly",
+ flags: []string{"--hostname="},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AllowSingleHosts: true,
+ Hostname: "foo",
+ },
+ 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",
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ },
+ curUser: "eve",
+ want: accidentalUpPrefix + " --hostname=foo --operator=alice",
+ },
+ {
+ name: "implicit_operator_matches_shell_user",
+ flags: []string{"--hostname=foo"},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ OperatorUser: "alice",
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("10.0.42.0/24"),
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ },
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("10.0.42.0/24"),
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ },
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("10.0.42.0/24"),
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ },
+ },
+ want: "",
+ },
+ {
+ name: "advertise_exit_node", // Issue 1859
+ flags: []string{"--advertise-exit-node"},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ },
+ want: "",
+ },
+ {
+ name: "advertise_exit_node_over_existing_routes",
+ flags: []string{"--advertise-exit-node"},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("1.2.0.0/16"),
+ },
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ netaddr.MustParseIPPrefix("1.2.0.0/16"),
+ },
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+
+ ExitNodeID: "fooID",
+ },
+ want: "",
+ },
+ {
+ name: "remove_all_implicit",
+ flags: []string{"--force-reauth"},
+ curPrefs: &ipn.Prefs{
+ WantRunning: true,
+ ControlURL: ipn.DefaultControlURL,
+ RouteAll: true,
+ AllowSingleHosts: false,
+ ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
+ CorpDNS: false,
+ ShieldsUp: true,
+ AdvertiseTags: []string{"tag:foo", "tag:bar"},
+ Hostname: "myhostname",
+ ForceDaemon: true,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("10.0.0.0/16"),
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ },
+ NetfilterMode: preftype.NetfilterNoDivert,
+ OperatorUser: "alice",
+ },
+ 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 --host-routes=false --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,
+ AllowSingleHosts: false,
+ ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
+ CorpDNS: false,
+ ShieldsUp: true,
+ AdvertiseTags: []string{"tag:foo", "tag:bar"},
+ Hostname: "myhostname",
+ ForceDaemon: true,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("10.0.0.0/16"),
+ },
+ NetfilterMode: preftype.NetfilterNoDivert,
+ OperatorUser: "alice",
+ },
+ 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 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
+ },
+ {
+ name: "loggedout_is_implicit",
+ flags: []string{"--hostname=foo"},
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ LoggedOut: true,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: 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,
+ AllowSingleHosts: true,
+
+ NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
+ },
+ goos: "windows",
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ netaddr.MustParseIPPrefix("1.2.0.0/16"),
+ },
+ },
+ 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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ netaddr.MustParseIPPrefix("1.2.0.0/16"),
+ },
+ },
+ 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,
+ AllowSingleHosts: true,
+
+ Hostname: "foo",
+ },
+ want: accidentalUpPrefix + " --authkey=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,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+
+ ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
+ },
+ want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
+ },
+ {
+ name: "error_exit_node_omit_with_id_pref",
+ flags: []string{"--hostname=foo"},
+ curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
+ curPrefs: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ NetfilterMode: preftype.NetfilterOn,
+
+ ExitNodeID: "some_stable_id",
+ },
+ want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
+ },
+ }
+ 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)
+ flagSet.Parse(tt.flags)
+ newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
+ if err != nil {
+ t.Fatal(err)
+ }
+ applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
+ var got string
+ if err := checkForAccidentalSettingReverts(flagSet, tt.curPrefs, newPrefs, upCheckEnv{
+ goos: goos,
+ curExitNodeIP: tt.curExitNodeIP,
+ }); 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)
+ 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,
+ NetfilterMode: preftype.NetfilterOn,
+ CorpDNS: true,
+ AllowSingleHosts: true,
+ },
+ },
+ {
+ name: "default_windows",
+ goos: "windows",
+ args: upArgsFromOSArgs("windows"),
+ want: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ WantRunning: true,
+ CorpDNS: true,
+ AllowSingleHosts: true,
+ NetfilterMode: preftype.NetfilterOn,
+ },
+ },
+ {
+ name: "advertise_default_route",
+ args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
+ want: &ipn.Prefs{
+ ControlURL: ipn.DefaultControlURL,
+ WantRunning: true,
+ AllowSingleHosts: true,
+ CorpDNS: true,
+ AdvertiseRoutes: []netaddr.IPPrefix{
+ netaddr.MustParseIPPrefix("0.0.0.0/0"),
+ netaddr.MustParseIPPrefix("::/0"),
+ },
+ NetfilterMode: preftype.NetfilterOn,
+ },
+ },
+ {
+ 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 IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
+ },
+ {
+ 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("a", 300),
+ },
+ wantErr: `hostname too long: 300 bytes (max 256)`,
+ },
+ {
+ 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: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
+ },
+ wantErr: `cannot use 100.105.106.107 as the 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,
+ },
+ },
+ {
+ 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,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var warnBuf bytes.Buffer
+ warnf := func(format string, a ...interface{}) {
+ fmt.Fprintf(&warnBuf, format, a...)
+ }
+ goos := tt.goos
+ if goos == "" {
+ goos = "linux"
+ }
+ st := tt.st
+ if st == nil {
+ st = new(ipnstate.Status)
+ }
+ got, err := prefsFromUpArgs(tt.args, warnf, 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[pref] = true
+ }
+ }
+
+ prefType := reflect.TypeOf(ipn.Prefs{})
+ for i := 0; i < prefType.NumField(); i++ {
+ prefName := prefType.Field(i).Name
+ if prefHasFlag[prefName] {
+ continue
+ }
+ switch prefName {
+ 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
+ }
+ 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)
+ 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)
+ }
+ })
+ }
+}