This commit is contained in:
2026-02-19 10:07:43 +00:00
parent 007438e372
commit 6e637ecf77
1763 changed files with 60820 additions and 279516 deletions

View File

@@ -5,25 +5,35 @@ package ipnlocal
import (
"cmp"
"crypto"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"runtime"
"slices"
"strings"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/testenv"
)
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.
//
@@ -36,8 +46,33 @@ type profileManager struct {
currentUserID ipn.WindowsUserID
knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
currentProfile ipn.LoginProfileView // always Valid.
prefs ipn.PrefsView // always Valid.
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
// Override for key.NewEmptyHardwareAttestationKey used for testing.
newEmptyHardwareAttestationKey func() (key.HardwareAttestationKey, error)
}
// 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) {
@@ -64,8 +99,7 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
if pm.currentUserID == uid {
return
}
pm.currentUserID = uid
if err := pm.SwitchToDefaultProfile(); err != nil {
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,
@@ -73,79 +107,122 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
// 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.NewProfileForUser(uid)
pm.SwitchToNewProfileForUser(uid)
}
}
// SetCurrentUserAndProfile sets the current user ID and switches the specified
// profile, if it is accessible to the user. If the profile does not exist,
// or is not accessible, it switches to the user's default profile,
// creating a new one if necessary.
// 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.SwitchProfile], but it is more efficient as it switches
// [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.
// 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 ID "", it creates a new
// profile for the user and switches to it, unless the current profile
// is already a new, empty profile owned by the user.
// 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 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 pm.currentProfile, false
// 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())
}
pm.NewProfileForUser(uid)
return pm.currentProfile, true
}
if profile, err := pm.ProfileByID(profileID); err == nil {
if pm.CurrentProfile().ID() == profileID {
return pm.currentProfile, false
}
if err := pm.SwitchProfile(profile.ID()); err == nil {
return pm.currentProfile, true
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 err := pm.SwitchToDefaultProfile(); err != nil {
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
pm.NewProfile()
if profile.ID() == "" { // new profile that has never been persisted
metricNewProfile.Add(1)
} else {
metricSwitchProfile.Add(1)
}
return pm.currentProfile, true
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
}
// DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user,
// or an empty string if the specified user does not have a default profile.
func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.ProfileID {
// 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("DefaultUserProfileID: ReadState(%q) = %v, %v", string(uid), len(b), err)
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("DefaultUserProfileID: windows: migrating from legacy preferences")
profile, err := pm.migrateFromLegacyPrefs(uid, false)
pm.dlogf("DefaultUserProfile: windows: migrating from legacy preferences")
profile, err := pm.migrateFromLegacyPrefs(uid)
if err == nil {
return profile.ID()
return profile
}
pm.logf("failed to migrate from legacy preferences: %v", err)
}
return ""
return pm.NewProfileForUser(uid)
}
pk := ipn.StateKey(string(b))
prof := pm.findProfileByKey(uid, pk)
if !prof.Valid() {
pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
return ""
pm.dlogf("DefaultUserProfile: no profile found for key: %q", pk)
return pm.NewProfileForUser(uid)
}
return prof.ID()
return prof
}
// checkProfileAccess returns an [errProfileAccessDenied] if the current user
@@ -251,12 +328,6 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
}
}
// Reset unloads the current profile, if any.
func (pm *profileManager) Reset() {
pm.currentUserID = ""
pm.NewProfile()
}
// 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].
@@ -288,13 +359,37 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
delete(pm.knownProfiles, p.ID())
}
}
pm.currentProfile = cp
// 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],
@@ -351,7 +446,20 @@ func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
// Update the current profile view to reflect the changes
// if the specified profile is the current profile.
if isCurrentProfile {
pm.currentProfile = lp.View()
// 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,
@@ -392,7 +500,33 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.Prof
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() != "" {
@@ -477,42 +611,32 @@ func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, e
return pm.loadSavedPrefs(p.Key())
}
// SwitchProfile switches to the profile with the given id.
// 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) SwitchProfile(id ipn.ProfileID) error {
metricSwitchProfile.Add(1)
kp, ok := pm.knownProfiles[id]
if !ok {
return errProfileNotFound
func (pm *profileManager) SwitchToProfileByID(id ipn.ProfileID) (_ ipn.LoginProfileView, changed bool, err error) {
if id == pm.currentProfile.ID() {
return pm.currentProfile, false, nil
}
if pm.currentProfile.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() {
return nil
}
if err := pm.checkProfileAccess(kp); err != nil {
return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id)
}
prefs, err := pm.loadSavedPrefs(kp.Key())
profile, err := pm.ProfileByID(id)
if err != nil {
return err
return pm.currentProfile, false, err
}
pm.prefs = prefs
pm.updateHealth()
pm.currentProfile = kp
return pm.setProfileAsUserDefault(kp)
return pm.SwitchToProfile(profile)
}
// SwitchToDefaultProfile switches to the default (last used) profile for the current user.
// It creates a new one and switches to it if the current user does not have a default 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) SwitchToDefaultProfile() error {
if id := pm.DefaultUserProfileID(pm.currentUserID); id != "" {
return pm.SwitchProfile(id)
}
pm.NewProfileForUser(pm.currentUserID)
return nil
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.
@@ -529,8 +653,8 @@ func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView)
return pm.WriteState(k, []byte(profile.Key()))
}
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
bs, err := pm.store.ReadState(key)
func (pm *profileManager) loadSavedPrefs(k ipn.StateKey) (ipn.PrefsView, error) {
bs, err := pm.store.ReadState(k)
if err == ipn.ErrStateNotExist || len(bs) == 0 {
return defaultPrefs, nil
}
@@ -538,10 +662,28 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
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)
// if supported by the platform, create an empty hardware attestation key to use when deserializing
// to avoid type exceptions from json.Unmarshaling into an interface{}.
hw, _ := pm.newEmptyHardwareAttestationKey()
savedPrefs.Persist = &persist.Persist{
AttestationKey: hw,
}
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty())
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
// Try loading again, this time ignoring the AttestationKey contents.
// If that succeeds, there's something wrong with the underlying
// attestation key mechanism (most likely the TPM changed), but we
// should at least proceed with client startup.
origErr := err
savedPrefs.Persist.AttestationKey = &noopAttestationKey{}
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %w", err)
} else {
pm.logf("failed to parse savedPrefs with attestation key (error: %v) but parsing without the attestation key succeeded; will proceed without using the old attestation key", origErr)
}
}
pm.logf("using backend prefs for %q: %v", k, savedPrefs.Pretty())
// Ignore any old stored preferences for https://login.tailscale.com
// as the control server that would override the new default of
@@ -558,7 +700,7 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
// 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() {
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !feature.CanAutoUpdate() {
savedPrefs.AutoUpdate.Apply.Clear()
}
@@ -590,7 +732,6 @@ var errProfileAccessDenied = errors.New("profile access denied")
// 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()
}
@@ -610,7 +751,7 @@ func (pm *profileManager) deleteCurrentProfile() error {
}
if pm.currentProfile.ID() == "" {
// Deleting the in-memory only new profile, just create a new one.
pm.NewProfile()
pm.SwitchToNewProfile()
return nil
}
return pm.deleteProfileNoPermCheck(pm.currentProfile)
@@ -620,12 +761,13 @@ func (pm *profileManager) deleteCurrentProfile() error {
// 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.NewProfile()
pm.SwitchToNewProfile()
}
if err := pm.WriteState(profile.Key(), nil); err != nil {
return err
}
delete(pm.knownProfiles, profile.ID())
metricDeleteProfile.Add(1)
return pm.writeKnownProfiles()
}
@@ -637,7 +779,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
currentProfileDeleted := false
writeKnownProfiles := func() error {
if currentProfileDeleted || pm.currentProfile.ID() == "" {
pm.NewProfile()
pm.SwitchToNewProfile()
}
return pm.writeKnownProfiles()
}
@@ -666,6 +808,7 @@ func (pm *profileManager) writeKnownProfiles() error {
if err != nil {
return err
}
metricProfileCount.Set(int64(len(pm.knownProfiles)))
return pm.WriteState(ipn.KnownProfilesStateKey, b)
}
@@ -676,45 +819,25 @@ func (pm *profileManager) updateHealth() {
pm.health.SetAutoUpdatePrefs(pm.prefs.AutoUpdate().Check, pm.prefs.AutoUpdate().Apply)
}
// NewProfile creates and switches to a new unnamed profile. The new profile is
// 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) NewProfile() {
pm.NewProfileForUser(pm.currentUserID)
func (pm *profileManager) SwitchToNewProfile() {
pm.SwitchToNewProfileForUser(pm.currentUserID)
}
// NewProfileForUser is like [profileManager.NewProfile], but it switches to the
// 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) NewProfileForUser(uid ipn.WindowsUserID) {
pm.currentUserID = uid
metricNewProfile.Add(1)
pm.prefs = defaultPrefs
pm.updateHealth()
newProfile := &ipn.LoginProfile{LocalUserID: uid}
pm.currentProfile = newProfile.View()
func (pm *profileManager) SwitchToNewProfileForUser(uid ipn.WindowsUserID) {
pm.SwitchToProfile(pm.NewProfileForUser(uid))
}
// newProfileWithPrefs creates a new profile with the specified prefs and assigns
// the specified uid as the profile owner. If switchNow is true, it switches to the
// newly created profile immediately. It returns the newly created profile on success,
// or an error on failure.
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) {
metricNewProfile.Add(1)
// zeroProfile is a read-only view of a new, empty profile that is not persisted to the store.
var zeroProfile = (&ipn.LoginProfile{}).View()
profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
if err != nil {
return ipn.LoginProfileView{}, err
}
if switchNow {
pm.currentProfile = profile
pm.prefs = prefs.AsStruct().View()
pm.updateHealth()
if err := pm.setProfileAsUserDefault(profile); err != nil {
return ipn.LoginProfileView{}, err
}
}
return profile, nil
// 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
@@ -742,7 +865,10 @@ func (pm *profileManager) CurrentPrefs() ipn.PrefsView {
// 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
testenv.AssertInTest()
bus := eventbus.New()
defer bus.Close()
ht := health.NewTracker(bus) // in tests, don't care about the health status
pm, err := newProfileManager(store, logf, ht)
if err != nil {
return ipn.PrefsView{}, err
@@ -798,35 +924,20 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
return nil, err
}
metricProfileCount.Set(int64(len(knownProfiles)))
pm := &profileManager{
goos: goos,
store: store,
knownProfiles: knownProfiles,
logf: logf,
health: ht,
goos: goos,
store: store,
knownProfiles: knownProfiles,
logf: logf,
health: ht,
newEmptyHardwareAttestationKey: key.NewEmptyHardwareAttestationKey,
}
var initialProfile ipn.LoginProfileView
if stateKey != "" {
for _, v := range knownProfiles {
if v.Key() == stateKey {
pm.currentProfile = v
}
}
if !pm.currentProfile.Valid() {
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
pm.currentUserID = ipn.WindowsUserID(suf)
}
pm.NewProfile()
} else {
pm.currentUserID = pm.currentProfile.LocalUserID()
}
prefs, err := pm.loadSavedPrefs(stateKey)
if err != nil {
return nil, err
}
if err := pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefs); err != nil {
return nil, err
}
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.
@@ -837,17 +948,24 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
} 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 _, err := pm.migrateFromLegacyPrefs(pm.currentUserID, true); err != nil {
return nil, err
}
} else {
pm.NewProfile()
}
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, switchNow bool) (ipn.LoginProfileView, error) {
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID) (ipn.LoginProfileView, error) {
metricMigration.Add(1)
sentinel, prefs, err := pm.loadLegacyPrefs(uid)
if err != nil {
@@ -855,7 +973,7 @@ func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNo
return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
}
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
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)
@@ -877,8 +995,27 @@ var (
metricSwitchProfile = clientmetric.NewCounter("profiles_switch")
metricDeleteProfile = clientmetric.NewCounter("profiles_delete")
metricDeleteAllProfile = clientmetric.NewCounter("profiles_delete_all")
metricProfileCount = clientmetric.NewGauge("profiles_count")
metricMigration = clientmetric.NewCounter("profiles_migration")
metricMigrationError = clientmetric.NewCounter("profiles_migration_error")
metricMigrationSuccess = clientmetric.NewCounter("profiles_migration_success")
)
// noopAttestationKey is a key.HardwareAttestationKey that always successfully
// unmarshals as a zero key.
type noopAttestationKey struct{}
func (n noopAttestationKey) Public() crypto.PublicKey {
panic("noopAttestationKey.Public should not be called; missing IsZero check somewhere?")
}
func (n noopAttestationKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
panic("noopAttestationKey.Sign should not be called; missing IsZero check somewhere?")
}
func (n noopAttestationKey) MarshalJSON() ([]byte, error) { return nil, nil }
func (n noopAttestationKey) UnmarshalJSON([]byte) error { return nil }
func (n noopAttestationKey) Close() error { return nil }
func (n noopAttestationKey) Clone() key.HardwareAttestationKey { return n }
func (n noopAttestationKey) IsZero() bool { return true }