mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-28 11:17:33 +00:00

In this PR, we enable the registration of LocalBackend extensions to exclude code specific to certain platforms or environments. We then introduce desktopSessionsExt, which is included only in Windows builds and only if the ts_omit_desktop_sessions tag is disabled for the build. This extension tracks desktop sessions and switches to (or remains on) the appropriate profile when a user signs in or out, locks their screen, or disconnects a remote session. As desktopSessionsExt requires an ipn/desktop.SessionManager, we register it with tsd.System for the tailscaled subprocess on Windows. We also fix a bug in the sessionWatcher implementation where it attempts to close a nil channel on stop. Updates #14823 Updates tailscale/corp#26247 Signed-off-by: Nick Khyl <nickk@tailscale.com>
179 lines
5.8 KiB
Go
179 lines
5.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Both the desktop session manager and multi-user support
|
|
// are currently available only on Windows.
|
|
// This file does not need to be built for other platforms.
|
|
|
|
//go:build windows && !ts_omit_desktop_sessions
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"tailscale.com/feature"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/desktop"
|
|
"tailscale.com/tsd"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/syspolicy"
|
|
)
|
|
|
|
func init() {
|
|
feature.Register("desktop-sessions")
|
|
RegisterExtension("desktop-sessions", newDesktopSessionsExt)
|
|
}
|
|
|
|
// desktopSessionsExt implements [localBackendExtension].
|
|
var _ localBackendExtension = (*desktopSessionsExt)(nil)
|
|
|
|
// desktopSessionsExt extends [LocalBackend] with desktop session management.
|
|
// It keeps Tailscale running in the background if Always-On mode is enabled,
|
|
// and switches to an appropriate profile when a user signs in or out,
|
|
// locks their screen, or disconnects a remote session.
|
|
type desktopSessionsExt struct {
|
|
logf logger.Logf
|
|
sm desktop.SessionManager
|
|
|
|
*LocalBackend // or nil, until Init is called
|
|
cleanup []func() // cleanup functions to call on shutdown
|
|
|
|
// mu protects all following fields.
|
|
// When both mu and [LocalBackend.mu] need to be taken,
|
|
// [LocalBackend.mu] must be taken before mu.
|
|
mu sync.Mutex
|
|
id2sess map[desktop.SessionID]*desktop.Session
|
|
}
|
|
|
|
// newDesktopSessionsExt returns a new [desktopSessionsExt],
|
|
// or an error if [desktop.SessionManager] is not available.
|
|
func newDesktopSessionsExt(logf logger.Logf, sys *tsd.System) (localBackendExtension, error) {
|
|
sm, ok := sys.SessionManager.GetOK()
|
|
if !ok {
|
|
return nil, errors.New("session manager is not available")
|
|
}
|
|
return &desktopSessionsExt{logf: logf, sm: sm, id2sess: make(map[desktop.SessionID]*desktop.Session)}, nil
|
|
}
|
|
|
|
// Init implements [localBackendExtension].
|
|
func (e *desktopSessionsExt) Init(lb *LocalBackend) (err error) {
|
|
e.LocalBackend = lb
|
|
unregisterResolver := lb.RegisterBackgroundProfileResolver(e.getBackgroundProfile)
|
|
unregisterSessionCb, err := e.sm.RegisterStateCallback(e.updateDesktopSessionState)
|
|
if err != nil {
|
|
unregisterResolver()
|
|
return fmt.Errorf("session callback registration failed: %w", err)
|
|
}
|
|
e.cleanup = []func(){unregisterResolver, unregisterSessionCb}
|
|
return nil
|
|
}
|
|
|
|
// updateDesktopSessionState is a [desktop.SessionStateCallback]
|
|
// invoked by [desktop.SessionManager] once for each existing session
|
|
// and whenever the session state changes. It updates the session map
|
|
// and switches to the best profile if necessary.
|
|
func (e *desktopSessionsExt) updateDesktopSessionState(session *desktop.Session) {
|
|
e.mu.Lock()
|
|
if session.Status != desktop.ClosedSession {
|
|
e.id2sess[session.ID] = session
|
|
} else {
|
|
delete(e.id2sess, session.ID)
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
var action string
|
|
switch session.Status {
|
|
case desktop.ForegroundSession:
|
|
// The user has either signed in or unlocked their session.
|
|
// For remote sessions, this may also mean the user has connected.
|
|
// The distinction isn't important for our purposes,
|
|
// so let's always say "signed in".
|
|
action = "signed in to"
|
|
case desktop.BackgroundSession:
|
|
action = "locked"
|
|
case desktop.ClosedSession:
|
|
action = "signed out from"
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
maybeUsername, _ := session.User.Username()
|
|
userIdentifier := cmp.Or(maybeUsername, string(session.User.UserID()), "user")
|
|
reason := fmt.Sprintf("%s %s session %v", userIdentifier, action, session.ID)
|
|
|
|
e.SwitchToBestProfile(reason)
|
|
}
|
|
|
|
// getBackgroundProfile is a [profileResolver] that works as follows:
|
|
//
|
|
// If Always-On mode is disabled, it returns no profile ("","",false).
|
|
//
|
|
// If AlwaysOn mode is enabled, it returns the current profile unless:
|
|
// - The current user has signed out.
|
|
// - Another user has a foreground (i.e. active/unlocked) session.
|
|
//
|
|
// If the current user's session runs in the background and no other user
|
|
// has a foreground session, it returns the current profile. This applies
|
|
// when a locally signed-in user locks their screen or when a remote user
|
|
// disconnects without signing out.
|
|
//
|
|
// In all other cases, it returns no profile ("","",false).
|
|
//
|
|
// It is called with [LocalBackend.mu] locked.
|
|
func (e *desktopSessionsExt) getBackgroundProfile() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
|
|
return "", "", false
|
|
}
|
|
|
|
isCurrentUserSingedIn := false
|
|
var foregroundUIDs []ipn.WindowsUserID
|
|
for _, s := range e.id2sess {
|
|
switch uid := s.User.UserID(); uid {
|
|
case e.pm.CurrentUserID():
|
|
isCurrentUserSingedIn = true
|
|
if s.Status == desktop.ForegroundSession {
|
|
// Keep the current profile if the user has a foreground session.
|
|
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
|
}
|
|
default:
|
|
if s.Status == desktop.ForegroundSession {
|
|
foregroundUIDs = append(foregroundUIDs, uid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there's no current user (e.g., tailscaled just started), or if the current
|
|
// user has no foreground session, switch to the default profile of the first user
|
|
// with a foreground session, if any.
|
|
for _, uid := range foregroundUIDs {
|
|
if profileID := e.pm.DefaultUserProfileID(uid); profileID != "" {
|
|
return uid, profileID, true
|
|
}
|
|
}
|
|
|
|
// If no user has a foreground session but the current user is still signed in,
|
|
// keep the current profile even if the session is not in the foreground,
|
|
// such as when the screen is locked or a remote session is disconnected.
|
|
if len(foregroundUIDs) == 0 && isCurrentUserSingedIn {
|
|
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
|
}
|
|
|
|
return "", "", false
|
|
}
|
|
|
|
// Shutdown implements [localBackendExtension].
|
|
func (e *desktopSessionsExt) Shutdown() error {
|
|
for _, f := range e.cleanup {
|
|
f()
|
|
}
|
|
e.cleanup = nil
|
|
e.LocalBackend = nil
|
|
return nil
|
|
}
|