mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-10 17:58:38 +00:00

We update profileManager to allow registering a single state (profile+prefs) change hook. This is to invert the dependency between the profileManager and the LocalBackend, so that instead of LocalBackend asking profileManager for the state, we can have profileManager call LocalBackend when the state changes. We also update feature.Hook with a new (*feature.Hook).GetOk method to avoid calling both IsSet and Get. Updates tailscale/corp#28014 Updates #12614 Signed-off-by: Nick Khyl <nickk@tailscale.com>
969 lines
37 KiB
Go
969 lines
37 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"cmp"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
|
|
"tailscale.com/clientupdate"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/health"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnext"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
)
|
|
|
|
var debug = envknob.RegisterBool("TS_DEBUG_PROFILES")
|
|
|
|
// [profileManager] implements [ipnext.ProfileStore].
|
|
var _ ipnext.ProfileStore = (*profileManager)(nil)
|
|
|
|
// profileManager is a wrapper around an [ipn.StateStore] that manages
|
|
// multiple profiles and the current profile.
|
|
//
|
|
// It is not safe for concurrent use.
|
|
type profileManager struct {
|
|
goos string // used for TestProfileManagementWindows
|
|
store ipn.StateStore
|
|
logf logger.Logf
|
|
health *health.Tracker
|
|
|
|
currentUserID ipn.WindowsUserID
|
|
knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
|
|
currentProfile ipn.LoginProfileView // always Valid (once [newProfileManager] returns).
|
|
prefs ipn.PrefsView // always Valid (once [newProfileManager] returns).
|
|
|
|
// StateChangeHook is an optional hook that is called when the current profile or prefs change,
|
|
// such as due to a profile switch or a change in the profile's preferences.
|
|
// It is typically set by the [LocalBackend] to invert the dependency between
|
|
// the [profileManager] and the [LocalBackend], so that instead of [LocalBackend]
|
|
// asking [profileManager] for the state, we can have [profileManager] call
|
|
// [LocalBackend] when the state changes. See also:
|
|
// https://github.com/tailscale/tailscale/pull/15791#discussion_r2060838160
|
|
StateChangeHook ipnext.ProfileStateChangeCallback
|
|
|
|
// 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.
|
|
extHost *ExtensionHost
|
|
}
|
|
|
|
// SetExtensionHost sets the [ExtensionHost] for the [profileManager].
|
|
// The specified host will be notified about profile and prefs changes
|
|
// and will immediately be notified about the current profile and prefs.
|
|
// A nil host is a valid, no-op host.
|
|
func (pm *profileManager) SetExtensionHost(host *ExtensionHost) {
|
|
pm.extHost = host
|
|
host.NotifyProfileChange(pm.currentProfile, pm.prefs, false)
|
|
}
|
|
|
|
func (pm *profileManager) dlogf(format string, args ...any) {
|
|
if !debug() {
|
|
return
|
|
}
|
|
pm.logf(format, args...)
|
|
}
|
|
|
|
func (pm *profileManager) WriteState(id ipn.StateKey, val []byte) error {
|
|
return ipn.WriteState(pm.store, id, val)
|
|
}
|
|
|
|
// CurrentUserID returns the current user ID. It is only non-empty on
|
|
// Windows where we have a multi-user system.
|
|
func (pm *profileManager) CurrentUserID() ipn.WindowsUserID {
|
|
return pm.currentUserID
|
|
}
|
|
|
|
// SetCurrentUserID sets the current user ID and switches to that user's default (last used) profile.
|
|
// If the specified user does not have a default profile, or the default profile could not be loaded,
|
|
// it creates a new one and switches to it. The uid is only non-empty on Windows where we have a multi-user system.
|
|
func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
|
|
if pm.currentUserID == uid {
|
|
return
|
|
}
|
|
if _, _, err := pm.SwitchToDefaultProfileForUser(uid); err != nil {
|
|
// SetCurrentUserID should never fail and must always switch to the
|
|
// user's default profile or create a new profile for the current user.
|
|
// Until we implement multi-user support and the new permission model,
|
|
// and remove the concept of the "current user" completely, we must ensure
|
|
// that when SetCurrentUserID exits, the profile in pm.currentProfile
|
|
// is either an existing profile owned by the user, or a new, empty profile.
|
|
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
|
|
pm.SwitchToNewProfileForUser(uid)
|
|
}
|
|
}
|
|
|
|
// SwitchToProfile switches to the specified profile and (temporarily,
|
|
// while the "current user" is still a thing on Windows; see tailscale/corp#18342)
|
|
// sets its owner as the current user. The profile must be a valid profile
|
|
// returned by the [profileManager], such as by [profileManager.Profiles],
|
|
// [profileManager.ProfileByID], or [profileManager.NewProfileForUser].
|
|
//
|
|
// It is a shorthand for [profileManager.SetCurrentUserID] followed by
|
|
// [profileManager.SwitchProfileByID], but it is more efficient as it switches
|
|
// directly to the specified profile rather than switching to the user's
|
|
// default profile first. It is a no-op if the specified profile is already
|
|
// the current profile.
|
|
//
|
|
// As a special case, if the specified profile view is not valid, it resets
|
|
// both the current user and the profile to a new, empty profile not owned
|
|
// by any user.
|
|
//
|
|
// 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 {
|
|
case !profile.Valid():
|
|
// Create a new profile that is not associated with any user.
|
|
profile = pm.NewProfileForUser("")
|
|
case profile == pm.currentProfile,
|
|
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.
|
|
//
|
|
// It includes three cases:
|
|
// 1. The target profile and the current profile are aliases referencing the [ipn.LoginProfile].
|
|
// The profile may be either a new (non-persisted) profile or an existing well-known profile.
|
|
// 2. The target profile is a well-known, persisted profile with the same ID as the current profile.
|
|
// 3. The target and the current profiles are both new (non-persisted) profiles and they are equal.
|
|
// At minimum, equality means that the profiles are owned by the same user on platforms that support it
|
|
// and the prefs are the same as well.
|
|
return pm.currentProfile, false, nil
|
|
case profile.ID() == "":
|
|
// Copy the specified profile to prevent accidental mutation.
|
|
profile = profile.AsStruct().View()
|
|
default:
|
|
// Find an existing profile by ID and load its prefs.
|
|
kp, ok := pm.knownProfiles[profile.ID()]
|
|
if !ok {
|
|
// The profile ID is not valid; it may have been deleted or never existed.
|
|
// As the target profile should have been returned by the [profileManager],
|
|
// this is unexpected and might indicate a bug in the code.
|
|
return pm.currentProfile, false, fmt.Errorf("[unexpected] %w: %s (%s)", errProfileNotFound, profile.Name(), profile.ID())
|
|
}
|
|
profile = kp
|
|
if prefs, err = pm.loadSavedPrefs(profile.Key()); err != nil {
|
|
return pm.currentProfile, false, fmt.Errorf("failed to load profile prefs for %s (%s): %w", profile.Name(), profile.ID(), err)
|
|
}
|
|
}
|
|
|
|
if profile.ID() == "" { // new profile that has never been persisted
|
|
metricNewProfile.Add(1)
|
|
} else {
|
|
metricSwitchProfile.Add(1)
|
|
}
|
|
|
|
pm.prefs = prefs
|
|
pm.updateHealth()
|
|
pm.currentProfile = profile
|
|
pm.currentUserID = profile.LocalUserID()
|
|
if err := pm.setProfileAsUserDefault(profile); err != nil {
|
|
// This is not a fatal error; we've already switched to the profile.
|
|
// But if updating the default profile fails, we should log it.
|
|
pm.logf("failed to set %s (%s) as the default profile: %v", profile.Name(), profile.ID(), err)
|
|
}
|
|
|
|
if f := pm.StateChangeHook; f != nil {
|
|
f(pm.currentProfile, pm.prefs, false)
|
|
}
|
|
// Do not call pm.extHost.NotifyProfileChange here; it is invoked in
|
|
// [LocalBackend.resetForProfileChangeLockedOnEntry] after the netmap reset.
|
|
// TODO(nickkhyl): Consider moving it here (or into the stateChangeCb handler
|
|
// in [LocalBackend]) once the profile/node state, including the netmap,
|
|
// is actually tied to the current profile.
|
|
|
|
return profile, true, nil
|
|
}
|
|
|
|
// DefaultUserProfile returns a read-only view of the default (last used) profile for the specified user.
|
|
// It returns a read-only view of a new, non-persisted profile if the specified user does not have a default profile.
|
|
func (pm *profileManager) DefaultUserProfile(uid ipn.WindowsUserID) ipn.LoginProfileView {
|
|
// Read the CurrentProfileKey from the store which stores
|
|
// the selected profile for the specified user.
|
|
b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid)))
|
|
pm.dlogf("DefaultUserProfile: ReadState(%q) = %v, %v", string(uid), len(b), err)
|
|
if err == ipn.ErrStateNotExist || len(b) == 0 {
|
|
if runtime.GOOS == "windows" {
|
|
pm.dlogf("DefaultUserProfile: windows: migrating from legacy preferences")
|
|
profile, err := pm.migrateFromLegacyPrefs(uid)
|
|
if err == nil {
|
|
return profile
|
|
}
|
|
pm.logf("failed to migrate from legacy preferences: %v", err)
|
|
}
|
|
return pm.NewProfileForUser(uid)
|
|
}
|
|
|
|
pk := ipn.StateKey(string(b))
|
|
prof := pm.findProfileByKey(uid, pk)
|
|
if !prof.Valid() {
|
|
pm.dlogf("DefaultUserProfile: no profile found for key: %q", pk)
|
|
return pm.NewProfileForUser(uid)
|
|
}
|
|
return prof
|
|
}
|
|
|
|
// checkProfileAccess returns an [errProfileAccessDenied] if the current user
|
|
// does not have access to the specified profile.
|
|
func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error {
|
|
return pm.checkProfileAccessAs(pm.currentUserID, profile)
|
|
}
|
|
|
|
// checkProfileAccessAs returns an [errProfileAccessDenied] if the specified user
|
|
// does not have access to the specified profile.
|
|
func (pm *profileManager) checkProfileAccessAs(uid ipn.WindowsUserID, profile ipn.LoginProfileView) error {
|
|
if uid != "" && profile.LocalUserID() != uid {
|
|
return errProfileAccessDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// allProfilesFor returns all profiles accessible to the specified user.
|
|
// The returned profiles are sorted by Name.
|
|
func (pm *profileManager) allProfilesFor(uid ipn.WindowsUserID) []ipn.LoginProfileView {
|
|
out := make([]ipn.LoginProfileView, 0, len(pm.knownProfiles))
|
|
for _, p := range pm.knownProfiles {
|
|
if pm.checkProfileAccessAs(uid, p) == nil {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
slices.SortFunc(out, func(a, b ipn.LoginProfileView) int {
|
|
return cmp.Compare(a.Name(), b.Name())
|
|
})
|
|
return out
|
|
}
|
|
|
|
// matchingProfiles is like [profileManager.allProfilesFor], but returns only profiles
|
|
// matching the given predicate.
|
|
func (pm *profileManager) matchingProfiles(uid ipn.WindowsUserID, f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
|
|
all := pm.allProfilesFor(uid)
|
|
out = all[:0]
|
|
for _, p := range all {
|
|
if f(p) {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// findMatchingProfiles returns all profiles accessible to the current user
|
|
// that represent the same node/user as prefs.
|
|
// The returned profiles are sorted by Name.
|
|
func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.PrefsView) []ipn.LoginProfileView {
|
|
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
|
return p.ControlURL() == prefs.ControlURL() &&
|
|
(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
|
|
p.NodeID() == prefs.Persist().NodeID())
|
|
})
|
|
}
|
|
|
|
// ProfileIDForName returns the profile ID for the profile with the
|
|
// given name. It returns "" if no such profile exists among profiles
|
|
// accessible to the current user.
|
|
func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
|
|
p := pm.findProfileByName(pm.currentUserID, name)
|
|
if !p.Valid() {
|
|
return ""
|
|
}
|
|
return p.ID()
|
|
}
|
|
|
|
func (pm *profileManager) findProfileByName(uid ipn.WindowsUserID, name string) ipn.LoginProfileView {
|
|
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
|
return p.Name() == name && pm.checkProfileAccessAs(uid, p) == nil
|
|
})
|
|
if len(out) == 0 {
|
|
return ipn.LoginProfileView{}
|
|
}
|
|
if len(out) > 1 {
|
|
pm.logf("[unexpected] multiple profiles with the same name")
|
|
}
|
|
return out[0]
|
|
}
|
|
|
|
func (pm *profileManager) findProfileByKey(uid ipn.WindowsUserID, key ipn.StateKey) ipn.LoginProfileView {
|
|
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
|
return p.Key() == key && pm.checkProfileAccessAs(uid, p) == nil
|
|
})
|
|
if len(out) == 0 {
|
|
return ipn.LoginProfileView{}
|
|
}
|
|
if len(out) > 1 {
|
|
pm.logf("[unexpected] multiple profiles with the same key")
|
|
}
|
|
return out[0]
|
|
}
|
|
|
|
func (pm *profileManager) setUnattendedModeAsConfigured() error {
|
|
if pm.goos != "windows" {
|
|
return nil
|
|
}
|
|
|
|
if pm.currentProfile.Key() != "" && pm.prefs.ForceDaemon() {
|
|
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key()))
|
|
} else {
|
|
return pm.WriteState(ipn.ServerModeStartKey, nil)
|
|
}
|
|
}
|
|
|
|
// SetPrefs sets the current profile's prefs to the provided value.
|
|
// It also saves the prefs to the [ipn.StateStore]. It stores a copy of the
|
|
// provided prefs, which may be accessed via [profileManager.CurrentPrefs].
|
|
//
|
|
// The [ipn.NetworkProfile] stores additional information about the tailnet the user
|
|
// is logged into so that we can keep track of things like their domain name
|
|
// across user switches to disambiguate the same account but a different tailnet.
|
|
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
|
|
cp := pm.currentProfile
|
|
if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName == "" {
|
|
// We don't know anything about this profile, so ignore it for now.
|
|
return pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefsIn.AsStruct().View())
|
|
}
|
|
|
|
// Check if we already have an existing profile that matches the user/node.
|
|
if existing := pm.findMatchingProfiles(pm.currentUserID, prefsIn); len(existing) > 0 {
|
|
// We already have a profile for this user/node we should reuse it. Also
|
|
// cleanup any other duplicate profiles.
|
|
cp = existing[0]
|
|
existing = existing[1:]
|
|
for _, p := range existing {
|
|
// Clear the state.
|
|
if err := pm.store.WriteState(p.Key(), nil); err != nil {
|
|
// We couldn't delete the state, so keep the profile around.
|
|
continue
|
|
}
|
|
// Remove the profile, knownProfiles will be persisted
|
|
// in [profileManager.setProfilePrefs] below.
|
|
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, [profileManager] notifies [ipnext.Extension]s about the profile change,
|
|
// so features migrated from LocalBackend to external packages should not be affected.
|
|
//
|
|
// See tailscale/corp#28014.
|
|
if !cp.Equals(pm.currentProfile) {
|
|
const sameNode = false // implicit profile switch
|
|
pm.currentProfile = cp
|
|
pm.prefs = prefsIn.AsStruct().View()
|
|
if f := pm.StateChangeHook; f != nil {
|
|
f(cp, prefsIn, sameNode)
|
|
}
|
|
pm.extHost.NotifyProfileChange(cp, prefsIn, sameNode)
|
|
}
|
|
cp, err := pm.setProfilePrefs(nil, prefsIn, np)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pm.setProfileAsUserDefault(cp)
|
|
}
|
|
|
|
// setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile],
|
|
// returning a read-only view of the updated profile on success. If the specified profile is nil,
|
|
// it defaults to the current profile. If the profile is not accessible by the current user,
|
|
// the method returns an [errProfileAccessDenied].
|
|
func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) (ipn.LoginProfileView, error) {
|
|
isCurrentProfile := lp == nil || (lp.ID != "" && lp.ID == pm.currentProfile.ID())
|
|
if isCurrentProfile {
|
|
lp = pm.CurrentProfile().AsStruct()
|
|
}
|
|
|
|
if err := pm.checkProfileAccess(lp.View()); err != nil {
|
|
return ipn.LoginProfileView{}, err
|
|
}
|
|
|
|
// An empty profile.ID indicates that the profile is new, the node info wasn't available,
|
|
// and it hasn't been persisted yet. We'll generate both an ID and [ipn.StateKey]
|
|
// once the information is available and needs to be persisted.
|
|
if lp.ID == "" {
|
|
if persist := prefsIn.Persist(); persist.Valid() && persist.NodeID() != "" && persist.UserProfile().LoginName != "" {
|
|
// Generate an ID and [ipn.StateKey] now that we have the node info.
|
|
lp.ID, lp.Key = newUnusedID(pm.knownProfiles)
|
|
}
|
|
|
|
// Set the current user as the profile owner, unless the current user ID does
|
|
// not represent a specific user, or the profile is already owned by a different user.
|
|
// It is only relevant on Windows where we have a multi-user system.
|
|
if lp.LocalUserID == "" && pm.currentUserID != "" {
|
|
lp.LocalUserID = pm.currentUserID
|
|
}
|
|
}
|
|
|
|
var up tailcfg.UserProfile
|
|
if persist := prefsIn.Persist(); persist.Valid() {
|
|
up = persist.UserProfile()
|
|
if up.DisplayName == "" {
|
|
up.DisplayName = up.LoginName
|
|
}
|
|
lp.NodeID = persist.NodeID()
|
|
} else {
|
|
lp.NodeID = ""
|
|
}
|
|
|
|
if prefsIn.ProfileName() != "" {
|
|
lp.Name = prefsIn.ProfileName()
|
|
} else {
|
|
lp.Name = up.LoginName
|
|
}
|
|
lp.ControlURL = prefsIn.ControlURL()
|
|
lp.UserProfile = up
|
|
lp.NetworkProfile = np
|
|
|
|
// Update the current profile view to reflect the changes
|
|
// if the specified profile is the current profile.
|
|
if isCurrentProfile {
|
|
// Always set pm.currentProfile to the new profile view for pointer equality.
|
|
// We check it further down the call stack.
|
|
lp := lp.View()
|
|
sameProfileInfo := lp.Equals(pm.currentProfile)
|
|
pm.currentProfile = lp
|
|
if !sameProfileInfo {
|
|
// But only invoke the callbacks if the profile info has actually changed.
|
|
const sameNode = true // just an info update; still the same node
|
|
pm.prefs = prefsIn.AsStruct().View() // suppress further callbacks for this change
|
|
if f := pm.StateChangeHook; f != nil {
|
|
f(lp, prefsIn, sameNode)
|
|
}
|
|
pm.extHost.NotifyProfileChange(lp, prefsIn, sameNode)
|
|
}
|
|
}
|
|
|
|
// An empty profile.ID indicates that the node info is not available yet,
|
|
// and the profile doesn't need to be saved on disk.
|
|
if lp.ID != "" {
|
|
pm.knownProfiles[lp.ID] = lp.View()
|
|
if err := pm.writeKnownProfiles(); err != nil {
|
|
return ipn.LoginProfileView{}, err
|
|
}
|
|
// Clone prefsIn and create a read-only view as a safety measure to
|
|
// prevent accidental preference mutations, both externally and internally.
|
|
if err := pm.setProfilePrefsNoPermCheck(lp.View(), prefsIn.AsStruct().View()); err != nil {
|
|
return ipn.LoginProfileView{}, err
|
|
}
|
|
}
|
|
return lp.View(), nil
|
|
}
|
|
|
|
func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.ProfileID, ipn.StateKey) {
|
|
var idb [2]byte
|
|
for {
|
|
rand.Read(idb[:])
|
|
id := ipn.ProfileID(fmt.Sprintf("%x", idb))
|
|
if _, ok := knownProfiles[id]; ok {
|
|
continue
|
|
}
|
|
return id, ipn.StateKey("profile-" + id)
|
|
}
|
|
}
|
|
|
|
// setProfilePrefsNoPermCheck sets the profile's prefs to the provided value.
|
|
// If the profile has the [ipn.LoginProfile.Key] set, it saves the prefs to the
|
|
// [ipn.StateStore] under that key. It returns an error if the profile is non-current
|
|
// and does not have its Key set, or if the prefs could not be saved.
|
|
// The method does not perform any additional checks on the specified
|
|
// profile, such as verifying the caller's access rights or checking
|
|
// if another profile for the same node already exists.
|
|
func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error {
|
|
isCurrentProfile := pm.currentProfile == profile
|
|
if isCurrentProfile {
|
|
oldPrefs := pm.prefs
|
|
pm.prefs = clonedPrefs
|
|
|
|
// Sadly, profile prefs can be changed in multiple ways.
|
|
// It's pretty chaotic, and in many cases callers use
|
|
// unexported methods of the profile manager instead of
|
|
// going through [LocalBackend.setPrefsLockedOnEntry]
|
|
// or at least using [profileManager.SetPrefs].
|
|
//
|
|
// While we should definitely clean this up to improve
|
|
// the overall structure of how prefs are set, which would
|
|
// also address current and future conflicts, such as
|
|
// competing features changing the same prefs, this method
|
|
// is currently the central place where we can detect all
|
|
// changes to the current profile's prefs.
|
|
//
|
|
// That said, regardless of the cleanup, we might want
|
|
// to keep the profileManager responsible for invoking
|
|
// profile- and prefs-related callbacks.
|
|
|
|
if !clonedPrefs.Equals(oldPrefs) {
|
|
if f := pm.StateChangeHook; f != nil {
|
|
f(pm.currentProfile, clonedPrefs, true)
|
|
}
|
|
pm.extHost.NotifyProfilePrefsChanged(pm.currentProfile, oldPrefs, clonedPrefs)
|
|
}
|
|
|
|
pm.updateHealth()
|
|
}
|
|
if profile.Key() != "" {
|
|
if err := pm.writePrefsToStore(profile.Key(), clonedPrefs); err != nil {
|
|
return err
|
|
}
|
|
} else if !isCurrentProfile {
|
|
return errors.New("cannot set prefs for a non-current in-memory profile")
|
|
}
|
|
if isCurrentProfile {
|
|
return pm.setUnattendedModeAsConfigured()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setPrefsNoPermCheck is like [profileManager.setProfilePrefsNoPermCheck], but sets the current profile's prefs.
|
|
func (pm *profileManager) setPrefsNoPermCheck(clonedPrefs ipn.PrefsView) error {
|
|
return pm.setProfilePrefsNoPermCheck(pm.currentProfile, clonedPrefs)
|
|
}
|
|
|
|
func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsView) error {
|
|
if key == "" {
|
|
return nil
|
|
}
|
|
if err := pm.WriteState(key, prefs.ToBytes()); err != nil {
|
|
pm.logf("WriteState(%q): %v", key, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Profiles returns the list of known profiles accessible to the current user.
|
|
func (pm *profileManager) Profiles() []ipn.LoginProfileView {
|
|
return pm.allProfilesFor(pm.currentUserID)
|
|
}
|
|
|
|
// ProfileByID returns a profile with the given id, if it is accessible to the current user.
|
|
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
|
|
// If the profile does not exist, it returns an [errProfileNotFound].
|
|
func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfileView, error) {
|
|
kp, err := pm.profileByIDNoPermCheck(id)
|
|
if err != nil {
|
|
return ipn.LoginProfileView{}, err
|
|
}
|
|
if err := pm.checkProfileAccess(kp); err != nil {
|
|
return ipn.LoginProfileView{}, err
|
|
}
|
|
return kp, nil
|
|
}
|
|
|
|
// profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't
|
|
// check user's access rights to the profile.
|
|
func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (ipn.LoginProfileView, error) {
|
|
if id == pm.currentProfile.ID() {
|
|
return pm.currentProfile, nil
|
|
}
|
|
kp, ok := pm.knownProfiles[id]
|
|
if !ok {
|
|
return ipn.LoginProfileView{}, errProfileNotFound
|
|
}
|
|
return kp, nil
|
|
}
|
|
|
|
// ProfilePrefs returns preferences for a profile with the given id.
|
|
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
|
|
// If the profile does not exist, it returns an [errProfileNotFound].
|
|
func (pm *profileManager) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error) {
|
|
kp, err := pm.profileByIDNoPermCheck(id)
|
|
if err != nil {
|
|
return ipn.PrefsView{}, errProfileNotFound
|
|
}
|
|
if err := pm.checkProfileAccess(kp); err != nil {
|
|
return ipn.PrefsView{}, err
|
|
}
|
|
return pm.profilePrefs(kp)
|
|
}
|
|
|
|
func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, error) {
|
|
if p.ID() == pm.currentProfile.ID() {
|
|
return pm.prefs, nil
|
|
}
|
|
return pm.loadSavedPrefs(p.Key())
|
|
}
|
|
|
|
// SwitchToProfileByID switches to the profile with the given id.
|
|
// It returns the current profile and whether the call resulted in a profile change.
|
|
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
|
|
// If the profile does not exist, it returns an [errProfileNotFound].
|
|
func (pm *profileManager) SwitchToProfileByID(id ipn.ProfileID) (_ ipn.LoginProfileView, changed bool, err error) {
|
|
if id == pm.currentProfile.ID() {
|
|
return pm.currentProfile, false, nil
|
|
}
|
|
profile, err := pm.ProfileByID(id)
|
|
if err != nil {
|
|
return pm.currentProfile, false, err
|
|
}
|
|
return pm.SwitchToProfile(profile)
|
|
}
|
|
|
|
// SwitchToDefaultProfileForUser switches to the default (last used) profile for the specified user.
|
|
// It creates a new one and switches to it if the specified user does not have a default profile,
|
|
// or returns an error if the default profile is inaccessible or could not be loaded.
|
|
func (pm *profileManager) SwitchToDefaultProfileForUser(uid ipn.WindowsUserID) (_ ipn.LoginProfileView, changed bool, err error) {
|
|
return pm.SwitchToProfile(pm.DefaultUserProfile(uid))
|
|
}
|
|
|
|
// SwitchToDefaultProfile is like [profileManager.SwitchToDefaultProfileForUser], but switches
|
|
// to the default profile for the current user.
|
|
func (pm *profileManager) SwitchToDefaultProfile() (_ ipn.LoginProfileView, changed bool, err error) {
|
|
return pm.SwitchToDefaultProfileForUser(pm.currentUserID)
|
|
}
|
|
|
|
// setProfileAsUserDefault sets the specified profile as the default for the current user.
|
|
// It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user.
|
|
func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView) error {
|
|
if profile.Key() == "" {
|
|
// The profile has not been persisted yet; ignore it for now.
|
|
return nil
|
|
}
|
|
if err := pm.checkProfileAccess(profile); err != nil {
|
|
return errProfileAccessDenied
|
|
}
|
|
k := ipn.CurrentProfileKey(string(pm.currentUserID))
|
|
return pm.WriteState(k, []byte(profile.Key()))
|
|
}
|
|
|
|
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
|
bs, err := pm.store.ReadState(key)
|
|
if err == ipn.ErrStateNotExist || len(bs) == 0 {
|
|
return defaultPrefs, nil
|
|
}
|
|
if err != nil {
|
|
return ipn.PrefsView{}, err
|
|
}
|
|
savedPrefs := ipn.NewPrefs()
|
|
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
|
|
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %v", err)
|
|
}
|
|
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty())
|
|
|
|
// Ignore any old stored preferences for https://login.tailscale.com
|
|
// as the control server that would override the new default of
|
|
// controlplane.tailscale.com.
|
|
if savedPrefs.ControlURL != "" &&
|
|
savedPrefs.ControlURL != ipn.DefaultControlURL &&
|
|
ipn.IsLoginServerSynonym(savedPrefs.ControlURL) {
|
|
savedPrefs.ControlURL = ""
|
|
}
|
|
// Before
|
|
// https://github.com/tailscale/tailscale/pull/11814/commits/1613b18f8280c2bce786980532d012c9f0454fa2#diff-314ba0d799f70c8998940903efb541e511f352b39a9eeeae8d475c921d66c2ac
|
|
// prefs could set AutoUpdate.Apply=true via EditPrefs or tailnet
|
|
// auto-update defaults. After that change, such value is "invalid" and
|
|
// cause any EditPrefs calls to fail (other than disabling auto-updates).
|
|
//
|
|
// Reset AutoUpdate.Apply if we detect such invalid prefs.
|
|
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
|
|
savedPrefs.AutoUpdate.Apply.Clear()
|
|
}
|
|
|
|
return savedPrefs.View(), nil
|
|
}
|
|
|
|
// CurrentProfile returns a read-only [ipn.LoginProfileView] of the current profile.
|
|
// The value may be zero if the profile is not persisted.
|
|
func (pm *profileManager) CurrentProfile() ipn.LoginProfileView {
|
|
return pm.currentProfile
|
|
}
|
|
|
|
// errProfileNotFound is returned by methods that accept a ProfileID
|
|
// when the specified profile does not exist.
|
|
var errProfileNotFound = errors.New("profile not found")
|
|
|
|
// errProfileAccessDenied is returned by methods that accept a ProfileID
|
|
// when the current user does not have access to the specified profile.
|
|
// It is used temporarily until we implement access checks based on the
|
|
// caller's identity in tailscale/corp#18342.
|
|
var errProfileAccessDenied = errors.New("profile access denied")
|
|
|
|
// DeleteProfile removes the profile with the given id. It returns
|
|
// [errProfileNotFound] if the profile does not exist, or an
|
|
// [errProfileAccessDenied] if the specified profile is not accessible
|
|
// to the current user.
|
|
// If the profile is the current profile, it is the equivalent of
|
|
// calling [profileManager.NewProfile] followed by [profileManager.DeleteProfile](id).
|
|
// This is useful for deleting the last profile. In other cases, it is
|
|
// recommended to call [profileManager.SwitchProfile] first.
|
|
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
|
|
metricDeleteProfile.Add(1)
|
|
if id == pm.currentProfile.ID() {
|
|
return pm.deleteCurrentProfile()
|
|
}
|
|
kp, ok := pm.knownProfiles[id]
|
|
if !ok {
|
|
return errProfileNotFound
|
|
}
|
|
if err := pm.checkProfileAccess(kp); err != nil {
|
|
return err
|
|
}
|
|
return pm.deleteProfileNoPermCheck(kp)
|
|
}
|
|
|
|
func (pm *profileManager) deleteCurrentProfile() error {
|
|
if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
|
|
return err
|
|
}
|
|
if pm.currentProfile.ID() == "" {
|
|
// Deleting the in-memory only new profile, just create a new one.
|
|
pm.SwitchToNewProfile()
|
|
return nil
|
|
}
|
|
return pm.deleteProfileNoPermCheck(pm.currentProfile)
|
|
}
|
|
|
|
// deleteProfileNoPermCheck is like [profileManager.DeleteProfile],
|
|
// but it doesn't check user's access rights to the profile.
|
|
func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
|
|
if profile.ID() == pm.currentProfile.ID() {
|
|
pm.SwitchToNewProfile()
|
|
}
|
|
if err := pm.WriteState(profile.Key(), nil); err != nil {
|
|
return err
|
|
}
|
|
delete(pm.knownProfiles, profile.ID())
|
|
return pm.writeKnownProfiles()
|
|
}
|
|
|
|
// DeleteAllProfilesForUser removes all known profiles accessible to the current user
|
|
// and switches to a new, empty profile.
|
|
func (pm *profileManager) DeleteAllProfilesForUser() error {
|
|
metricDeleteAllProfile.Add(1)
|
|
|
|
currentProfileDeleted := false
|
|
writeKnownProfiles := func() error {
|
|
if currentProfileDeleted || pm.currentProfile.ID() == "" {
|
|
pm.SwitchToNewProfile()
|
|
}
|
|
return pm.writeKnownProfiles()
|
|
}
|
|
|
|
for _, kp := range pm.knownProfiles {
|
|
if pm.checkProfileAccess(kp) != nil {
|
|
// Skip profiles we don't have access to.
|
|
continue
|
|
}
|
|
if err := pm.WriteState(kp.Key(), nil); err != nil {
|
|
// Write to remove references to profiles we've already deleted, but
|
|
// return the original error.
|
|
writeKnownProfiles()
|
|
return err
|
|
}
|
|
delete(pm.knownProfiles, kp.ID())
|
|
if kp.ID() == pm.currentProfile.ID() {
|
|
currentProfileDeleted = true
|
|
}
|
|
}
|
|
return writeKnownProfiles()
|
|
}
|
|
|
|
func (pm *profileManager) writeKnownProfiles() error {
|
|
b, err := json.Marshal(pm.knownProfiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pm.WriteState(ipn.KnownProfilesStateKey, b)
|
|
}
|
|
|
|
func (pm *profileManager) updateHealth() {
|
|
if !pm.prefs.Valid() {
|
|
return
|
|
}
|
|
pm.health.SetAutoUpdatePrefs(pm.prefs.AutoUpdate().Check, pm.prefs.AutoUpdate().Apply)
|
|
}
|
|
|
|
// SwitchToNewProfile creates and switches to a new unnamed profile. The new profile is
|
|
// not persisted until [profileManager.SetPrefs] is called with a logged-in user.
|
|
func (pm *profileManager) SwitchToNewProfile() {
|
|
pm.SwitchToNewProfileForUser(pm.currentUserID)
|
|
}
|
|
|
|
// SwitchToNewProfileForUser is like [profileManager.SwitchToNewProfile], but it switches to the
|
|
// specified user and sets that user as the profile owner for the new profile.
|
|
func (pm *profileManager) SwitchToNewProfileForUser(uid ipn.WindowsUserID) {
|
|
pm.SwitchToProfile(pm.NewProfileForUser(uid))
|
|
}
|
|
|
|
// zeroProfile is a read-only view of a new, empty profile that is not persisted to the store.
|
|
var zeroProfile = (&ipn.LoginProfile{}).View()
|
|
|
|
// NewProfileForUser creates a new profile for the specified user and returns a read-only view of it.
|
|
// It neither switches to the new profile nor persists it to the store.
|
|
func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) ipn.LoginProfileView {
|
|
return (&ipn.LoginProfile{LocalUserID: uid}).View()
|
|
}
|
|
|
|
// 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
|
|
// Apple platforms).
|
|
var defaultPrefs = func() ipn.PrefsView {
|
|
prefs := ipn.NewPrefs()
|
|
prefs.LoggedOut = true
|
|
prefs.WantRunning = false
|
|
|
|
return prefs.View()
|
|
}()
|
|
|
|
// Store returns the [ipn.StateStore] used by the [profileManager].
|
|
func (pm *profileManager) Store() ipn.StateStore {
|
|
return pm.store
|
|
}
|
|
|
|
// CurrentPrefs returns a read-only view of the current prefs.
|
|
// The returned view is always valid.
|
|
func (pm *profileManager) CurrentPrefs() ipn.PrefsView {
|
|
return pm.prefs
|
|
}
|
|
|
|
// ReadStartupPrefsForTest reads the startup prefs from disk. It is only used for testing.
|
|
func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsView, error) {
|
|
ht := new(health.Tracker) // in tests, don't care about the health status
|
|
pm, err := newProfileManager(store, logf, ht)
|
|
if err != nil {
|
|
return ipn.PrefsView{}, err
|
|
}
|
|
return pm.CurrentPrefs(), nil
|
|
}
|
|
|
|
// newProfileManager creates a new [profileManager] using the provided [ipn.StateStore].
|
|
// It also loads the list of known profiles from the store.
|
|
func newProfileManager(store ipn.StateStore, logf logger.Logf, health *health.Tracker) (*profileManager, error) {
|
|
return newProfileManagerWithGOOS(store, logf, health, envknob.GOOS())
|
|
}
|
|
|
|
func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
|
|
startKey := ipn.CurrentProfileStateKey
|
|
if goos == "windows" {
|
|
// When tailscaled runs on Windows it is not typically run unattended.
|
|
// So we can't use the profile mechanism to load the profile at startup.
|
|
startKey = ipn.ServerModeStartKey
|
|
}
|
|
autoStartKey, err := store.ReadState(startKey)
|
|
if err != nil && err != ipn.ErrStateNotExist {
|
|
return "", fmt.Errorf("calling ReadState on state store: %w", err)
|
|
}
|
|
return ipn.StateKey(autoStartKey), nil
|
|
}
|
|
|
|
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]ipn.LoginProfileView, error) {
|
|
var knownProfiles map[ipn.ProfileID]ipn.LoginProfileView
|
|
prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
|
|
switch err {
|
|
case nil:
|
|
if err := json.Unmarshal(prfB, &knownProfiles); err != nil {
|
|
return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
|
|
}
|
|
case ipn.ErrStateNotExist:
|
|
knownProfiles = make(map[ipn.ProfileID]ipn.LoginProfileView)
|
|
default:
|
|
return nil, fmt.Errorf("calling ReadState on state store: %w", err)
|
|
}
|
|
return knownProfiles, nil
|
|
}
|
|
|
|
func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *health.Tracker, goos string) (*profileManager, error) {
|
|
logf = logger.WithPrefix(logf, "pm: ")
|
|
stateKey, err := readAutoStartKey(store, goos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
knownProfiles, err := readKnownProfiles(store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pm := &profileManager{
|
|
goos: goos,
|
|
store: store,
|
|
knownProfiles: knownProfiles,
|
|
logf: logf,
|
|
health: ht,
|
|
}
|
|
|
|
var initialProfile ipn.LoginProfileView
|
|
if stateKey != "" {
|
|
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.
|
|
// Specifically this reaches down into legacy preference loading that is
|
|
// specialized by profiles_windows.go and fails in tests on an invalid
|
|
// uid passed in from the unix tests. The uid's used for Windows tests
|
|
// and runtime must be valid Windows security identifier structures.
|
|
} 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 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
|
|
}
|
|
return pm, nil
|
|
}
|
|
|
|
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID) (ipn.LoginProfileView, error) {
|
|
metricMigration.Add(1)
|
|
sentinel, prefs, err := pm.loadLegacyPrefs(uid)
|
|
if err != nil {
|
|
metricMigrationError.Add(1)
|
|
return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
|
|
}
|
|
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
|
|
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)
|
|
}
|
|
pm.completeMigration(sentinel)
|
|
pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
|
|
metricMigrationSuccess.Add(1)
|
|
return profile, nil
|
|
}
|
|
|
|
func (pm *profileManager) requiresBackfill() bool {
|
|
return pm != nil &&
|
|
pm.currentProfile.Valid() &&
|
|
pm.currentProfile.NetworkProfile().RequiresBackfill()
|
|
}
|
|
|
|
var (
|
|
metricNewProfile = clientmetric.NewCounter("profiles_new")
|
|
metricSwitchProfile = clientmetric.NewCounter("profiles_switch")
|
|
metricDeleteProfile = clientmetric.NewCounter("profiles_delete")
|
|
metricDeleteAllProfile = clientmetric.NewCounter("profiles_delete_all")
|
|
|
|
metricMigration = clientmetric.NewCounter("profiles_migration")
|
|
metricMigrationError = clientmetric.NewCounter("profiles_migration_error")
|
|
metricMigrationSuccess = clientmetric.NewCounter("profiles_migration_success")
|
|
)
|