ipn/ipnlocal: fix profile duplication

We would only look for duplicate profiles when a new login
occurred but when using `--force-reauth` we could switch
users which would end up with duplicate profiles.

Updates #7726

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/8814/head
Maisem Ali 10 months ago committed by Maisem Ali
parent 500b9579d5
commit 3e255d76e1

@ -16,9 +16,9 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/cmpx"
"tailscale.com/util/winutil" "tailscale.com/util/winutil"
) )
@ -33,15 +33,9 @@ type profileManager struct {
logf logger.Logf logf logger.Logf
currentUserID ipn.WindowsUserID currentUserID ipn.WindowsUserID
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil
currentProfile *ipn.LoginProfile // always non-nil currentProfile *ipn.LoginProfile // always non-nil
prefs ipn.PrefsView // always Valid. prefs ipn.PrefsView // always Valid.
// 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
} }
func (pm *profileManager) dlogf(format string, args ...any) { func (pm *profileManager) dlogf(format string, args ...any) {
@ -107,40 +101,45 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error {
} }
pm.currentProfile = prof pm.currentProfile = prof
pm.prefs = prefs pm.prefs = prefs
pm.isNewProfile = false
return nil return nil
} }
// matchingProfiles returns all profiles that match the given predicate and // allProfiles returns all profiles that belong to the currentUserID.
// belong to the currentUserID. // The returned profiles are sorted by Name.
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) { func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
for _, p := range pm.knownProfiles { for _, p := range pm.knownProfiles {
if p.LocalUserID == pm.currentUserID && f(p) { if p.LocalUserID == pm.currentUserID {
out = append(out, p) out = append(out, p)
} }
} }
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
return cmpx.Compare(a.Name, b.Name)
})
return out return out
} }
// findProfilesByNodeID returns all profiles that have the provided nodeID and // matchingProfiles returns all profiles that match the given predicate and
// belong to the same control server. // belong to the currentUserID.
func (pm *profileManager) findProfilesByNodeID(controlURL string, nodeID tailcfg.StableNodeID) []*ipn.LoginProfile { // The returned profiles are sorted by Name.
if nodeID.IsZero() { func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
return nil all := pm.allProfiles()
out = all[:0]
for _, p := range all {
if f(p) {
out = append(out, p)
}
} }
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool { return out
return p.NodeID == nodeID && p.ControlURL == controlURL
})
} }
// findProfilesByUserID returns all profiles that have the provided userID and // findMatchinProfiles returns all profiles that represent the same node/user as
// belong to the same control server. // prefs.
func (pm *profileManager) findProfilesByUserID(controlURL string, userID tailcfg.UserID) []*ipn.LoginProfile { // The returned profiles are sorted by Name.
if userID.IsZero() { func (pm *profileManager) findMatchingProfiles(prefs *ipn.Prefs) []*ipn.LoginProfile {
return nil
}
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool { return pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
return p.UserProfile.ID == userID && p.ControlURL == controlURL return p.ControlURL == prefs.ControlURL &&
(p.UserProfile.ID == prefs.Persist.UserProfile.ID ||
p.NodeID == prefs.Persist.NodeID)
}) })
} }
@ -206,40 +205,47 @@ func init() {
// It also saves the prefs to the StateStore. It stores a copy of the // It also saves the prefs to the StateStore. It stores a copy of the
// provided prefs, which may be accessed via CurrentPrefs. // provided prefs, which may be accessed via CurrentPrefs.
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error { func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
prefs := prefsIn.AsStruct().View() prefs := prefsIn.AsStruct()
newPersist := prefs.Persist().AsStruct() newPersist := prefs.Persist
if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" { if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" {
return pm.setPrefsLocked(prefs) // We don't know anything about this profile, so ignore it for now.
return pm.setPrefsLocked(prefs.View())
} }
up := newPersist.UserProfile up := newPersist.UserProfile
if up.DisplayName == "" { if up.DisplayName == "" {
up.DisplayName = up.LoginName up.DisplayName = up.LoginName
} }
cp := pm.currentProfile cp := pm.currentProfile
if pm.isNewProfile { // Check if we already have an existing profile that matches the user/node.
pm.isNewProfile = false if existing := pm.findMatchingProfiles(prefs); len(existing) > 0 {
// Check if we already have a profile for this user. // We already have a profile for this user/node we should reuse it. Also
existing := pm.findProfilesByUserID(prefs.ControlURL(), newPersist.UserProfile.ID) // cleanup any other duplicate profiles.
// Also check if we have a profile with the same NodeID. cp = existing[0]
existing = append(existing, pm.findProfilesByNodeID(prefs.ControlURL(), newPersist.NodeID)...) existing = existing[1:]
if len(existing) == 0 { for _, p := range existing {
cp.ID, cp.Key = newUnusedID(pm.knownProfiles) // Clear the state.
} else { if err := pm.store.WriteState(p.Key, nil); err != nil {
// Only one profile per user/nodeID should exist. // We couldn't delete the state, so keep the profile around.
for _, p := range existing[1:] { continue
// Best effort cleanup.
pm.DeleteProfile(p.ID)
} }
cp = existing[0] // 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 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() if prefs.ProfileName != "" {
cp.Name = prefs.ProfileName
} else { } else {
cp.Name = up.LoginName cp.Name = up.LoginName
} }
cp.ControlURL = prefs.ControlURL() cp.ControlURL = prefs.ControlURL
cp.UserProfile = newPersist.UserProfile cp.UserProfile = newPersist.UserProfile
cp.NodeID = newPersist.NodeID cp.NodeID = newPersist.NodeID
pm.knownProfiles[cp.ID] = cp pm.knownProfiles[cp.ID] = cp
@ -250,7 +256,7 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
if err := pm.setAsUserSelectedProfileLocked(); err != nil { if err := pm.setAsUserSelectedProfileLocked(); err != nil {
return err return err
} }
if err := pm.setPrefsLocked(prefs); err != nil { if err := pm.setPrefsLocked(prefs.View()); err != nil {
return err return err
} }
return nil return nil
@ -273,7 +279,7 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
// is not new. // is not new.
func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error { func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error {
pm.prefs = clonedPrefs pm.prefs = clonedPrefs
if pm.isNewProfile { if pm.currentProfile.ID == "" {
return nil return nil
} }
if err := pm.writePrefsToStore(pm.currentProfile.Key, pm.prefs); err != nil { if err := pm.writePrefsToStore(pm.currentProfile.Key, pm.prefs); err != nil {
@ -295,12 +301,9 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
// Profiles returns the list of known profiles. // Profiles returns the list of known profiles.
func (pm *profileManager) Profiles() []ipn.LoginProfile { func (pm *profileManager) Profiles() []ipn.LoginProfile {
profiles := pm.matchingProfiles(func(*ipn.LoginProfile) bool { return true }) allProfiles := pm.allProfiles()
slices.SortFunc(profiles, func(a, b *ipn.LoginProfile) int { out := make([]ipn.LoginProfile, 0, len(allProfiles))
return strings.Compare(a.Name, b.Name) for _, p := range allProfiles {
})
out := make([]ipn.LoginProfile, 0, len(profiles))
for _, p := range profiles {
out = append(out, *p) out = append(out, *p)
} }
return out return out
@ -328,7 +331,6 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
} }
pm.prefs = prefs pm.prefs = prefs
pm.currentProfile = kp pm.currentProfile = kp
pm.isNewProfile = false
return pm.setAsUserSelectedProfileLocked() return pm.setAsUserSelectedProfileLocked()
} }
@ -380,7 +382,7 @@ var errProfileNotFound = errors.New("profile not found")
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error { func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
metricDeleteProfile.Add(1) metricDeleteProfile.Add(1)
if id == "" && pm.isNewProfile { if id == "" {
// Deleting the in-memory only new profile, just create a new one. // Deleting the in-memory only new profile, just create a new one.
pm.NewProfile() pm.NewProfile()
return nil return nil
@ -431,7 +433,6 @@ func (pm *profileManager) NewProfile() {
metricNewProfile.Add(1) metricNewProfile.Add(1)
pm.prefs = defaultPrefs pm.prefs = defaultPrefs
pm.isNewProfile = true
pm.currentProfile = &ipn.LoginProfile{} pm.currentProfile = &ipn.LoginProfile{}
} }

@ -16,9 +16,7 @@ import (
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) { func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
k := ipn.LegacyGlobalDaemonStateKey k := ipn.LegacyGlobalDaemonStateKey
switch { switch {
case runtime.GOOS == "ios": case runtime.GOOS == "ios", version.IsSandboxedMacOS():
k = "ipn-go-bridge"
case version.IsSandboxedMacOS():
k = "ipn-go-bridge" k = "ipn-go-bridge"
case runtime.GOOS == "android": case runtime.GOOS == "android":
k = "ipn-android" k = "ipn-android"

@ -203,11 +203,8 @@ func TestProfileDupe(t *testing.T) {
{reauth, user1Node1}, {reauth, user1Node1},
}, },
profs: []*persist.Persist{ profs: []*persist.Persist{
// BUG: This is incorrect, and should be:
// user1Node1,
// user2Node2,
user1Node1,
user1Node1, user1Node1,
user2Node2,
}, },
}, },
{ {
@ -218,11 +215,8 @@ func TestProfileDupe(t *testing.T) {
{reauth, user2Node1}, {reauth, user2Node1},
}, },
profs: []*persist.Persist{ profs: []*persist.Persist{
// BUG: This is incorrect, and should be:
// user2Node1,
// user3Node3,
user1Node1,
user2Node1, user2Node1,
user3Node3,
}, },
}, },
{ {
@ -233,11 +227,8 @@ func TestProfileDupe(t *testing.T) {
{reauth, user1Node2}, {reauth, user1Node2},
}, },
profs: []*persist.Persist{ profs: []*persist.Persist{
// BUG: This is incorrect, and should be:
// user1Node2,
// user3Node3,
user1Node1,
user1Node2, user1Node2,
user3Node3,
}, },
}, },
{ {

Loading…
Cancel
Save