mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
dd50dcd067
We would end up with duplicate profiles for the node as the UserID would have chnaged. In order to correctly deduplicate profiles, we need to look at both the UserID and the NodeID. A single machine can only ever have 1 profile per NodeID and 1 profile per UserID. Note: UserID of a Node can change when the node is tagged/untagged, and the NodeID of a device can change when the node is deleted so we need to check for both. Updates #713 Signed-off-by: Maisem Ali <maisem@tailscale.com>
509 lines
14 KiB
Go
509 lines
14 KiB
Go
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"runtime"
|
|
"time"
|
|
|
|
"golang.org/x/exp/slices"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/strs"
|
|
"tailscale.com/version"
|
|
)
|
|
|
|
// profileManager is a wrapper around a StateStore that manages
|
|
// multiple profiles and the current profile.
|
|
type profileManager struct {
|
|
store ipn.StateStore
|
|
logf logger.Logf
|
|
|
|
currentUserID string // only used on Windows
|
|
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
|
|
currentProfile *ipn.LoginProfile
|
|
prefs ipn.PrefsView
|
|
|
|
// isNewProfile is a sentinel value that indicates that the
|
|
// current profile is new and has not been saved to disk yet.
|
|
// It is reset to false after a call to SetPrefs with a filled
|
|
// in LoginName.
|
|
isNewProfile bool
|
|
}
|
|
|
|
// CurrentUser returns the current user ID. It is only non-empty on
|
|
// Windows where we have a multi-user system.
|
|
func (pm *profileManager) CurrentUser() string {
|
|
return pm.currentUserID
|
|
}
|
|
|
|
// SetCurrentUser sets the current user ID. The uid is only non-empty
|
|
// on Windows where we have a multi-user system.
|
|
func (pm *profileManager) SetCurrentUser(uid string) error {
|
|
if pm.currentUserID == uid {
|
|
return nil
|
|
}
|
|
cpk := ipn.CurrentProfileKey(uid)
|
|
if b, err := pm.store.ReadState(cpk); err == nil {
|
|
pk := ipn.StateKey(string(b))
|
|
prefs, err := pm.loadSavedPrefs(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pm.currentProfile = pm.findProfileByKey(pk)
|
|
pm.prefs = prefs
|
|
pm.isNewProfile = false
|
|
} else if err == ipn.ErrStateNotExist {
|
|
pm.NewProfile()
|
|
} else {
|
|
return err
|
|
}
|
|
pm.currentUserID = uid
|
|
return nil
|
|
}
|
|
|
|
func (pm *profileManager) findProfilesByNodeID(nodeID tailcfg.StableNodeID) []*ipn.LoginProfile {
|
|
if nodeID.IsZero() {
|
|
return nil
|
|
}
|
|
var out []*ipn.LoginProfile
|
|
for _, p := range pm.knownProfiles {
|
|
if p.NodeID == nodeID {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (pm *profileManager) findProfilesByUserID(userID tailcfg.UserID) []*ipn.LoginProfile {
|
|
if userID.IsZero() {
|
|
return nil
|
|
}
|
|
var out []*ipn.LoginProfile
|
|
for _, p := range pm.knownProfiles {
|
|
if p.UserProfile.ID == userID {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
|
|
for _, p := range pm.knownProfiles {
|
|
if p.Name == name {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
|
|
for _, p := range pm.knownProfiles {
|
|
if p.Key == key {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pm *profileManager) setUnattendedModeAsConfigured() error {
|
|
if pm.currentUserID == "" {
|
|
return nil
|
|
}
|
|
|
|
if pm.prefs.ForceDaemon() {
|
|
return pm.store.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
|
|
} else {
|
|
return pm.store.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.
|
|
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
|
|
prefs := prefsIn.AsStruct().View()
|
|
newPersist := prefs.Persist()
|
|
if newPersist == nil || newPersist.LoginName == "" {
|
|
return pm.setPrefsLocked(prefs)
|
|
}
|
|
up := newPersist.UserProfile
|
|
if up.LoginName == "" {
|
|
// Backwards compatibility with old prefs files.
|
|
up.LoginName = newPersist.LoginName
|
|
} else {
|
|
newPersist.LoginName = up.LoginName
|
|
}
|
|
if up.DisplayName == "" {
|
|
up.DisplayName = up.LoginName
|
|
}
|
|
cp := pm.currentProfile
|
|
if pm.isNewProfile {
|
|
pm.isNewProfile = false
|
|
// Check if we already have a profile for this user.
|
|
existing := pm.findProfilesByUserID(newPersist.UserProfile.ID)
|
|
// Also check if we have a profile with the same NodeID.
|
|
existing = append(existing, pm.findProfilesByNodeID(newPersist.NodeID)...)
|
|
if len(existing) == 0 {
|
|
cp.ID, cp.Key = newUnusedID(pm.knownProfiles)
|
|
} else {
|
|
// Only one profile per user/nodeID should exist.
|
|
for _, p := range existing[1:] {
|
|
// Best effort cleanup.
|
|
pm.DeleteProfile(p.ID)
|
|
}
|
|
cp = existing[0]
|
|
}
|
|
cp.LocalUserID = pm.currentUserID
|
|
}
|
|
cp.UserProfile = newPersist.UserProfile
|
|
cp.NodeID = newPersist.NodeID
|
|
cp.Name = up.LoginName
|
|
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); 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.isNewProfile {
|
|
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.store.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 {
|
|
var profiles []ipn.LoginProfile
|
|
for _, p := range pm.knownProfiles {
|
|
if p.LocalUserID == pm.currentUserID {
|
|
profiles = append(profiles, *p)
|
|
}
|
|
}
|
|
slices.SortFunc(profiles, func(a, b ipn.LoginProfile) bool {
|
|
return a.Name < b.Name
|
|
})
|
|
return profiles
|
|
}
|
|
|
|
// 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
|
|
pm.isNewProfile = false
|
|
return pm.setAsUserSelectedProfileLocked()
|
|
}
|
|
|
|
func (pm *profileManager) setAsUserSelectedProfileLocked() error {
|
|
k := ipn.CurrentProfileKey(pm.currentUserID)
|
|
return pm.store.WriteState(k, []byte(pm.currentProfile.Key))
|
|
}
|
|
|
|
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
|
bs, err := pm.store.ReadState(key)
|
|
if err != nil {
|
|
if err == ipn.ErrStateNotExist {
|
|
return emptyPrefs, 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 == "" && pm.isNewProfile {
|
|
// 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.store.WriteState(kp.Key, nil); err != nil {
|
|
return err
|
|
}
|
|
delete(pm.knownProfiles, id)
|
|
return pm.writeKnownProfiles()
|
|
}
|
|
|
|
func (pm *profileManager) writeKnownProfiles() error {
|
|
b, err := json.Marshal(pm.knownProfiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pm.store.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 = emptyPrefs
|
|
pm.isNewProfile = true
|
|
pm.currentProfile = &ipn.LoginProfile{}
|
|
}
|
|
|
|
// emptyPrefs is the default prefs for a new profile.
|
|
var emptyPrefs = func() ipn.PrefsView {
|
|
prefs := ipn.NewPrefs()
|
|
prefs.WantRunning = false
|
|
return prefs.View()
|
|
}()
|
|
|
|
// 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.
|
|
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.
|
|
// If a state key is provided, it will be used to load the current profile.
|
|
func newProfileManager(store ipn.StateStore, logf logger.Logf, stateKey ipn.StateKey) (*profileManager, error) {
|
|
return newProfileManagerWithGOOS(store, logf, stateKey, runtime.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, stateKey ipn.StateKey, goos string) (*profileManager, error) {
|
|
if stateKey == "" {
|
|
var err error
|
|
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 := strs.CutPrefix(string(stateKey), "user-"); ok {
|
|
pm.currentUserID = 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
|
|
}
|
|
} else if len(knownProfiles) == 0 && goos != "windows" {
|
|
// No known profiles, try a migration.
|
|
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()
|
|
k := ipn.LegacyGlobalDaemonStateKey
|
|
switch {
|
|
case runtime.GOOS == "ios":
|
|
k = "ipn-go-bridge"
|
|
case version.IsSandboxedMacOS():
|
|
k = "ipn-go-bridge"
|
|
case runtime.GOOS == "android":
|
|
k = "ipn-android"
|
|
}
|
|
prefs, err := pm.loadSavedPrefs(k)
|
|
if err != nil {
|
|
metricMigrationError.Add(1)
|
|
return fmt.Errorf("calling ReadState on state store: %w", err)
|
|
}
|
|
pm.logf("migrating %q profile to new format", k)
|
|
if err := pm.SetPrefs(prefs); err != nil {
|
|
metricMigrationError.Add(1)
|
|
return fmt.Errorf("migrating _daemon profile: %w", err)
|
|
}
|
|
// Do not delete the old state key, as we may be downgraded to an
|
|
// older version that still relies on it.
|
|
metricMigrationSuccess.Add(1)
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
metricNewProfile = clientmetric.NewCounter("profiles_new")
|
|
metricSwitchProfile = clientmetric.NewCounter("profiles_switch")
|
|
metricDeleteProfile = clientmetric.NewCounter("profiles_delete")
|
|
|
|
metricMigration = clientmetric.NewCounter("profiles_migration")
|
|
metricMigrationError = clientmetric.NewCounter("profiles_migration_error")
|
|
metricMigrationSuccess = clientmetric.NewCounter("profiles_migration_success")
|
|
)
|