mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-20 11:58:39 +00:00
ipn/ipn{local,server}: extract logic that determines the "best" Tailscale profile to use
In this PR, we further refactor LocalBackend and Unattended Mode to extract the logic that determines which profile should be used at the time of the check, such as when a LocalAPI client connects or disconnects. We then update (*LocalBackend).switchProfileLockedOnEntry to to switch to the profile returned by (*LocalBackend).resolveBestProfileLocked() rather than to the caller-specified specified profile, and rename it to switchToBestProfileLockedOnEntry. This is done in preparation for updating (*LocalBackend).getBackgroundProfileIDLocked to support Always-On mode by determining which profile to use based on which users, if any, are currently logged in and have an active foreground desktop session. Updates #14823 Updates tailscale/corp#26247 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
b7f508fccf
commit
7aef4fd44d
@ -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{})
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user