mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-10 09:48:35 +00:00

updates tailscale/corp#28092 Adds metrics for various client events: * Enabling an exit node * Enabling a mullvad exit node * Enabling a preferred exit node * Setting WantRunning to true/false * Requesting a bug report ID * Profile counts * Profile deletions * Captive portal detection Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
973 lines
37 KiB
Go
973 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 {
|
|
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())
|
|
metricDeleteProfile.Add(1)
|
|
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
|
|
}
|
|
metricProfileCount.Set(int64(len(pm.knownProfiles)))
|
|
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
|
|
}
|
|
|
|
metricProfileCount.Set(int64(len(knownProfiles)))
|
|
|
|
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")
|
|
metricProfileCount = clientmetric.NewGauge("profiles_count")
|
|
|
|
metricMigration = clientmetric.NewCounter("profiles_migration")
|
|
metricMigrationError = clientmetric.NewCounter("profiles_migration_error")
|
|
metricMigrationSuccess = clientmetric.NewCounter("profiles_migration_success")
|
|
)
|