cmd/tailscale/cli: fix race condition in `up --force-reauth`

This commit fixes a race condition where `tailscale up --force-reauth` would
exit prematurely on an already-logged in device.

Previously, the CLI would wait for IPN to report the "Running" state and then
exit. However, this could happen before the new auth URL was printed, leading
to two distinct issues:

*   **Without seamless key renewal:** The CLI could exit immediately after
    the `StartLoginInteractive` call, before IPN has time to switch into
    the "Starting" state or send a new auth URL back to the CLI.
*   **With seamless key renewal:** IPN stays in the "Running" state
    throughout the process, so the CLI exits immediately without performing
    any reauthentication.

The fix is to change the CLI's exit condition.

Instead of waiting for the "Running" state, if we're doing a `--force-reauth`
we now wait to see the node key change, which is a more reliable indicator
that a successful authentication has occurred.

Updates tailscale/corp#31476
Updates tailscale/tailscale#17108

Signed-off-by: Alex Chan <alexc@tailscale.com>
pull/17284/head
Alex Chan 3 months ago committed by Alex Chan
parent c011369de2
commit 41a2aaf1da

@ -446,6 +446,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
return fixTailscaledConnectError(err) return fixTailscaledConnectError(err)
} }
origAuthURL := st.AuthURL origAuthURL := st.AuthURL
origNodeKey := st.Self.PublicKey
// printAuthURL reports whether we should print out the // printAuthURL reports whether we should print out the
// provided auth URL from an IPN notify. // provided auth URL from an IPN notify.
@ -597,13 +598,24 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
} }
} }
running := make(chan bool, 1) upComplete := make(chan bool, 1)
watchErr := make(chan error, 1) watchErr := make(chan error, 1)
go func() { go func() {
var printed bool // whether we've yet printed anything to stdout or stderr var printed bool // whether we've yet printed anything to stdout or stderr
var lastURLPrinted string var lastURLPrinted string
// If we're doing a force-reauth, we need to get two notifications:
//
// 1. IPN is running
// 2. The node key has changed
//
// These two notifications arrive separately, and trying to combine them
// has caused unexpected issues elsewhere in `tailscale up`. For now, we
// track them separately.
ipnIsRunning := false
waitingForKeyChange := upArgs.forceReauth
for { for {
n, err := watcher.Next() n, err := watcher.Next()
if err != nil { if err != nil {
@ -614,29 +626,34 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
msg := *n.ErrMessage msg := *n.ErrMessage
fatalf("backend error: %v\n", msg) fatalf("backend error: %v\n", msg)
} }
if s := n.State; s != nil && *s == ipn.NeedsMachineAuth {
printed = true
if env.upArgs.json {
printUpDoneJSON(ipn.NeedsMachineAuth, "")
} else {
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL(policyclient.Get()))
}
}
if s := n.State; s != nil { if s := n.State; s != nil {
switch *s { ipnIsRunning = *s == ipn.Running
case ipn.NeedsMachineAuth: }
printed = true if n.NetMap != nil && n.NetMap.NodeKey != origNodeKey {
if env.upArgs.json { waitingForKeyChange = false
printUpDoneJSON(ipn.NeedsMachineAuth, "") }
} else { if ipnIsRunning && !waitingForKeyChange {
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL(policyclient.Get())) // Done full authentication process
} if env.upArgs.json {
case ipn.Running: printUpDoneJSON(ipn.Running, "")
// Done full authentication process } else if printed {
if env.upArgs.json { // Only need to print an update if we printed the "please click" message earlier.
printUpDoneJSON(ipn.Running, "") fmt.Fprintf(Stderr, "Success.\n")
} else if printed { }
// Only need to print an update if we printed the "please click" message earlier. select {
fmt.Fprintf(Stderr, "Success.\n") case upComplete <- true:
} default:
select {
case running <- true:
default:
}
cancelWatch()
} }
cancelWatch()
return
} }
if url := n.BrowseToURL; url != nil { if url := n.BrowseToURL; url != nil {
authURL := *url authURL := *url
@ -698,18 +715,18 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
timeoutCh = timeoutTimer.C timeoutCh = timeoutTimer.C
} }
select { select {
case <-running: case <-upComplete:
return nil return nil
case <-watchCtx.Done(): case <-watchCtx.Done():
select { select {
case <-running: case <-upComplete:
return nil return nil
default: default:
} }
return watchCtx.Err() return watchCtx.Err()
case err := <-watchErr: case err := <-watchErr:
select { select {
case <-running: case <-upComplete:
return nil return nil
default: default:
} }

@ -337,14 +337,6 @@ func TestOneNodeUpAuth(t *testing.T) {
t.Run(fmt.Sprintf("%s-seamless-%t", tt.name, useSeamlessKeyRenewal), func(t *testing.T) { t.Run(fmt.Sprintf("%s-seamless-%t", tt.name, useSeamlessKeyRenewal), func(t *testing.T) {
tstest.Parallel(t) tstest.Parallel(t)
// TODO(alexc): This test is failing because of a bug in `tailscale up` where
// it waits for ipn to enter the "Running" state. If we're already logged in
// and running, this completes immediately, before we've had a chance to show
// the user the auth URL.
if tt.name == "up-with-force-reauth-after-login" {
t.Skip()
}
env := NewTestEnv(t, ConfigureControl( env := NewTestEnv(t, ConfigureControl(
func(control *testcontrol.Server) { func(control *testcontrol.Server) {
if tt.authKey != "" { if tt.authKey != "" {

Loading…
Cancel
Save