diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 5c1a69e76..25f4d552f 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3795,14 +3795,15 @@ func (b *LocalBackend) shouldUploadServices() bool { // // On non-multi-user systems, the actor should be set to nil. func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) { - var uid ipn.WindowsUserID - if actor != nil { - uid = actor.UserID() - } - unlock := b.lockAndGetUnlock() defer unlock() + var userIdentifier string + if user := cmp.Or(actor, b.currentUser); user != nil { + maybeUsername, _ := user.Username() + userIdentifier = cmp.Or(maybeUsername, string(user.UserID())) + } + if actor != b.currentUser { if c, ok := b.currentUser.(ipnauth.ActorCloser); ok { c.Close() @@ -3810,46 +3811,108 @@ func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) { b.currentUser = actor } - if b.pm.CurrentUserID() == uid { - return - } - - var profileID ipn.ProfileID - if actor != nil { - profileID = b.pm.DefaultUserProfileID(uid) - } else if uid, profileID = b.getBackgroundProfileIDLocked(); profileID != "" { - b.logf("client disconnected; staying alive in server mode") + var action string + if actor == nil { + action = "disconnected" } else { - b.logf("client disconnected; stopping server") - } - - if err := b.switchProfileLockedOnEntry(uid, profileID, unlock); err != nil { - b.logf("failed switching profile to %q: %v", profileID, err) + action = "connected" } + reason := fmt.Sprintf("client %s (%s)", action, userIdentifier) + b.switchToBestProfileLockedOnEntry(reason, unlock) } -// switchProfileLockedOnEntry is like [LocalBackend.SwitchProfile], -// but b.mu must held on entry, but it is released on exit. -func (b *LocalBackend) switchProfileLockedOnEntry(uid ipn.WindowsUserID, profileID ipn.ProfileID, unlock unlockOnce) error { +// switchToBestProfileLockedOnEntry selects the best profile to use, +// as reported by [LocalBackend.resolveBestProfileLocked], and switches +// to it, unless it's already the current profile. The reason indicates +// why the profile is being switched, such as due to a client connecting +// or disconnecting and is used for logging. +// +// b.mu must held on entry. It is released on exit. +func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock unlockOnce) { defer unlock() - if b.pm.CurrentUserID() == uid && b.pm.CurrentProfile().ID() == profileID { - return nil - } oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault() - if changed := b.pm.SetCurrentUserAndProfile(uid, profileID); !changed { - return nil + uid, profileID, background := b.resolveBestProfileLocked() + cp, switched := b.pm.SetCurrentUserAndProfile(uid, profileID) + switch { + case !switched && cp.ID() == "": + b.logf("%s: staying on empty profile", reason) + case !switched: + b.logf("%s: staying on profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID()) + case cp.ID() == "": + b.logf("%s: disconnecting Tailscale", reason) + case background: + b.logf("%s: switching to background profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID()) + default: + b.logf("%s: switching to profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID()) + } + if !switched { + return } // As an optimization, only reset the dialPlan if the control URL changed. if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(); oldControlURL != newControlURL { b.resetDialPlan() } - return b.resetForProfileChangeLockedOnEntry(unlock) + if err := b.resetForProfileChangeLockedOnEntry(unlock); err != nil { + // TODO(nickkhyl): The actual reset cannot fail. However, + // the TKA initialization or [LocalBackend.Start] can fail. + // These errors are not critical as far as we're concerned. + // But maybe we should post a notification to the API watchers? + b.logf("failed switching profile to %q: %v", profileID, err) + } } -// getBackgroundProfileIDLocked returns the profile ID to use when no GUI/CLI -// client is connected, or "" if Tailscale should not run in the background. +// resolveBestProfileLocked returns the best profile to use based on the current +// state of the backend, such as whether a GUI/CLI client is connected and whether +// the unattended mode is enabled. +// +// It returns the user ID, profile ID, and whether the returned profile is +// considered a background profile. A background profile is used when no OS user +// is actively using Tailscale, such as when no GUI/CLI client is connected +// and Unattended Mode is enabled (see also [LocalBackend.getBackgroundProfileLocked]). +// An empty profile ID indicates that Tailscale should switch to an empty profile. +// +// b.mu must be held. +func (b *LocalBackend) resolveBestProfileLocked() (userID ipn.WindowsUserID, profileID ipn.ProfileID, isBackground bool) { + // If a GUI/CLI client is connected, use the connected user's profile, which means + // either the current profile if owned by the user, or their default profile. + if b.currentUser != nil { + cp := b.pm.CurrentProfile() + uid := b.currentUser.UserID() + + var profileID ipn.ProfileID + // TODO(nickkhyl): check if the current profile is allowed on the device, + // such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet. + // See tailscale/corp#26249. + if cp.LocalUserID() == uid { + profileID = cp.ID() + } else { + profileID = b.pm.DefaultUserProfileID(uid) + } + return uid, profileID, false + } + + // Otherwise, if on Windows, use the background profile if one is set. + // This includes staying on the current profile if Unattended Mode is enabled. + // If the returned background profileID is "", Tailscale will disconnect + // and remain idle until a GUI or CLI client connects. + if goos := envknob.GOOS(); goos == "windows" { + uid, profileID := b.getBackgroundProfileLocked() + return uid, profileID, true + } + + // On other platforms, however, Tailscale continues to run in the background + // using the current profile. + // + // TODO(nickkhyl): check if the current profile is allowed on the device, + // such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet. + // See tailscale/corp#26249. + return b.pm.CurrentUserID(), b.pm.CurrentProfile().ID(), false +} + +// getBackgroundProfileLocked returns the user and profile ID to use when no GUI/CLI +// client is connected, or "","" if Tailscale should not run in the background. // As of 2025-02-07, it is only used on Windows. -func (b *LocalBackend) getBackgroundProfileIDLocked() (ipn.WindowsUserID, ipn.ProfileID) { +func (b *LocalBackend) getBackgroundProfileLocked() (ipn.WindowsUserID, ipn.ProfileID) { // If Unattended Mode is enabled for the current profile, keep using it. if b.pm.CurrentPrefs().ForceDaemon() { return b.pm.CurrentProfile().LocalUserID(), b.pm.CurrentProfile().ID() @@ -7190,9 +7253,9 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err // Needs to happen without b.mu held. defer prevCC.Shutdown() } - if err := b.initTKALocked(); err != nil { - return err - } + // TKA errors should not prevent resetting the backend state. + // However, we should still return the error to the caller. + tkaErr := b.initTKALocked() b.lastServeConfJSON = mem.B(nil) b.serveConfig = ipn.ServeConfigView{} b.lastSuggestedExitNode = "" @@ -7201,6 +7264,9 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err b.setAtomicValuesFromPrefsLocked(b.pm.CurrentPrefs()) b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu b.health.SetLocalLogConfigHealth(nil) + if tkaErr != nil { + return tkaErr + } return b.Start(ipn.Options{}) } diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index 65714874a..10a110e61 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -91,24 +91,25 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) { // profile for the user and switches to it, unless the current profile // is already a new, empty profile owned by the user. // -// It reports whether the call resulted in a profile switch. -func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (changed bool) { +// It returns the current profile and whether the call resulted +// in a profile switch. +func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (cp ipn.LoginProfileView, changed bool) { pm.currentUserID = uid if profileID == "" { if pm.currentProfile.ID() == "" && pm.currentProfile.LocalUserID() == uid { - return false + return pm.currentProfile, false } pm.NewProfileForUser(uid) - return true + return pm.currentProfile, true } if profile, err := pm.ProfileByID(profileID); err == nil { if pm.CurrentProfile().ID() == profileID { - return false + return pm.currentProfile, false } if err := pm.SwitchProfile(profile.ID()); err == nil { - return true + return pm.currentProfile, true } } @@ -116,7 +117,7 @@ func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profil pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err) pm.NewProfile() } - return true + return pm.currentProfile, true } // DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user, diff --git a/ipn/ipnserver/actor.go b/ipn/ipnserver/actor.go index 594ebf2d5..b0245b0a8 100644 --- a/ipn/ipnserver/actor.go +++ b/ipn/ipnserver/actor.go @@ -32,6 +32,7 @@ type actor struct { ci *ipnauth.ConnIdentity clientID ipnauth.ClientID + userID ipn.WindowsUserID // cached Windows user ID of the connected client process. // accessOverrideReason specifies the reason for overriding certain access restrictions, // such as permitting a user to disconnect when the always-on mode is enabled, // provided that such justification is allowed by the policy. @@ -59,7 +60,14 @@ func newActor(logf logger.Logf, c net.Conn) (*actor, error) { // connectivity on domain-joined devices and/or be slow. clientID = ipnauth.ClientIDFrom(pid) } - return &actor{logf: logf, ci: ci, clientID: clientID, isLocalSystem: connIsLocalSystem(ci)}, nil + return &actor{ + logf: logf, + ci: ci, + clientID: clientID, + userID: ci.WindowsUserID(), + isLocalSystem: connIsLocalSystem(ci), + }, + nil } // actorWithAccessOverride returns a new actor that carries the specified @@ -106,7 +114,7 @@ func (a *actor) IsLocalAdmin(operatorUID string) bool { // UserID implements [ipnauth.Actor]. func (a *actor) UserID() ipn.WindowsUserID { - return a.ci.WindowsUserID() + return a.userID } func (a *actor) pid() int {