diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index 2dc32b7c0..d6de6ff94 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -852,9 +852,47 @@ func TestExitNodeIPOfArg(t *testing.T) { want netaddr.IP 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_not_exit", + arg: "1.2.3.4", + st: &ipnstate.Status{ + BackendState: "Running", + Peer: map[key.NodePublic]*ipnstate.PeerStatus{ + key.NewNode().Public(): { + TailscaleIPs: []netaddr.IP{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: []netaddr.IP{mustIP("1.2.3.4")}, + ExitNodeOption: true, + }, + }, + }, want: mustIP("1.2.3.4"), }, { @@ -870,15 +908,16 @@ func TestExitNodeIPOfArg(t *testing.T) { MagicDNSSuffix: ".foo", Peer: map[key.NodePublic]*ipnstate.PeerStatus{ key.NewNode().Public(): { - DNSName: "skippy.foo.", - TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + DNSName: "skippy.foo.", + TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + ExitNodeOption: true, }, }, }, want: mustIP("1.0.0.2"), }, { - name: "ambiguous", + name: "name_not_exit", arg: "skippy", st: &ipnstate.Status{ MagicDNSSuffix: ".foo", @@ -887,9 +926,25 @@ func TestExitNodeIPOfArg(t *testing.T) { DNSName: "skippy.foo.", TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, }, + }, + }, + wantErr: `node "skippy" is not advertising an exit node`, + }, + { + 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")}, + ExitNodeOption: true, + }, key.NewNode().Public(): { - DNSName: "SKIPPY.foo.", - TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + DNSName: "SKIPPY.foo.", + TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")}, + ExitNodeOption: true, }, }, }, diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 136fa5552..598097572 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -193,12 +193,38 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([] return routes, nil } +// peerWithTailscaleIP returns the peer in st with the provided +// Tailscale IP. +func peerWithTailscaleIP(st *ipnstate.Status, ip netaddr.IP) (ps *ipnstate.PeerStatus, ok bool) { + for _, ps := range st.Peer { + for _, ip2 := range ps.TailscaleIPs { + if ip == ip2 { + return ps, true + } + } + } + return nil, false +} + +// exitNodeIPOfArg maps from a user-provided CLI flag value to an IP +// address they want to use as an exit node. 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 { + // If we're online already and have a netmap, double check that the IP + // address specified is valid. + if st.BackendState == "Running" { + ps, ok := peerWithTailscaleIP(st, ip) + if !ok { + return ip, fmt.Errorf("no node found in netmap with IP %v", ip) + } + if !ps.ExitNodeOption { + return ip, fmt.Errorf("node %v is not advertising an exit node", ip) + } + } return ip, err } match := 0 @@ -211,6 +237,9 @@ func exitNodeIPOfArg(arg string, st *ipnstate.Status) (ip netaddr.IP, err error) if len(ps.TailscaleIPs) == 0 { return ip, fmt.Errorf("node %q has no Tailscale IP?", arg) } + if !ps.ExitNodeOption { + return ip, fmt.Errorf("node %q is not advertising an exit node", arg) + } ip = ps.TailscaleIPs[0] } switch match {