// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package ipnlocal import ( "encoding/json" "fmt" "os/user" "strconv" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "tailscale.com/clientupdate" "tailscale.com/envknob" "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/util/must" ) func TestProfileCurrentUserSwitch(t *testing.T) { store := new(mem.Store) pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } id := 0 newProfile := func(t *testing.T, loginName string) ipn.PrefsView { id++ t.Helper() pm.NewProfile() p := pm.CurrentPrefs().AsStruct() p.Persist = &persist.Persist{ NodeID: tailcfg.StableNodeID(fmt.Sprint(id)), PrivateNodeKey: key.NewNode(), UserProfile: tailcfg.UserProfile{ ID: tailcfg.UserID(id), LoginName: loginName, }, } if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil { t.Fatal(err) } return p.View() } pm.SetCurrentUserID("user1") newProfile(t, "user1") cp := pm.currentProfile pm.DeleteProfile(cp.ID) if pm.currentProfile == nil { t.Fatal("currentProfile is nil") } else if pm.currentProfile.ID != "" { t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID) } if !pm.CurrentPrefs().Equals(defaultPrefs) { t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty()) } pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } pm.SetCurrentUserID("user1") if pm.currentProfile == nil { t.Fatal("currentProfile is nil") } else if pm.currentProfile.ID != "" { t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID) } if !pm.CurrentPrefs().Equals(defaultPrefs) { t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty()) } } func TestProfileList(t *testing.T) { store := new(mem.Store) pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } id := 0 newProfile := func(t *testing.T, loginName string) ipn.PrefsView { id++ t.Helper() pm.NewProfile() p := pm.CurrentPrefs().AsStruct() p.Persist = &persist.Persist{ NodeID: tailcfg.StableNodeID(fmt.Sprint(id)), PrivateNodeKey: key.NewNode(), UserProfile: tailcfg.UserProfile{ ID: tailcfg.UserID(id), LoginName: loginName, }, } if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil { t.Fatal(err) } return p.View() } checkProfiles := func(t *testing.T, want ...string) { t.Helper() got := pm.Profiles() if len(got) != len(want) { t.Fatalf("got %d profiles, want %d", len(got), len(want)) } for i, w := range want { if got[i].Name != w { t.Errorf("got profile %d name %q, want %q", i, got[i].Name, w) } } } pm.SetCurrentUserID("user1") newProfile(t, "alice") newProfile(t, "bob") checkProfiles(t, "alice", "bob") pm.SetCurrentUserID("user2") checkProfiles(t) newProfile(t, "carol") carol := pm.currentProfile checkProfiles(t, "carol") pm.SetCurrentUserID("user1") checkProfiles(t, "alice", "bob") if lp := pm.findProfileByKey(carol.Key); lp != nil { t.Fatalf("found profile for user2 in user1's profile list") } if lp := pm.findProfileByName(carol.Name); lp != nil { t.Fatalf("found profile for user2 in user1's profile list") } pm.SetCurrentUserID("user2") checkProfiles(t, "carol") } func TestProfileDupe(t *testing.T) { newPersist := func(user, node int) *persist.Persist { return &persist.Persist{ NodeID: tailcfg.StableNodeID(fmt.Sprintf("node%d", node)), UserProfile: tailcfg.UserProfile{ ID: tailcfg.UserID(user), LoginName: fmt.Sprintf("user%d@example.com", user), }, } } user1Node1 := newPersist(1, 1) user1Node2 := newPersist(1, 2) user2Node1 := newPersist(2, 1) user2Node2 := newPersist(2, 2) user3Node3 := newPersist(3, 3) reauth := func(pm *profileManager, p *persist.Persist) { prefs := ipn.NewPrefs() prefs.Persist = p must.Do(pm.SetPrefs(prefs.View(), ipn.NetworkProfile{})) } login := func(pm *profileManager, p *persist.Persist) { pm.NewProfile() reauth(pm, p) } type step struct { fn func(pm *profileManager, p *persist.Persist) p *persist.Persist } tests := []struct { name string steps []step profs []*persist.Persist }{ { name: "reauth-new-node", steps: []step{ {login, user1Node1}, {reauth, user3Node3}, }, profs: []*persist.Persist{ user3Node3, }, }, { name: "reauth-same-node", steps: []step{ {login, user1Node1}, {reauth, user1Node1}, }, profs: []*persist.Persist{ user1Node1, }, }, { name: "reauth-other-profile", steps: []step{ {login, user1Node1}, {login, user2Node2}, {reauth, user1Node1}, }, profs: []*persist.Persist{ user1Node1, user2Node2, }, }, { name: "reauth-replace-user", steps: []step{ {login, user1Node1}, {login, user3Node3}, {reauth, user2Node1}, }, profs: []*persist.Persist{ user2Node1, user3Node3, }, }, { name: "reauth-replace-node", steps: []step{ {login, user1Node1}, {login, user3Node3}, {reauth, user1Node2}, }, profs: []*persist.Persist{ user1Node2, user3Node3, }, }, { name: "login-same-node", steps: []step{ {login, user1Node1}, {login, user3Node3}, // random other profile {login, user1Node1}, }, profs: []*persist.Persist{ user1Node1, user3Node3, }, }, { name: "login-replace-user", steps: []step{ {login, user1Node1}, {login, user3Node3}, // random other profile {login, user2Node1}, }, profs: []*persist.Persist{ user2Node1, user3Node3, }, }, { name: "login-replace-node", steps: []step{ {login, user1Node1}, {login, user3Node3}, // random other profile {login, user1Node2}, }, profs: []*persist.Persist{ user1Node2, user3Node3, }, }, { name: "login-new-node", steps: []step{ {login, user1Node1}, {login, user2Node2}, }, profs: []*persist.Persist{ user1Node1, user2Node2, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { store := new(mem.Store) pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } for _, s := range tc.steps { s.fn(pm, s.p) } profs := pm.Profiles() var got []*persist.Persist for _, p := range profs { prefs, err := pm.loadSavedPrefs(p.Key) if err != nil { t.Fatal(err) } got = append(got, prefs.Persist().AsStruct()) } d := cmp.Diff(tc.profs, got, cmpopts.SortSlices(func(a, b *persist.Persist) bool { if a.NodeID != b.NodeID { return a.NodeID < b.NodeID } return a.UserProfile.ID < b.UserProfile.ID })) if d != "" { t.Fatal(d) } }) } } // TestProfileManagement tests creating, loading, and switching profiles. func TestProfileManagement(t *testing.T) { store := new(mem.Store) pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } wantCurProfile := "" wantProfiles := map[string]ipn.PrefsView{ "": defaultPrefs, } checkProfiles := func(t *testing.T) { t.Helper() prof := pm.CurrentProfile() t.Logf("\tCurrentProfile = %q", prof) if prof.Name != wantCurProfile { t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) } profiles := pm.Profiles() wantLen := len(wantProfiles) if _, ok := wantProfiles[""]; ok { wantLen-- } if len(profiles) != wantLen { t.Fatalf("Profiles = %v; want %v", profiles, wantProfiles) } p := pm.CurrentPrefs() t.Logf("\tCurrentPrefs = %s", p.Pretty()) if !p.Valid() { t.Fatalf("CurrentPrefs = %v; want valid", p) } if !p.Equals(wantProfiles[wantCurProfile]) { t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) } for _, p := range profiles { got, err := pm.loadSavedPrefs(p.Key) if err != nil { t.Fatal(err) } // Use Hostname as a proxy for all prefs. if !got.Equals(wantProfiles[p.Name]) { t.Fatalf("Prefs for profile %q =\n got=%+v\nwant=%v", p, got.Pretty(), wantProfiles[p.Name].Pretty()) } } } logins := make(map[string]tailcfg.UserID) nodeIDs := make(map[string]tailcfg.StableNodeID) setPrefs := func(t *testing.T, loginName string) ipn.PrefsView { t.Helper() p := pm.CurrentPrefs().AsStruct() uid := logins[loginName] if uid.IsZero() { uid = tailcfg.UserID(len(logins) + 1) logins[loginName] = uid } nid := nodeIDs[loginName] if nid.IsZero() { nid = tailcfg.StableNodeID(fmt.Sprint(len(nodeIDs) + 1)) nodeIDs[loginName] = nid } p.Persist = &persist.Persist{ PrivateNodeKey: key.NewNode(), UserProfile: tailcfg.UserProfile{ ID: uid, LoginName: loginName, }, NodeID: nid, } if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil { t.Fatal(err) } return p.View() } t.Logf("Check initial state from empty store") checkProfiles(t) { t.Logf("Set prefs for default profile") wantProfiles["user@1.example.com"] = setPrefs(t, "user@1.example.com") wantCurProfile = "user@1.example.com" delete(wantProfiles, "") } checkProfiles(t) t.Logf("Create new profile") pm.NewProfile() wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) { t.Logf("Set prefs for test profile") wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com") wantCurProfile = "user@2.example.com" delete(wantProfiles, "") } checkProfiles(t) t.Logf("Recreate profile manager from store") // Recreate the profile manager to ensure that it can load the profiles // from the store at startup. pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } checkProfiles(t) t.Logf("Delete default profile") if err := pm.DeleteProfile(pm.findProfileByName("user@1.example.com").ID); err != nil { t.Fatal(err) } delete(wantProfiles, "user@1.example.com") checkProfiles(t) t.Logf("Recreate profile manager from store after deleting default profile") // Recreate the profile manager to ensure that it can load the profiles // from the store at startup. pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } checkProfiles(t) t.Logf("Create new profile - 2") pm.NewProfile() wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) t.Logf("Login with the existing profile") wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com") delete(wantProfiles, "") wantCurProfile = "user@2.example.com" checkProfiles(t) t.Logf("Tag the current the profile") nodeIDs["tagged-node.2.ts.net"] = nodeIDs["user@2.example.com"] wantProfiles["tagged-node.2.ts.net"] = setPrefs(t, "tagged-node.2.ts.net") delete(wantProfiles, "user@2.example.com") wantCurProfile = "tagged-node.2.ts.net" checkProfiles(t) t.Logf("Relogin") wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com") delete(wantProfiles, "tagged-node.2.ts.net") wantCurProfile = "user@2.example.com" checkProfiles(t) if !clientupdate.CanAutoUpdate() { t.Logf("Save an invalid AutoUpdate pref value") prefs := pm.CurrentPrefs().AsStruct() prefs.AutoUpdate.Apply.Set(true) if err := pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}); err != nil { t.Fatal(err) } if !pm.CurrentPrefs().AutoUpdate().Apply.EqualBool(true) { t.Fatal("SetPrefs failed to save auto-update setting") } // Re-load profiles to trigger migration for invalid auto-update value. pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux") if err != nil { t.Fatal(err) } checkProfiles(t) if pm.CurrentPrefs().AutoUpdate().Apply.EqualBool(true) { t.Fatal("invalid auto-update setting persisted after reload") } } } // TestProfileManagementWindows tests going into and out of Unattended mode on // Windows. func TestProfileManagementWindows(t *testing.T) { u, err := user.Current() if err != nil { t.Fatal(err) } uid := ipn.WindowsUserID(u.Uid) store := new(mem.Store) pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows") if err != nil { t.Fatal(err) } wantCurProfile := "" wantProfiles := map[string]ipn.PrefsView{ "": defaultPrefs, } checkProfiles := func(t *testing.T) { t.Helper() prof := pm.CurrentProfile() t.Logf("\tCurrentProfile = %q", prof) if prof.Name != wantCurProfile { t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) } if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) { t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) } } logins := make(map[string]tailcfg.UserID) setPrefs := func(t *testing.T, loginName string, forceDaemon bool) ipn.PrefsView { id := logins[loginName] if id.IsZero() { id = tailcfg.UserID(len(logins) + 1) logins[loginName] = id } p := pm.CurrentPrefs().AsStruct() p.ForceDaemon = forceDaemon p.Persist = &persist.Persist{ UserProfile: tailcfg.UserProfile{ ID: id, LoginName: loginName, }, NodeID: tailcfg.StableNodeID(strconv.Itoa(int(id))), } if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil { t.Fatal(err) } return p.View() } t.Logf("Check initial state from empty store") checkProfiles(t) { t.Logf("Set user1 as logged in user") if err := pm.SetCurrentUserID(uid); err != nil { t.Fatalf("can't set user id: %s", err) } checkProfiles(t) t.Logf("Save prefs for user1") wantProfiles["default"] = setPrefs(t, "default", false) wantCurProfile = "default" } checkProfiles(t) { t.Logf("Create new profile") pm.NewProfile() wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) t.Logf("Save as test profile") wantProfiles["test"] = setPrefs(t, "test", false) wantCurProfile = "test" checkProfiles(t) } t.Logf("Recreate profile manager from store, should reset prefs") // Recreate the profile manager to ensure that it can load the profiles // from the store at startup. pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows") if err != nil { t.Fatal(err) } wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) { t.Logf("Set user1 as current user") if err := pm.SetCurrentUserID(uid); err != nil { t.Fatal(err) } wantCurProfile = "test" } checkProfiles(t) { t.Logf("set unattended mode") wantProfiles["test"] = setPrefs(t, "test", true) } if pm.CurrentUserID() != uid { t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid) } // Recreate the profile manager to ensure that it starts with test profile. pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows") if err != nil { t.Fatal(err) } checkProfiles(t) if pm.CurrentUserID() != uid { t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid) } } func TestProfileBackfillStatefulFiltering(t *testing.T) { envknob.Setenv("TS_DEBUG_PROFILES", "true") tests := []struct { noSNAT bool noStateful opt.Bool want bool }{ // Default: NoSNAT is false, NoStatefulFiltering is false, so // we want it to stay false. {false, "false", false}, // NoSNAT being set to true and NoStatefulFiltering being false // should result in NoStatefulFiltering still being false, // since it was explicitly set. {true, "false", false}, // If NoSNAT is false, and NoStatefulFiltering is unset, we // backfill it to 'false'. {false, "", false}, // If NoSNAT is true, and NoStatefulFiltering is unset, we // backfill to 'true' to not break users of NoSNAT. // // In other words: if the user is not using SNAT, they almost // certainly also don't want to use stateful filtering. {true, "", true}, // However, if the user specifies both NoSNAT and stateful // filtering, don't change that. {true, "true", true}, {false, "true", true}, } for _, tt := range tests { t.Run(fmt.Sprintf("noSNAT=%v,noStateful=%q", tt.noSNAT, tt.noStateful), func(t *testing.T) { prefs := ipn.NewPrefs() prefs.Persist = &persist.Persist{ NodeID: tailcfg.StableNodeID("node1"), UserProfile: tailcfg.UserProfile{ ID: tailcfg.UserID(1), LoginName: "user1@example.com", }, } prefs.NoSNAT = tt.noSNAT prefs.NoStatefulFiltering = tt.noStateful // Make enough of a state store to load the prefs. const profileName = "profile1" bn := must.Get(json.Marshal(map[string]any{ string(ipn.CurrentProfileStateKey): []byte(profileName), string(ipn.KnownProfilesStateKey): must.Get(json.Marshal(map[ipn.ProfileID]*ipn.LoginProfile{ profileName: { ID: "profile1-id", Key: profileName, }, })), profileName: prefs.ToBytes(), })) store := new(mem.Store) err := store.LoadFromJSON([]byte(bn)) if err != nil { t.Fatal(err) } ht := new(health.Tracker) pm, err := newProfileManagerWithGOOS(store, t.Logf, ht, "linux") if err != nil { t.Fatal(err) } // Get the current profile and verify that we backfilled our // StatefulFiltering boolean. pf := pm.CurrentPrefs() if !pf.NoStatefulFiltering().EqualBool(tt.want) { t.Fatalf("got NoStatefulFiltering=%v, want %v", pf.NoStatefulFiltering(), tt.want) } }) } }