mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
2dc0645368
This PR starts to persist the NetMap tailnet name in SetPrefs so that tailscaled clients can use this value to disambiguate fast user switching from one tailnet to another that are under the same exact login. We will also try to backfill this information during backend starts and profile switches so that users don't have to re-authenticate their profile. The first client to use this new information is the CLI in 'tailscale switch -list' which now uses text/tabwriter to display the ID, Tailnet, and Account. Since account names are ambiguous, we allow the user to pass 'tailscale switch ID' to specify the exact tailnet they want to switch to. Updates #9286 Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
627 lines
18 KiB
Go
627 lines
18 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/netip"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/cmpx"
|
|
"tailscale.com/util/winutil"
|
|
)
|
|
|
|
var errAlreadyMigrated = errors.New("profile migration already completed")
|
|
|
|
var debug = envknob.RegisterBool("TS_DEBUG_PROFILES")
|
|
|
|
// profileManager is a wrapper around a StateStore that manages
|
|
// multiple profiles and the current profile.
|
|
//
|
|
// It is not safe for concurrent use.
|
|
type profileManager struct {
|
|
store ipn.StateStore
|
|
logf logger.Logf
|
|
|
|
currentUserID ipn.WindowsUserID
|
|
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil
|
|
currentProfile *ipn.LoginProfile // always non-nil
|
|
prefs ipn.PrefsView // always Valid.
|
|
}
|
|
|
|
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. The uid is only non-empty
|
|
// on Windows where we have a multi-user system.
|
|
func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error {
|
|
if pm.currentUserID == uid {
|
|
return nil
|
|
}
|
|
prev := pm.currentUserID
|
|
pm.currentUserID = uid
|
|
if uid == "" && prev != "" {
|
|
// This is a local user logout, or app shutdown.
|
|
// Clear the current profile.
|
|
pm.NewProfile()
|
|
return nil
|
|
}
|
|
|
|
// Read the CurrentProfileKey from the store which stores
|
|
// the selected profile for the current user.
|
|
b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid)))
|
|
pm.dlogf("SetCurrentUserID: ReadState(%q) = %v, %v", string(uid), len(b), err)
|
|
if err == ipn.ErrStateNotExist || len(b) == 0 {
|
|
if runtime.GOOS == "windows" {
|
|
pm.dlogf("SetCurrentUserID: windows: migrating from legacy preferences")
|
|
if err := pm.migrateFromLegacyPrefs(); err != nil && !errors.Is(err, errAlreadyMigrated) {
|
|
return err
|
|
}
|
|
} else {
|
|
pm.NewProfile()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Now attempt to load the profile using the key we just read.
|
|
pk := ipn.StateKey(string(b))
|
|
prof := pm.findProfileByKey(pk)
|
|
if prof == nil {
|
|
pm.dlogf("SetCurrentUserID: no profile found for key: %q", pk)
|
|
pm.NewProfile()
|
|
return nil
|
|
}
|
|
prefs, err := pm.loadSavedPrefs(pk)
|
|
if err != nil {
|
|
pm.NewProfile()
|
|
return err
|
|
}
|
|
pm.currentProfile = prof
|
|
pm.prefs = prefs
|
|
return nil
|
|
}
|
|
|
|
// allProfiles returns all profiles that belong to the currentUserID.
|
|
// The returned profiles are sorted by Name.
|
|
func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
|
|
for _, p := range pm.knownProfiles {
|
|
if p.LocalUserID == pm.currentUserID {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
|
|
return cmpx.Compare(a.Name, b.Name)
|
|
})
|
|
return out
|
|
}
|
|
|
|
// matchingProfiles returns all profiles that match the given predicate and
|
|
// belong to the currentUserID.
|
|
// The returned profiles are sorted by Name.
|
|
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
|
|
all := pm.allProfiles()
|
|
out = all[:0]
|
|
for _, p := range all {
|
|
if f(p) {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// findMatchinProfiles returns all profiles that represent the same node/user as
|
|
// prefs.
|
|
// The returned profiles are sorted by Name.
|
|
func (pm *profileManager) findMatchingProfiles(prefs *ipn.Prefs) []*ipn.LoginProfile {
|
|
return pm.matchingProfiles(func(p *ipn.LoginProfile) 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.
|
|
func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
|
|
p := pm.findProfileByName(name)
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
return p.ID
|
|
}
|
|
|
|
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
|
|
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
|
|
return p.Name == name
|
|
})
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
if len(out) > 1 {
|
|
pm.logf("[unxpected] multiple profiles with the same name")
|
|
}
|
|
return out[0]
|
|
}
|
|
|
|
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
|
|
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
|
|
return p.Key == key
|
|
})
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
if len(out) > 1 {
|
|
pm.logf("[unxpected] multiple profiles with the same key")
|
|
}
|
|
return out[0]
|
|
}
|
|
|
|
func (pm *profileManager) setUnattendedModeAsConfigured() error {
|
|
if pm.currentUserID == "" {
|
|
return nil
|
|
}
|
|
|
|
if pm.prefs.ForceDaemon() {
|
|
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
|
|
} else {
|
|
return pm.WriteState(ipn.ServerModeStartKey, nil)
|
|
}
|
|
}
|
|
|
|
// Reset unloads the current profile, if any.
|
|
func (pm *profileManager) Reset() {
|
|
pm.currentUserID = ""
|
|
pm.NewProfile()
|
|
}
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
// SetPrefs sets the current profile's prefs to the provided value.
|
|
// It also saves the prefs to the StateStore. It stores a copy of the
|
|
// provided prefs, which may be accessed via CurrentPrefs.
|
|
//
|
|
// 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 {
|
|
prefs := prefsIn.AsStruct()
|
|
newPersist := prefs.Persist
|
|
if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" {
|
|
// We don't know anything about this profile, so ignore it for now.
|
|
return pm.setPrefsLocked(prefs.View())
|
|
}
|
|
up := newPersist.UserProfile
|
|
if up.DisplayName == "" {
|
|
up.DisplayName = up.LoginName
|
|
}
|
|
cp := pm.currentProfile
|
|
// Check if we already have an existing profile that matches the user/node.
|
|
if existing := pm.findMatchingProfiles(prefs); 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 below.
|
|
delete(pm.knownProfiles, p.ID)
|
|
}
|
|
} else if cp.ID == "" {
|
|
// We didn't have an existing profile, so create a new one.
|
|
cp.ID, cp.Key = newUnusedID(pm.knownProfiles)
|
|
cp.LocalUserID = pm.currentUserID
|
|
} else {
|
|
// This means that there was a force-reauth as a new node that
|
|
// we haven't seen before.
|
|
}
|
|
|
|
if prefs.ProfileName != "" {
|
|
cp.Name = prefs.ProfileName
|
|
} else {
|
|
cp.Name = up.LoginName
|
|
}
|
|
cp.ControlURL = prefs.ControlURL
|
|
cp.UserProfile = newPersist.UserProfile
|
|
cp.NodeID = newPersist.NodeID
|
|
cp.NetworkProfile = np
|
|
pm.knownProfiles[cp.ID] = cp
|
|
pm.currentProfile = cp
|
|
if err := pm.writeKnownProfiles(); err != nil {
|
|
return err
|
|
}
|
|
if err := pm.setAsUserSelectedProfileLocked(); err != nil {
|
|
return err
|
|
}
|
|
if err := pm.setPrefsLocked(prefs.View()); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (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)
|
|
}
|
|
}
|
|
|
|
// setPrefsLocked sets the current profile's prefs to the provided value.
|
|
// It also saves the prefs to the StateStore, if the current profile
|
|
// is not new.
|
|
func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error {
|
|
pm.prefs = clonedPrefs
|
|
if pm.currentProfile.ID == "" {
|
|
return nil
|
|
}
|
|
if err := pm.writePrefsToStore(pm.currentProfile.Key, pm.prefs); err != nil {
|
|
return err
|
|
}
|
|
return pm.setUnattendedModeAsConfigured()
|
|
}
|
|
|
|
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.
|
|
func (pm *profileManager) Profiles() []ipn.LoginProfile {
|
|
allProfiles := pm.allProfiles()
|
|
out := make([]ipn.LoginProfile, 0, len(allProfiles))
|
|
for _, p := range allProfiles {
|
|
out = append(out, *p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SwitchProfile switches to the profile with the given id.
|
|
// If the profile is not known, it returns an errProfileNotFound.
|
|
func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
|
|
metricSwitchProfile.Add(1)
|
|
|
|
kp, ok := pm.knownProfiles[id]
|
|
if !ok {
|
|
return errProfileNotFound
|
|
}
|
|
|
|
if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() {
|
|
return nil
|
|
}
|
|
if kp.LocalUserID != pm.currentUserID {
|
|
return fmt.Errorf("profile %q is not owned by current user", id)
|
|
}
|
|
prefs, err := pm.loadSavedPrefs(kp.Key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pm.prefs = prefs
|
|
pm.currentProfile = kp
|
|
return pm.setAsUserSelectedProfileLocked()
|
|
}
|
|
|
|
func (pm *profileManager) setAsUserSelectedProfileLocked() error {
|
|
k := ipn.CurrentProfileKey(string(pm.currentUserID))
|
|
return pm.WriteState(k, []byte(pm.currentProfile.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, err := ipn.PrefsFromBytes(bs)
|
|
if err != nil {
|
|
return ipn.PrefsView{}, fmt.Errorf("PrefsFromBytes: %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 = ""
|
|
}
|
|
return savedPrefs.View(), nil
|
|
}
|
|
|
|
// CurrentProfile returns the current LoginProfile.
|
|
// The value may be zero if the profile is not persisted.
|
|
func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
|
|
return *pm.currentProfile
|
|
}
|
|
|
|
// errProfileNotFound is returned by methods that accept a ProfileID.
|
|
var errProfileNotFound = errors.New("profile not found")
|
|
|
|
// DeleteProfile removes the profile with the given id. It returns
|
|
// errProfileNotFound if the profile does not exist.
|
|
// If the profile is the current profile, it is the equivalent of
|
|
// calling NewProfile() followed by DeleteProfile(id). This is
|
|
// useful for deleting the last profile. In other cases, it is
|
|
// recommended to call SwitchProfile() first.
|
|
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
|
|
metricDeleteProfile.Add(1)
|
|
|
|
if id == "" {
|
|
// Deleting the in-memory only new profile, just create a new one.
|
|
pm.NewProfile()
|
|
return nil
|
|
}
|
|
kp, ok := pm.knownProfiles[id]
|
|
if !ok {
|
|
return errProfileNotFound
|
|
}
|
|
if kp.ID == pm.currentProfile.ID {
|
|
pm.NewProfile()
|
|
}
|
|
if err := pm.WriteState(kp.Key, nil); err != nil {
|
|
return err
|
|
}
|
|
delete(pm.knownProfiles, id)
|
|
return pm.writeKnownProfiles()
|
|
}
|
|
|
|
// DeleteAllProfiles removes all known profiles and switches to a new empty
|
|
// profile.
|
|
func (pm *profileManager) DeleteAllProfiles() error {
|
|
metricDeleteAllProfile.Add(1)
|
|
|
|
for _, kp := range pm.knownProfiles {
|
|
if err := pm.WriteState(kp.Key, nil); err != nil {
|
|
// Write to remove references to profiles we've already deleted, but
|
|
// return the original error.
|
|
pm.writeKnownProfiles()
|
|
return err
|
|
}
|
|
delete(pm.knownProfiles, kp.ID)
|
|
}
|
|
pm.NewProfile()
|
|
return pm.writeKnownProfiles()
|
|
}
|
|
|
|
func (pm *profileManager) writeKnownProfiles() error {
|
|
b, err := json.Marshal(pm.knownProfiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pm.WriteState(ipn.KnownProfilesStateKey, b)
|
|
}
|
|
|
|
// NewProfile creates and switches to a new unnamed profile. The new profile is
|
|
// not persisted until SetPrefs is called with a logged-in user.
|
|
func (pm *profileManager) NewProfile() {
|
|
metricNewProfile.Add(1)
|
|
|
|
pm.prefs = defaultPrefs
|
|
pm.currentProfile = &ipn.LoginProfile{}
|
|
}
|
|
|
|
// defaultPrefs is the default prefs for a new profile.
|
|
var defaultPrefs = func() ipn.PrefsView {
|
|
prefs := ipn.NewPrefs()
|
|
prefs.LoggedOut = true
|
|
prefs.WantRunning = false
|
|
|
|
controlURL, _ := winutil.GetPolicyString("LoginURL")
|
|
prefs.ControlURL = controlURL
|
|
|
|
prefs.ExitNodeIP = resolveExitNodeIP(netip.Addr{})
|
|
|
|
// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
|
|
// backend), so this has to convert between the two conventions.
|
|
shieldsUp, _ := winutil.GetPolicyString("AllowIncomingConnections")
|
|
prefs.ShieldsUp = shieldsUp == "never"
|
|
forceDaemon, _ := winutil.GetPolicyString("UnattendedMode")
|
|
prefs.ForceDaemon = forceDaemon == "always"
|
|
|
|
return prefs.View()
|
|
}()
|
|
|
|
func resolveExitNodeIP(defIP netip.Addr) (ret netip.Addr) {
|
|
ret = defIP
|
|
if exitNode, _ := winutil.GetPolicyString("ExitNodeIP"); exitNode != "" {
|
|
if ip, err := netip.ParseAddr(exitNode); err == nil {
|
|
ret = ip
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Store returns the 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) {
|
|
pm, err := newProfileManager(store, logf)
|
|
if err != nil {
|
|
return ipn.PrefsView{}, err
|
|
}
|
|
return pm.CurrentPrefs(), nil
|
|
}
|
|
|
|
// newProfileManager creates a new ProfileManager using the provided StateStore.
|
|
// It also loads the list of known profiles from the StateStore.
|
|
func newProfileManager(store ipn.StateStore, logf logger.Logf) (*profileManager, error) {
|
|
return newProfileManagerWithGOOS(store, logf, 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.LoginProfile, error) {
|
|
var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
|
|
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.LoginProfile)
|
|
default:
|
|
return nil, fmt.Errorf("calling ReadState on state store: %w", err)
|
|
}
|
|
return knownProfiles, nil
|
|
}
|
|
|
|
func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, 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{
|
|
store: store,
|
|
knownProfiles: knownProfiles,
|
|
logf: logf,
|
|
}
|
|
|
|
if stateKey != "" {
|
|
for _, v := range knownProfiles {
|
|
if v.Key == stateKey {
|
|
pm.currentProfile = v
|
|
}
|
|
}
|
|
if pm.currentProfile == nil {
|
|
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.setPrefsLocked(prefs); err != nil {
|
|
return nil, err
|
|
}
|
|
// 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 err := pm.migrateFromLegacyPrefs(); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
pm.NewProfile()
|
|
}
|
|
|
|
return pm, nil
|
|
}
|
|
|
|
func (pm *profileManager) migrateFromLegacyPrefs() error {
|
|
metricMigration.Add(1)
|
|
pm.NewProfile()
|
|
sentinel, prefs, err := pm.loadLegacyPrefs()
|
|
if err != nil {
|
|
metricMigrationError.Add(1)
|
|
return fmt.Errorf("load legacy prefs: %w", err)
|
|
}
|
|
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
|
|
if err := pm.SetPrefs(prefs, ipn.NetworkProfile{}); err != nil {
|
|
metricMigrationError.Add(1)
|
|
return fmt.Errorf("migrating _daemon profile: %w", err)
|
|
}
|
|
pm.completeMigration(sentinel)
|
|
pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
|
|
metricMigrationSuccess.Add(1)
|
|
return nil
|
|
}
|
|
|
|
func (pm *profileManager) requiresBackfill() bool {
|
|
return pm != nil &&
|
|
pm.currentProfile != nil &&
|
|
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")
|
|
)
|