mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-28 11:17:33 +00:00
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
|
||
|
}
|