From f91481075d7a03fa9bfc7791d51759785becae3f Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 2 Dec 2021 13:47:07 -0800 Subject: [PATCH] cmd/tailscale: let --exit-node= take a machine name in addition to IP If you're online, let tailscale up --exit-node=NAME map NAME to its IP. We don't store the exit node name server-side in prefs, avoiding the concern raised earlier. Fixes #3062 Change-Id: Ieea5ceec1a30befc67e9d6b8a530b3cb047b6b40 Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/cli/cli_test.go | 78 ++++++++++++++++++++++++++++++++++- cmd/tailscale/cli/up.go | 37 +++++++++++++++-- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index 867ff9a6f..2dc32b7c0 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -18,6 +18,7 @@ import ( "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tstest" + "tailscale.com/types/key" "tailscale.com/types/persist" "tailscale.com/types/preftype" "tailscale.com/version/distro" @@ -567,7 +568,7 @@ func TestPrefsFromUpArgs(t *testing.T) { args: upArgsT{ exitNodeIP: "foo", }, - wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`, + wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`, }, { name: "error_exit_node_allow_lan_without_exit_node", @@ -841,3 +842,78 @@ func TestUpdatePrefs(t *testing.T) { }) } } + +func TestExitNodeIPOfArg(t *testing.T) { + mustIP := netaddr.MustParseIP + tests := []struct { + name string + arg string + st *ipnstate.Status + want netaddr.IP + wantErr string + }{ + { + name: "ip", + arg: "1.2.3.4", + want: mustIP("1.2.3.4"), + }, + { + name: "no_match", + arg: "unknown", + st: &ipnstate.Status{MagicDNSSuffix: ".foo"}, + wantErr: `invalid value "unknown" for --exit-node; must be IP or unique node name`, + }, + { + name: "name", + arg: "skippy", + st: &ipnstate.Status{ + MagicDNSSuffix: ".foo", + Peer: map[key.NodePublic]*ipnstate.PeerStatus{ + key.NewNode().Public(): { + DNSName: "skippy.foo.", + TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + }, + }, + }, + want: mustIP("1.0.0.2"), + }, + { + name: "ambiguous", + arg: "skippy", + st: &ipnstate.Status{ + MagicDNSSuffix: ".foo", + Peer: map[key.NodePublic]*ipnstate.PeerStatus{ + key.NewNode().Public(): { + DNSName: "skippy.foo.", + TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + }, + key.NewNode().Public(): { + DNSName: "SKIPPY.foo.", + TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + }, + }, + }, + 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) + } + }) + } +} diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 5d2916f27..a2ca9a410 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -28,6 +28,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/preftype" + "tailscale.com/util/dnsname" "tailscale.com/version/distro" ) @@ -73,7 +74,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet { upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes") upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel") upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes") - upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node") + upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node") upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node") upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections") upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")") @@ -190,6 +191,36 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([] return routes, nil } +func exitNodeIPOfArg(arg string, st *ipnstate.Status) (ip netaddr.IP, err error) { + if arg == "" { + return ip, errors.New("invalid use of exitNodeIPOfArg with empty string") + } + ip, err = netaddr.ParseIP(arg) + if err == nil { + return ip, err + } + match := 0 + for _, ps := range st.Peer { + baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix) + if !strings.EqualFold(arg, baseName) { + continue + } + match++ + if len(ps.TailscaleIPs) == 0 { + return ip, fmt.Errorf("node %q has no Tailscale IP?", arg) + } + ip = ps.TailscaleIPs[0] + } + switch match { + case 0: + return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", arg) + case 1: + return ip, nil + default: + return ip, fmt.Errorf("ambiguous exit node name %q", arg) + } +} + // prefsFromUpArgs returns the ipn.Prefs for the provided args. // // Note that the parameters upArgs and warnf are named intentionally @@ -205,9 +236,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo var exitNodeIP netaddr.IP if upArgs.exitNodeIP != "" { var err error - exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP) + exitNodeIP, err = exitNodeIPOfArg(upArgs.exitNodeIP, st) if err != nil { - return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err) + return nil, err } } else if upArgs.exitNodeAllowLANAccess { return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")