Skip to content

Commit 3e255d7

Browse files
Maisem Alimaisem
Maisem Ali
authored andcommitted
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 tailscale#7726 Signed-off-by: Maisem Ali <maisem@tailscale.com>
1 parent 500b957 commit 3e255d7

File tree

3 files changed

+66
-76
lines changed

3 files changed

+66
-76
lines changed

ipn/ipnlocal/profiles.go

Lines changed: 62 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import (
1616
"golang.org/x/exp/slices"
1717
"tailscale.com/envknob"
1818
"tailscale.com/ipn"
19-
"tailscale.com/tailcfg"
2019
"tailscale.com/types/logger"
2120
"tailscale.com/util/clientmetric"
21+
"tailscale.com/util/cmpx"
2222
"tailscale.com/util/winutil"
2323
)
2424

@@ -33,15 +33,9 @@ type profileManager struct {
3333
logf logger.Logf
3434

3535
currentUserID ipn.WindowsUserID
36-
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
37-
currentProfile *ipn.LoginProfile // always non-nil
38-
prefs ipn.PrefsView // always Valid.
39-
40-
// isNewProfile is a sentinel value that indicates that the
41-
// current profile is new and has not been saved to disk yet.
42-
// It is reset to false after a call to SetPrefs with a filled
43-
// in LoginName.
44-
isNewProfile bool
36+
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil
37+
currentProfile *ipn.LoginProfile // always non-nil
38+
prefs ipn.PrefsView // always Valid.
4539
}
4640

4741
func (pm *profileManager) dlogf(format string, args ...any) {
@@ -107,40 +101,45 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error {
107101
}
108102
pm.currentProfile = prof
109103
pm.prefs = prefs
110-
pm.isNewProfile = false
111104
return nil
112105
}
113106

114-
// matchingProfiles returns all profiles that match the given predicate and
115-
// belong to the currentUserID.
116-
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
107+
// allProfiles returns all profiles that belong to the currentUserID.
108+
// The returned profiles are sorted by Name.
109+
func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
117110
for _, p := range pm.knownProfiles {
118-
if p.LocalUserID == pm.currentUserID && f(p) {
111+
if p.LocalUserID == pm.currentUserID {
119112
out = append(out, p)
120113
}
121114
}
115+
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
116+
return cmpx.Compare(a.Name, b.Name)
117+
})
122118
return out
123119
}
124120

125-
// findProfilesByNodeID returns all profiles that have the provided nodeID and
126-
// belong to the same control server.
127-
func (pm *profileManager) findProfilesByNodeID(controlURL string, nodeID tailcfg.StableNodeID) []*ipn.LoginProfile {
128-
if nodeID.IsZero() {
129-
return nil
121+
// matchingProfiles returns all profiles that match the given predicate and
122+
// belong to the currentUserID.
123+
// The returned profiles are sorted by Name.
124+
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
125+
all := pm.allProfiles()
126+
out = all[:0]
127+
for _, p := range all {
128+
if f(p) {
129+
out = append(out, p)
130+
}
130131
}
131-
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
132-
return p.NodeID == nodeID && p.ControlURL == controlURL
133-
})
132+
return out
134133
}
135134

136-
// findProfilesByUserID returns all profiles that have the provided userID and
137-
// belong to the same control server.
138-
func (pm *profileManager) findProfilesByUserID(controlURL string, userID tailcfg.UserID) []*ipn.LoginProfile {
139-
if userID.IsZero() {
140-
return nil
141-
}
135+
// findMatchinProfiles returns all profiles that represent the same node/user as
136+
// prefs.
137+
// The returned profiles are sorted by Name.
138+
func (pm *profileManager) findMatchingProfiles(prefs *ipn.Prefs) []*ipn.LoginProfile {
142139
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
143-
return p.UserProfile.ID == userID && p.ControlURL == controlURL
140+
return p.ControlURL == prefs.ControlURL &&
141+
(p.UserProfile.ID == prefs.Persist.UserProfile.ID ||
142+
p.NodeID == prefs.Persist.NodeID)
144143
})
145144
}
146145

