ipn/ipnlocal: update profileManager to use SwitchToProfile when switching to the initial profile

This further minimizes the number of places where the profile manager updates the current profile and prefs.

We also document a scenario where an implicit profile switch can occur.
We should be able to address it after (partially?) inverting the dependency between
LocalBackend and profileManager, so that profileManager notifies LocalBackend
of profile changes instead of the other way around.

Updates tailscale/corp#28014
Updates #12614

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2025-04-25 14:53:48 -05:00 committed by Nick Khyl
parent f468919f95
commit 0cfd643d95

View File

@ -40,8 +40,8 @@ type profileManager struct {
currentUserID ipn.WindowsUserID
knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
currentProfile ipn.LoginProfileView // always Valid.
prefs ipn.PrefsView // always Valid.
currentProfile ipn.LoginProfileView // always Valid (once [newProfileManager] returns).
prefs ipn.PrefsView // always Valid (once [newProfileManager] returns).
// extHost is the bridge between [profileManager] and the registered [ipnext.Extension]s.
// It may be nil in tests. A nil pointer is a valid, no-op host.
@ -111,6 +111,9 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
//
// It returns the current profile and whether the call resulted in a profile change,
// or an error if the specified profile does not exist or its prefs could not be loaded.
//
// It may be called during [profileManager] initialization before [newProfileManager] returns
// and must check whether pm.currentProfile is Valid before using it.
func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.LoginProfileView, changed bool, err error) {
prefs := defaultPrefs
switch {
@ -118,7 +121,7 @@ func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.
// Create a new profile that is not associated with any user.
profile = pm.NewProfileForUser("")
case profile == pm.currentProfile,
profile.ID() != "" && profile.ID() == pm.currentProfile.ID(),
profile.ID() != "" && pm.currentProfile.Valid() && profile.ID() == pm.currentProfile.ID(),
profile.ID() == "" && profile.Equals(pm.currentProfile) && prefs.Equals(pm.prefs):
// The profile is already the current profile; no need to switch.
//
@ -176,7 +179,7 @@ func (pm *profileManager) DefaultUserProfile(uid ipn.WindowsUserID) ipn.LoginPro
if err == ipn.ErrStateNotExist || len(b) == 0 {
if runtime.GOOS == "windows" {
pm.dlogf("DefaultUserProfile: windows: migrating from legacy preferences")
profile, err := pm.migrateFromLegacyPrefs(uid, false)
profile, err := pm.migrateFromLegacyPrefs(uid)
if err == nil {
return profile
}
@ -328,6 +331,23 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
delete(pm.knownProfiles, p.ID())
}
}
// TODO(nickkhyl): Revisit how we handle implicit switching to a different profile,
// which occurs when prefsIn represents a node/user different from that of the
// currentProfile. It happens when a login (either reauth or user-initiated login)
// is completed with a different node/user identity than the one currently in use.
//
// Currently, we overwrite the existing profile prefs with the ones from prefsIn,
// where prefsIn is the previous profile's prefs with an updated Persist, LoggedOut,
// WantRunning and possibly other fields. This may not be the desired behavior.
//
// Additionally, LocalBackend doesn't treat it as a proper profile switch, meaning that
// [LocalBackend.resetForProfileChangeLockedOnEntry] is not called and certain
// node/profile-specific state may not be reset as expected.
//
// However, LocalBackend notifies [ipnext.Extension]s about the profile change,
// so features migrated from LocalBackend to external packages should not be affected.
//
// See tailscale/corp#28014.
pm.currentProfile = cp
cp, err := pm.setProfilePrefs(nil, prefsIn, np)
if err != nil {
@ -746,28 +766,6 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) ipn.LoginProf
return (&ipn.LoginProfile{LocalUserID: uid}).View()
}
// newProfileWithPrefs creates a new profile with the specified prefs and assigns
// the specified uid as the profile owner. If switchNow is true, it switches to the
// newly created profile immediately. It returns the newly created profile on success,
// or an error on failure.
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) {
metricNewProfile.Add(1)
profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
if err != nil {
return ipn.LoginProfileView{}, err
}
if switchNow {
pm.currentProfile = profile
pm.prefs = prefs.AsStruct().View()
pm.updateHealth()
if err := pm.setProfileAsUserDefault(profile); err != nil {
return ipn.LoginProfileView{}, err
}
}
return profile, nil
}
// defaultPrefs is the default prefs for a new profile. This initializes before
// even this package's init() so do not rely on other parts of the system being
// fully initialized here (for example, syspolicy will not be available on
@ -857,27 +855,9 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
health: ht,
}
var initialProfile ipn.LoginProfileView
if stateKey != "" {
for _, v := range knownProfiles {
if v.Key() == stateKey {
pm.currentProfile = v
}
}
if !pm.currentProfile.Valid() {
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
pm.currentUserID = ipn.WindowsUserID(suf)
}
pm.SwitchToNewProfile()
} else {
pm.currentUserID = pm.currentProfile.LocalUserID()
}
prefs, err := pm.loadSavedPrefs(stateKey)
if err != nil {
return nil, err
}
if err := pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefs); err != nil {
return nil, err
}
initialProfile = pm.findProfileByKey("", stateKey)
// Most platform behavior is controlled by the goos parameter, however
// some behavior is implied by build tag and fails when run on Windows,
// so we explicitly avoid that behavior when running on Windows.
@ -888,17 +868,24 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
} else if len(knownProfiles) == 0 && goos != "windows" && runtime.GOOS != "windows" {
// No known profiles, try a migration.
pm.dlogf("no known profiles; trying to migrate from legacy prefs")
if _, err := pm.migrateFromLegacyPrefs(pm.currentUserID, true); err != nil {
if initialProfile, err = pm.migrateFromLegacyPrefs(pm.currentUserID); err != nil {
}
}
if !initialProfile.Valid() {
var initialUserID ipn.WindowsUserID
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
initialUserID = ipn.WindowsUserID(suf)
}
initialProfile = pm.NewProfileForUser(initialUserID)
}
if _, _, err := pm.SwitchToProfile(initialProfile); err != nil {
return nil, err
}
} else {
pm.SwitchToNewProfile()
}
return pm, nil
}
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (ipn.LoginProfileView, error) {
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID) (ipn.LoginProfileView, error) {
metricMigration.Add(1)
sentinel, prefs, err := pm.loadLegacyPrefs(uid)
if err != nil {
@ -906,7 +893,7 @@ func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNo
return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
}
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
if err != nil {
metricMigrationError.Add(1)
return ipn.LoginProfileView{}, fmt.Errorf("migrating _daemon profile: %w", err)