mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-23 09:06:24 +00:00
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>
This commit is contained in:
@@ -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,30 +626,35 @@ 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 {
|
if s := n.State; s != nil && *s == ipn.NeedsMachineAuth {
|
||||||
switch *s {
|
printed = true
|
||||||
case ipn.NeedsMachineAuth:
|
if env.upArgs.json {
|
||||||
printed = true
|
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||||
if env.upArgs.json {
|
} else {
|
||||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL(policyclient.Get()))
|
||||||
} else {
|
|
||||||
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL(policyclient.Get()))
|
|
||||||
}
|
|
||||||
case ipn.Running:
|
|
||||||
// Done full authentication process
|
|
||||||
if env.upArgs.json {
|
|
||||||
printUpDoneJSON(ipn.Running, "")
|
|
||||||
} else if printed {
|
|
||||||
// Only need to print an update if we printed the "please click" message earlier.
|
|
||||||
fmt.Fprintf(Stderr, "Success.\n")
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case running <- true:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
cancelWatch()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s := n.State; s != nil {
|
||||||
|
ipnIsRunning = *s == ipn.Running
|
||||||
|
}
|
||||||
|
if n.NetMap != nil && n.NetMap.NodeKey != origNodeKey {
|
||||||
|
waitingForKeyChange = false
|
||||||
|
}
|
||||||
|
if ipnIsRunning && !waitingForKeyChange {
|
||||||
|
// Done full authentication process
|
||||||
|
if env.upArgs.json {
|
||||||
|
printUpDoneJSON(ipn.Running, "")
|
||||||
|
} else if printed {
|
||||||
|
// Only need to print an update if we printed the "please click" message earlier.
|
||||||
|
fmt.Fprintf(Stderr, "Success.\n")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case upComplete <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
cancelWatch()
|
||||||
|
return
|
||||||
|
}
|
||||||
if url := n.BrowseToURL; url != nil {
|
if url := n.BrowseToURL; url != nil {
|
||||||
authURL := *url
|
authURL := *url
|
||||||
if !printAuthURL(authURL) || authURL == lastURLPrinted {
|
if !printAuthURL(authURL) || authURL == lastURLPrinted {
|
||||||
@@ -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 != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user