@@ -206,40 +205,47 @@ func init() {
206205
// It also saves the prefs to the StateStore. It stores a copy of the
207206
// provided prefs, which may be accessed via CurrentPrefs.
208207
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
209-
prefs := prefsIn.AsStruct().View()
210-
newPersist := prefs.Persist().AsStruct()
208+
prefs := prefsIn.AsStruct()
209+
newPersist := prefs.Persist
211210
if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" {
212-
return pm.setPrefsLocked(prefs)
211+
// We don't know anything about this profile, so ignore it for now.
212+
return pm.setPrefsLocked(prefs.View())
213213
}
214214
up := newPersist.UserProfile
215215
if up.DisplayName == "" {
216216
up.DisplayName = up.LoginName
217217
}
218218
cp := pm.currentProfile
219-
if pm.isNewProfile {
220-
pm.isNewProfile = false
221-
// Check if we already have a profile for this user.
222-
existing := pm.findProfilesByUserID(prefs.ControlURL(), newPersist.UserProfile.ID)
223-
// Also check if we have a profile with the same NodeID.
224-
existing = append(existing, pm.findProfilesByNodeID(prefs.ControlURL(), newPersist.NodeID)...)
225-
if len(existing) == 0 {
226-
cp.ID, cp.Key = newUnusedID(pm.knownProfiles)
227-
} else {
228-
// Only one profile per user/nodeID should exist.
229-
for _, p := range existing[1:] {
230-
// Best effort cleanup.
231-
pm.DeleteProfile(p.ID)
219+
// Check if we already have an existing profile that matches the user/node.
220+
if existing := pm.findMatchingProfiles(prefs); len(existing) > 0 {
221+
// We already have a profile for this user/node we should reuse it. Also
222+
// cleanup any other duplicate profiles.
223+
cp = existing[0]
224+
existing = existing[1:]
225+
for _, p := range existing {
226+
// Clear the state.
227+
if err := pm.store.WriteState(p.Key, nil); err != nil {
228+
// We couldn't delete the state, so keep the profile around.
229+
continue
232230
}
233-
cp = existing[0]
231+
// Remove the profile, knownProfiles will be persisted below.
232+
delete(pm.knownProfiles, p.ID)
234233
}
234+
} else if cp.ID == "" {
235+
// We didn't have an existing profile, so create a new one.
236+
cp.ID, cp.Key = newUnusedID(pm.knownProfiles)
235237
cp.LocalUserID = pm.currentUserID
238+
} else {
239+
// This means that there was a force-reauth as a new node that
240+
// we haven't seen before.
236241
}
237-
if prefs.ProfileName() != "" {
238-
cp.Name = prefs.ProfileName()
242+
243+
if prefs.ProfileName != "" {
244+
cp.Name = prefs.ProfileName
239245
} else {
240246
cp.Name = up.LoginName
241247
}
242-
cp.ControlURL = prefs.ControlURL()
248+
cp.ControlURL = prefs.ControlURL
243249
cp.UserProfile = newPersist.UserProfile
244250
cp.NodeID = newPersist.NodeID
245251
pm.knownProfiles[cp.ID] = cp
@@ -250,7 +256,7 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
250256
if err := pm.setAsUserSelectedProfileLocked(); err != nil {
251257
return err
252258
}
253-
if err := pm.setPrefsLocked(prefs); err != nil {
259+
if err := pm.setPrefsLocked(prefs.View()); err != nil {
254260
return err
255261
}
256262
return nil
@@ -273,7 +279,7 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
273279
// is not new.
274280
func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error {
275281
pm.prefs = clonedPrefs
276-
if pm.isNewProfile {
282+
if pm.currentProfile.ID == "" {
277283
return nil
278284
}
279285
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
295301

296302
// Profiles returns the list of known profiles.
297303
func (pm *profileManager) Profiles() []ipn.LoginProfile {
298-
profiles := pm.matchingProfiles(func(*ipn.LoginProfile) bool { return true })
299-
slices.SortFunc(profiles, func(a, b *ipn.LoginProfile) int {
300-
return strings.Compare(a.Name, b.Name)
301-
})
302-
out := make([]ipn.LoginProfile, 0, len(profiles))
303-
for _, p := range profiles {
304+
allProfiles := pm.allProfiles()
305+
out := make([]ipn.LoginProfile, 0, len(allProfiles))
306+
for _, p := range allProfiles {
304307
out = append(out, *p)
305308
}
306309
return out
@@ -328,7 +331,6 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
328331
}
329332
pm.prefs = prefs
330333
pm.currentProfile = kp
331-
pm.isNewProfile = false
332334
return pm.setAsUserSelectedProfileLocked()
333335
}
334336

@@ -380,7 +382,7 @@ var errProfileNotFound = errors.New("profile not found")
380382
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
381383
metricDeleteProfile.Add(1)
382384

383-
if id == "" && pm.isNewProfile {
385+
if id == "" {
384386
// Deleting the in-memory only new profile, just create a new one.
385387
pm.NewProfile()
386388
return nil
@@ -431,7 +433,6 @@ func (pm *profileManager) NewProfile() {
431433
metricNewProfile.Add(1)
432434

433435
pm.prefs = defaultPrefs
434-
pm.isNewProfile = true
435436
pm.currentProfile = &ipn.LoginProfile{}
436437
}
437438

ipn/ipnlocal/profiles_notwindows.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ import (
1616
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
1717
k := ipn.LegacyGlobalDaemonStateKey
1818
switch {
19-
case runtime.GOOS == "ios":
20-
k = "ipn-go-bridge"
21-
case version.IsSandboxedMacOS():
19+
case runtime.GOOS == "ios", version.IsSandboxedMacOS():
2220
k = "ipn-go-bridge"
2321
case runtime.GOOS == "android":
2422
k = "ipn-android"

ipn/ipnlocal/profiles_test.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,8 @@ func TestProfileDupe(t *testing.T) {
203203
{reauth, user1Node1},
204204
},
205205
profs: []*persist.Persist{
206-
// BUG: This is incorrect, and should be:
207-
// user1Node1,
208-
// user2Node2,
209-
user1Node1,
210206
user1Node1,
207+
user2Node2,
211208
},
212209
},
213210
{
@@ -218,11 +215,8 @@ func TestProfileDupe(t *testing.T) {
218215
{reauth, user2Node1},
219216
},
220217
profs: []*persist.Persist{
221-
// BUG: This is incorrect, and should be:
222-
// user2Node1,
223-
// user3Node3,
224-
user1Node1,
225218
user2Node1,
219+
user3Node3,
226220
},
227221
},
228222
{
@@ -233,11 +227,8 @@ func TestProfileDupe(t *testing.T) {
233227
{reauth, user1Node2},
234228
},
235229
profs: []*persist.Persist{
236-
// BUG: This is incorrect, and should be:
237-
// user1Node2,
238-
// user3Node3,
239-
user1Node1,
240230
user1Node2,
231+
user3Node3,
241232
},
242233
},
243234
{

0 commit comments

Comments
 (0)