ipn, cmd/tailscale/cli: allow setting FQDN sans dot as an exit node
In #10057, @seigel pointed out an inconsistency in the help text for `exit-node list` and `set --exit-node`: 1. Use `tailscale exit-node list`, which has a column titled "hostname" and tells you that you can use a hostname with `set --exit-node`: ```console $ tailscale exit-node list IP HOSTNAME COUNTRY CITY STATUS 100.98.193.6 linode-vps.tailfa84dd.ts.net - - - […] 100.93.242.75 ua-iev-wg-001.mullvad.ts.net Ukraine Kyiv - # To view the complete list of exit nodes for a country, use `tailscale exit-node list --filter=` followed by the country name. # To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP. # To have Tailscale suggest an exit node, use `tailscale exit-node suggest`. ``` (This is the same format hostnames are presented in the admin console.) 2. Try copy/pasting a hostname into `set --exit-node`: ```console $ tailscale set --exit-node=linode-vps.tailfa84dd.ts.net invalid value "linode-vps.tailfa84dd.ts.net" for --exit-node; must be IP or unique node name ``` 3. Note that the command allows some hostnames, if they're from nodes in a different tailnet: ```console $ tailscale set --exit-node= ua-iev-wg-001.mullvad.ts.net $ echo $? 0 ``` This patch addresses the inconsistency in two ways: 1. Allow using `tailscale set --exit-node=` with an FQDN that's missing the trailing dot, matching the formatting used in `exit-node list` and the admin console. 2. Make the description of valid exit nodes consistent across commands ("hostname or IP"). Updates #10057 Change-Id: If5d74f950cc1a9cc4b0ebc0c2f2d70689ffe4d73 Signed-off-by: Alex Chan <alexc@tailscale.com>
This commit is contained in:
@@ -769,7 +769,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
args: upArgsT{
|
args: upArgsT{
|
||||||
exitNodeIP: "foo",
|
exitNodeIP: "foo",
|
||||||
},
|
},
|
||||||
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
|
wantErr: `invalid value "foo" for --exit-node; must be IP or hostname`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error_exit_node_allow_lan_without_exit_node",
|
name: "error_exit_node_allow_lan_without_exit_node",
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func runExitNodeList(ctx context.Context, args []string) error {
|
|||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
fmt.Fprintln(w, "# To view the complete list of exit nodes for a country, use `tailscale exit-node list --filter=` followed by the country name.")
|
fmt.Fprintln(w, "# To view the complete list of exit nodes for a country, use `tailscale exit-node list --filter=` followed by the country name.")
|
||||||
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP.")
|
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the IP or hostname.")
|
||||||
if hasAnyExitNodeSuggestions(peers) {
|
if hasAnyExitNodeSuggestions(peers) {
|
||||||
fmt.Fprintln(w, "# To have Tailscale suggest an exit node, use `tailscale exit-node suggest`.")
|
fmt.Fprintln(w, "# To have Tailscale suggest an exit node, use `tailscale exit-node suggest`.")
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -896,8 +896,17 @@ func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netip.Addr, err error) {
|
|||||||
}
|
}
|
||||||
match := 0
|
match := 0
|
||||||
for _, ps := range st.Peer {
|
for _, ps := range st.Peer {
|
||||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
// Compare to the peer name in three forms:
|
||||||
if !strings.EqualFold(s, baseName) && !strings.EqualFold(s, ps.DNSName) {
|
//
|
||||||
|
// - base name ("example")
|
||||||
|
// - FQDN ("example.tail1234.ts.net.")
|
||||||
|
// - FQDN sans dot ("example.tail1234.ts.net", as returned by `tailscale exit-node list`
|
||||||
|
// and the admin console)
|
||||||
|
//
|
||||||
|
fqdn := ps.DNSName
|
||||||
|
baseName := dnsname.TrimSuffix(fqdn, st.MagicDNSSuffix)
|
||||||
|
fqdnSansDot := dnsname.TrimSuffix(fqdn, ".")
|
||||||
|
if !strings.EqualFold(s, baseName) && !strings.EqualFold(s, fqdn) && !strings.EqualFold(s, fqdnSansDot) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
match++
|
match++
|
||||||
@@ -911,7 +920,7 @@ func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netip.Addr, err error) {
|
|||||||
}
|
}
|
||||||
switch match {
|
switch match {
|
||||||
case 0:
|
case 0:
|
||||||
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", s)
|
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or hostname", s)
|
||||||
case 1:
|
case 1:
|
||||||
if !isRemoteIP(st, ip) {
|
if !isRemoteIP(st, ip) {
|
||||||
return ip, ExitNodeLocalIPError{s}
|
return ip, ExitNodeLocalIPError{s}
|
||||||
|
|||||||
+17
-2
@@ -1009,7 +1009,7 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
|||||||
name: "no_match",
|
name: "no_match",
|
||||||
arg: "unknown",
|
arg: "unknown",
|
||||||
st: &ipnstate.Status{MagicDNSSuffix: ".foo"},
|
st: &ipnstate.Status{MagicDNSSuffix: ".foo"},
|
||||||
wantErr: `invalid value "unknown" for --exit-node; must be IP or unique node name`,
|
wantErr: `invalid value "unknown" for --exit-node; must be IP or hostname`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
@@ -1041,6 +1041,21 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: mustIP("1.0.0.2"),
|
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: "name_not_exit",
|
name: "name_not_exit",
|
||||||
arg: "skippy",
|
arg: "skippy",
|
||||||
@@ -1067,7 +1082,7 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: `invalid value "skippy.bar." for --exit-node; must be IP or unique node name`,
|
wantErr: `invalid value "skippy.bar." for --exit-node; must be IP or hostname`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ambiguous",
|
name: "ambiguous",
|
||||||
|
|||||||
Reference in New Issue
Block a user