ipn/ipnlocal: set WantRunning upon an interactive login, but not during a seamless renewal or a profile switch
The LocalBackend's state machine starts in NoState and soon transitions to NeedsLogin if there's no auto-start profile, with the profileManager starting with a new empty profile. Notably, entering the NeedsLogin state blocks engine updates. We expect the user to transition out of this state by logging in interactively, and we set WantRunning to true when controlclient enters the StateAuthenticated state. While our intention is correct, and completing an interactive login should set WantRunning to true, our assumption that logging into the current Tailscale profile is the only way to transition out of the NeedsLogin state is not accurate. Another common transition path includes an explicit profile switch (via LocalBackend.SwitchProfile) or an implicit switch when a Windows user connects to the backend. This results in a bug where WantRunning is set to true even when it was previously set to false, and the user expressed no intention of changing it. A similar issue occurs when switching from (sic) a Tailnet that has seamlessRenewalEnabled, regardless of the current state of the LocalBackend's state machine, and also results in unexpectedly set WantRunning. While this behavior is generally undesired, it is also incorrect that it depends on the control knobs of the Tailnet we're switching from rather than the Tailnet we're switching to. However, this issue needs to be addressed separately. This PR updates LocalBackend.SetControlClientStatus to only set WantRunning to true in response to an interactive login as indicated by a non-empty authURL. Fixes #6668 Fixes #11280 Updates #12756 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
+24
-15
@@ -1342,20 +1342,26 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
prefs.Persist = st.Persist.AsStruct()
|
||||
}
|
||||
}
|
||||
if st.LoginFinished() {
|
||||
if b.authURL != "" {
|
||||
b.resetAuthURLLocked()
|
||||
// Interactive login finished successfully (URL visited).
|
||||
// After an interactive login, the user always wants
|
||||
// WantRunning.
|
||||
if !prefs.WantRunning {
|
||||
prefs.WantRunning = true
|
||||
prefsChanged = true
|
||||
}
|
||||
}
|
||||
if prefs.LoggedOut {
|
||||
prefs.LoggedOut = false
|
||||
prefsChanged = true
|
||||
}
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
b.authURLTime = b.clock.Now()
|
||||
}
|
||||
if (wasBlocked || b.seamlessRenewalEnabled()) && st.LoginFinished() {
|
||||
// Interactive login finished successfully (URL visited).
|
||||
// After an interactive login, the user always wants
|
||||
// WantRunning.
|
||||
if !prefs.WantRunning || prefs.LoggedOut {
|
||||
prefsChanged = true
|
||||
}
|
||||
prefs.WantRunning = true
|
||||
prefs.LoggedOut = false
|
||||
}
|
||||
if shouldAutoExitNode() {
|
||||
// Re-evaluate exit node suggestion in case circumstances have changed.
|
||||
_, err := b.suggestExitNodeLocked(curNetMap)
|
||||
@@ -4704,8 +4710,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
|
||||
activeLogin := b.activeLogin
|
||||
authURL := b.authURL
|
||||
if newState == ipn.Running {
|
||||
b.authURL = ""
|
||||
b.authURLTime = time.Time{}
|
||||
b.resetAuthURLLocked()
|
||||
|
||||
// Start a captive portal detection loop if none has been
|
||||
// started. Create a new context if none is present, since it
|
||||
@@ -4987,7 +4992,7 @@ func (b *LocalBackend) resetControlClientLocked() controlclient.Client {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.authURL = ""
|
||||
b.resetAuthURLLocked()
|
||||
|
||||
// When we clear the control client, stop any outstanding netmap expiry
|
||||
// timer; synthesizing a new netmap while we don't have a control
|
||||
@@ -5007,6 +5012,11 @@ func (b *LocalBackend) resetControlClientLocked() controlclient.Client {
|
||||
return prev
|
||||
}
|
||||
|
||||
func (b *LocalBackend) resetAuthURLLocked() {
|
||||
b.authURL = ""
|
||||
b.authURLTime = time.Time{}
|
||||
}
|
||||
|
||||
// ResetForClientDisconnect resets the backend for GUI clients running
|
||||
// in interactive (non-headless) mode. This is currently used only by
|
||||
// Windows. This causes all state to be cleared, lest an unrelated user
|
||||
@@ -5034,8 +5044,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.currentUser = nil
|
||||
}
|
||||
b.keyExpired = false
|
||||
b.authURL = ""
|
||||
b.authURLTime = time.Time{}
|
||||
b.resetAuthURLLocked()
|
||||
b.activeLogin = ""
|
||||
b.resetDialPlan()
|
||||
b.setAtomicValuesFromPrefsLocked(ipn.PrefsView{})
|
||||
|
||||
Reference in New Issue
Block a user