tailscale/ipn/ipnlocal/profiles_test.go

702 lines
18 KiB
Go

// 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=%q, want %v", pf.NoStatefulFiltering(), tt.want)
}
})
}
}
// TestDefaultPrefs tests that defaultPrefs is just NewPrefs with
// LoggedOut=true (the Prefs we use before connecting to control). We shouldn't
// be putting any defaulting there, and instead put all defaults in NewPrefs.
func TestDefaultPrefs(t *testing.T) {
p1 := ipn.NewPrefs()
p1.LoggedOut = true
p1.WantRunning = false
p2 := defaultPrefs
if !p1.View().Equals(p2) {
t.Errorf("defaultPrefs is %s, want %s; defaultPrefs should only modify WantRunning and LoggedOut, all other defaults should be in ipn.NewPrefs.", p2.Pretty(), p1.Pretty())
}
}