Răsfoiți Sursa

ipn: generate LoginProfileView and use it instead of *LoginProfile where appropriate

Conventionally, we use views (e.g., ipn.PrefsView, tailcfg.NodeView, etc.) when
dealing with structs that shouldn't be mutated. However, ipn.LoginProfile has been
an exception so far, with a mix of passing and returning LoginProfile by reference
(allowing accidental mutations) and by value (which is wasteful, given its
current size of 192 bytes).

In this PR, we generate an ipn.LoginProfileView and use it instead of passing/returning
LoginProfiles by mutable reference or copying them when passing/returning by value.
Now, LoginProfiles can only be mutated by (*profileManager).setProfilePrefs.

Updates #14823

Signed-off-by: Nick Khyl <[email protected]>
Nick Khyl 1 an în urmă
părinte
comite
4e7f4086b2

+ 1 - 1
ipn/doc.go

@@ -1,7 +1,7 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 // SPDX-License-Identifier: BSD-3-Clause
 
 
-//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
+//go:generate go run tailscale.com/cmd/viewer -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
 
 
 // Package ipn implements the interactions between the Tailscale cloud
 // Package ipn implements the interactions between the Tailscale cloud
 // control plane and the local network stack.
 // control plane and the local network stack.

+ 23 - 0
ipn/ipn_clone.go

@@ -17,6 +17,29 @@ import (
 	"tailscale.com/types/ptr"
 	"tailscale.com/types/ptr"
 )
 )
 
 
+// Clone makes a deep copy of LoginProfile.
+// The result aliases no memory with the original.
+func (src *LoginProfile) Clone() *LoginProfile {
+	if src == nil {
+		return nil
+	}
+	dst := new(LoginProfile)
+	*dst = *src
+	return dst
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _LoginProfileCloneNeedsRegeneration = LoginProfile(struct {
+	ID             ProfileID
+	Name           string
+	NetworkProfile NetworkProfile
+	Key            StateKey
+	UserProfile    tailcfg.UserProfile
+	NodeID         tailcfg.StableNodeID
+	LocalUserID    WindowsUserID
+	ControlURL     string
+}{})
+
 // Clone makes a deep copy of Prefs.
 // Clone makes a deep copy of Prefs.
 // The result aliases no memory with the original.
 // The result aliases no memory with the original.
 func (src *Prefs) Clone() *Prefs {
 func (src *Prefs) Clone() *Prefs {

+ 67 - 1
ipn/ipn_view.go

@@ -18,7 +18,73 @@ import (
 	"tailscale.com/types/views"
 	"tailscale.com/types/views"
 )
 )
 
 
-//go:generate go run tailscale.com/cmd/cloner  -clonefunc=false -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
+//go:generate go run tailscale.com/cmd/cloner  -clonefunc=false -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
+
+// View returns a read-only view of LoginProfile.
+func (p *LoginProfile) View() LoginProfileView {
+	return LoginProfileView{ж: p}
+}
+
+// LoginProfileView provides a read-only view over LoginProfile.
+//
+// Its methods should only be called if `Valid()` returns true.
+type LoginProfileView struct {
+	// ж is the underlying mutable value, named with a hard-to-type
+	// character that looks pointy like a pointer.
+	// It is named distinctively to make you think of how dangerous it is to escape
+	// to callers. You must not let callers be able to mutate it.
+	ж *LoginProfile
+}
+
+// Valid reports whether v's underlying value is non-nil.
+func (v LoginProfileView) Valid() bool { return v.ж != nil }
+
+// AsStruct returns a clone of the underlying value which aliases no memory with
+// the original.
+func (v LoginProfileView) AsStruct() *LoginProfile {
+	if v.ж == nil {
+		return nil
+	}
+	return v.ж.Clone()
+}
+
+func (v LoginProfileView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
+
+func (v *LoginProfileView) UnmarshalJSON(b []byte) error {
+	if v.ж != nil {
+		return errors.New("already initialized")
+	}
+	if len(b) == 0 {
+		return nil
+	}
+	var x LoginProfile
+	if err := json.Unmarshal(b, &x); err != nil {
+		return err
+	}
+	v.ж = &x
+	return nil
+}
+
+func (v LoginProfileView) ID() ProfileID                    { return v.ж.ID }
+func (v LoginProfileView) Name() string                     { return v.ж.Name }
+func (v LoginProfileView) NetworkProfile() NetworkProfile   { return v.ж.NetworkProfile }
+func (v LoginProfileView) Key() StateKey                    { return v.ж.Key }
+func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
+func (v LoginProfileView) NodeID() tailcfg.StableNodeID     { return v.ж.NodeID }
+func (v LoginProfileView) LocalUserID() WindowsUserID       { return v.ж.LocalUserID }
+func (v LoginProfileView) ControlURL() string               { return v.ж.ControlURL }
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _LoginProfileViewNeedsRegeneration = LoginProfile(struct {
+	ID             ProfileID
+	Name           string
+	NetworkProfile NetworkProfile
+	Key            StateKey
+	UserProfile    tailcfg.UserProfile
+	NodeID         tailcfg.StableNodeID
+	LocalUserID    WindowsUserID
+	ControlURL     string
+}{})
 
 
 // View returns a read-only view of Prefs.
 // View returns a read-only view of Prefs.
 func (p *Prefs) View() PrefsView {
 func (p *Prefs) View() PrefsView {

+ 15 - 15
ipn/ipnlocal/local.go

@@ -4045,7 +4045,7 @@ func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error {
 		// No profile with that name exists. That's fine.
 		// No profile with that name exists. That's fine.
 		return nil
 		return nil
 	}
 	}
-	if id != b.pm.CurrentProfile().ID {
+	if id != b.pm.CurrentProfile().ID() {
 		// Name is already in use by another profile.
 		// Name is already in use by another profile.
 		return fmt.Errorf("profile name %q already in use", p.ProfileName)
 		return fmt.Errorf("profile name %q already in use", p.ProfileName)
 	}
 	}
@@ -4127,7 +4127,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
 	}
 	}
 
 
 	prefs := newp.View()
 	prefs := newp.View()
-	np := b.pm.CurrentProfile().NetworkProfile
+	np := b.pm.CurrentProfile().NetworkProfile()
 	if netMap != nil {
 	if netMap != nil {
 		np = ipn.NetworkProfile{
 		np = ipn.NetworkProfile{
 			MagicDNSName: b.netMap.MagicDNSSuffix(),
 			MagicDNSName: b.netMap.MagicDNSSuffix(),
@@ -5663,7 +5663,7 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
 	unlock = b.lockAndGetUnlock()
 	unlock = b.lockAndGetUnlock()
 	defer unlock()
 	defer unlock()
 
 
-	if err := b.pm.DeleteProfile(profile.ID); err != nil {
+	if err := b.pm.DeleteProfile(profile.ID()); err != nil {
 		b.logf("error deleting profile: %v", err)
 		b.logf("error deleting profile: %v", err)
 		return err
 		return err
 	}
 	}
@@ -6039,7 +6039,7 @@ func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
 // the method to only run the reset-logic and not reload the store from memory to ensure
 // the method to only run the reset-logic and not reload the store from memory to ensure
 // foreground sessions are not removed if they are not saved on disk.
 // foreground sessions are not removed if they are not saved on disk.
 func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
 func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
-	if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID == "" {
+	if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID() == "" {
 		// We're not logged in, so we don't have a profile.
 		// We're not logged in, so we don't have a profile.
 		// Don't try to load the serve config.
 		// Don't try to load the serve config.
 		b.lastServeConfJSON = mem.B(nil)
 		b.lastServeConfJSON = mem.B(nil)
@@ -6047,7 +6047,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
 		return
 		return
 	}
 	}
 
 
-	confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID)
+	confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID())
 	// TODO(maisem,bradfitz): prevent reading the config from disk
 	// TODO(maisem,bradfitz): prevent reading the config from disk
 	// if the profile has not changed.
 	// if the profile has not changed.
 	confj, err := b.store.ReadState(confKey)
 	confj, err := b.store.ReadState(confKey)
@@ -7000,7 +7000,7 @@ func (b *LocalBackend) ShouldInterceptVIPServiceTCPPort(ap netip.AddrPort) bool
 // It will restart the backend on success.
 // It will restart the backend on success.
 // If the profile is not known, it returns an errProfileNotFound.
 // If the profile is not known, it returns an errProfileNotFound.
 func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
 func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
-	if b.CurrentProfile().ID == profile {
+	if b.CurrentProfile().ID() == profile {
 		return nil
 		return nil
 	}
 	}
 	unlock := b.lockAndGetUnlock()
 	unlock := b.lockAndGetUnlock()
@@ -7023,12 +7023,12 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
 
 
 func (b *LocalBackend) initTKALocked() error {
 func (b *LocalBackend) initTKALocked() error {
 	cp := b.pm.CurrentProfile()
 	cp := b.pm.CurrentProfile()
-	if cp.ID == "" {
+	if cp.ID() == "" {
 		b.tka = nil
 		b.tka = nil
 		return nil
 		return nil
 	}
 	}
 	if b.tka != nil {
 	if b.tka != nil {
-		if b.tka.profile == cp.ID {
+		if b.tka.profile == cp.ID() {
 			// Already initialized.
 			// Already initialized.
 			return nil
 			return nil
 		}
 		}
@@ -7058,7 +7058,7 @@ func (b *LocalBackend) initTKALocked() error {
 		}
 		}
 
 
 		b.tka = &tkaState{
 		b.tka = &tkaState{
-			profile:   cp.ID,
+			profile:   cp.ID(),
 			authority: authority,
 			authority: authority,
 			storage:   storage,
 			storage:   storage,
 		}
 		}
@@ -7111,7 +7111,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
 	unlock := b.lockAndGetUnlock()
 	unlock := b.lockAndGetUnlock()
 	defer unlock()
 	defer unlock()
 
 
-	needToRestart := b.pm.CurrentProfile().ID == p
+	needToRestart := b.pm.CurrentProfile().ID() == p
 	if err := b.pm.DeleteProfile(p); err != nil {
 	if err := b.pm.DeleteProfile(p); err != nil {
 		if err == errProfileNotFound {
 		if err == errProfileNotFound {
 			return nil
 			return nil
@@ -7126,7 +7126,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
 
 
 // CurrentProfile returns the current LoginProfile.
 // CurrentProfile returns the current LoginProfile.
 // The value may be zero if the profile is not persisted.
 // The value may be zero if the profile is not persisted.
-func (b *LocalBackend) CurrentProfile() ipn.LoginProfile {
+func (b *LocalBackend) CurrentProfile() ipn.LoginProfileView {
 	b.mu.Lock()
 	b.mu.Lock()
 	defer b.mu.Unlock()
 	defer b.mu.Unlock()
 	return b.pm.CurrentProfile()
 	return b.pm.CurrentProfile()
@@ -7147,7 +7147,7 @@ func (b *LocalBackend) NewProfile() error {
 }
 }
 
 
 // ListProfiles returns a list of all LoginProfiles.
 // ListProfiles returns a list of all LoginProfiles.
-func (b *LocalBackend) ListProfiles() []ipn.LoginProfile {
+func (b *LocalBackend) ListProfiles() []ipn.LoginProfileView {
 	b.mu.Lock()
 	b.mu.Lock()
 	defer b.mu.Unlock()
 	defer b.mu.Unlock()
 	return b.pm.Profiles()
 	return b.pm.Profiles()
@@ -7353,7 +7353,7 @@ func (b *LocalBackend) UnadvertiseRoute(toRemove ...netip.Prefix) error {
 
 
 // namespace a key with the profile manager's current profile key, if any
 // namespace a key with the profile manager's current profile key, if any
 func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.StateKey {
 func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.StateKey {
-	return pm.CurrentProfile().Key + "||" + key
+	return pm.CurrentProfile().Key() + "||" + key
 }
 }
 
 
 const routeInfoStateStoreKey ipn.StateKey = "_routeInfo"
 const routeInfoStateStoreKey ipn.StateKey = "_routeInfo"
@@ -7361,7 +7361,7 @@ const routeInfoStateStoreKey ipn.StateKey = "_routeInfo"
 func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error {
 func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error {
 	b.mu.Lock()
 	b.mu.Lock()
 	defer b.mu.Unlock()
 	defer b.mu.Unlock()
-	if b.pm.CurrentProfile().ID == "" {
+	if b.pm.CurrentProfile().ID() == "" {
 		return nil
 		return nil
 	}
 	}
 	key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey)
 	key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey)
@@ -7373,7 +7373,7 @@ func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error {
 }
 }
 
 
 func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) {
 func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) {
-	if b.pm.CurrentProfile().ID == "" {
+	if b.pm.CurrentProfile().ID() == "" {
 		return &appc.RouteInfo{}, nil
 		return &appc.RouteInfo{}, nil
 	}
 	}
 	key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey)
 	key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey)

+ 3 - 3
ipn/ipnlocal/local_test.go

@@ -4087,9 +4087,9 @@ func TestReadWriteRouteInfo(t *testing.T) {
 	b := newTestBackend(t)
 	b := newTestBackend(t)
 	prof1 := ipn.LoginProfile{ID: "id1", Key: "key1"}
 	prof1 := ipn.LoginProfile{ID: "id1", Key: "key1"}
 	prof2 := ipn.LoginProfile{ID: "id2", Key: "key2"}
 	prof2 := ipn.LoginProfile{ID: "id2", Key: "key2"}
-	b.pm.knownProfiles["id1"] = &prof1
-	b.pm.knownProfiles["id2"] = &prof2
-	b.pm.currentProfile = &prof1
+	b.pm.knownProfiles["id1"] = prof1.View()
+	b.pm.knownProfiles["id2"] = prof2.View()
+	b.pm.currentProfile = prof1.View()
 
 
 	// set up routeInfo
 	// set up routeInfo
 	ri1 := &appc.RouteInfo{}
 	ri1 := &appc.RouteInfo{}

+ 2 - 2
ipn/ipnlocal/network-lock.go

@@ -407,7 +407,7 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
 //
 //
 // b.mu must be held.
 // b.mu must be held.
 func (b *LocalBackend) chonkPathLocked() string {
 func (b *LocalBackend) chonkPathLocked() string {
-	return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID))
+	return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID()))
 }
 }
 
 
 // tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
 // tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
@@ -455,7 +455,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
 	}
 	}
 
 
 	b.tka = &tkaState{
 	b.tka = &tkaState{
-		profile:   b.pm.CurrentProfile().ID,
+		profile:   b.pm.CurrentProfile().ID(),
 		authority: authority,
 		authority: authority,
 		storage:   chonk,
 		storage:   chonk,
 	}
 	}

+ 8 - 8
ipn/ipnlocal/network-lock_test.go

@@ -202,7 +202,7 @@ func TestTKADisablementFlow(t *testing.T) {
 	}).View(), ipn.NetworkProfile{}))
 	}).View(), ipn.NetworkProfile{}))
 
 
 	temp := t.TempDir()
 	temp := t.TempDir()
-	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 	os.Mkdir(tkaPath, 0755)
 	os.Mkdir(tkaPath, 0755)
 	chonk, err := tka.ChonkDir(tkaPath)
 	chonk, err := tka.ChonkDir(tkaPath)
 	if err != nil {
 	if err != nil {
@@ -410,7 +410,7 @@ func TestTKASync(t *testing.T) {
 			}
 			}
 
 
 			temp := t.TempDir()
 			temp := t.TempDir()
-			tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+			tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 			os.Mkdir(tkaPath, 0755)
 			os.Mkdir(tkaPath, 0755)
 			// Setup the TKA authority on the node.
 			// Setup the TKA authority on the node.
 			nodeStorage, err := tka.ChonkDir(tkaPath)
 			nodeStorage, err := tka.ChonkDir(tkaPath)
@@ -710,7 +710,7 @@ func TestTKADisable(t *testing.T) {
 	}).View(), ipn.NetworkProfile{}))
 	}).View(), ipn.NetworkProfile{}))
 
 
 	temp := t.TempDir()
 	temp := t.TempDir()
-	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 	os.Mkdir(tkaPath, 0755)
 	os.Mkdir(tkaPath, 0755)
 	key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
 	key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
 	chonk, err := tka.ChonkDir(tkaPath)
 	chonk, err := tka.ChonkDir(tkaPath)
@@ -770,7 +770,7 @@ func TestTKADisable(t *testing.T) {
 		ccAuto:  cc,
 		ccAuto:  cc,
 		logf:    t.Logf,
 		logf:    t.Logf,
 		tka: &tkaState{
 		tka: &tkaState{
-			profile:   pm.CurrentProfile().ID,
+			profile:   pm.CurrentProfile().ID(),
 			authority: authority,
 			authority: authority,
 			storage:   chonk,
 			storage:   chonk,
 		},
 		},
@@ -805,7 +805,7 @@ func TestTKASign(t *testing.T) {
 	key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
 	key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
 
 
 	temp := t.TempDir()
 	temp := t.TempDir()
-	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 	os.Mkdir(tkaPath, 0755)
 	os.Mkdir(tkaPath, 0755)
 	chonk, err := tka.ChonkDir(tkaPath)
 	chonk, err := tka.ChonkDir(tkaPath)
 	if err != nil {
 	if err != nil {
@@ -890,7 +890,7 @@ func TestTKAForceDisable(t *testing.T) {
 	}).View(), ipn.NetworkProfile{}))
 	}).View(), ipn.NetworkProfile{}))
 
 
 	temp := t.TempDir()
 	temp := t.TempDir()
-	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 	os.Mkdir(tkaPath, 0755)
 	os.Mkdir(tkaPath, 0755)
 	chonk, err := tka.ChonkDir(tkaPath)
 	chonk, err := tka.ChonkDir(tkaPath)
 	if err != nil {
 	if err != nil {
@@ -989,7 +989,7 @@ func TestTKAAffectedSigs(t *testing.T) {
 	tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
 	tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
 
 
 	temp := t.TempDir()
 	temp := t.TempDir()
-	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 	os.Mkdir(tkaPath, 0755)
 	os.Mkdir(tkaPath, 0755)
 	chonk, err := tka.ChonkDir(tkaPath)
 	chonk, err := tka.ChonkDir(tkaPath)
 	if err != nil {
 	if err != nil {
@@ -1124,7 +1124,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
 	compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1}
 	compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1}
 
 
 	temp := t.TempDir()
 	temp := t.TempDir()
-	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
+	tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
 	os.Mkdir(tkaPath, 0755)
 	os.Mkdir(tkaPath, 0755)
 	chonk, err := tka.ChonkDir(tkaPath)
 	chonk, err := tka.ChonkDir(tkaPath)
 	if err != nil {
 	if err != nil {

+ 109 - 95
ipn/ipnlocal/profiles.go

@@ -35,9 +35,9 @@ type profileManager struct {
 	health *health.Tracker
 	health *health.Tracker
 
 
 	currentUserID  ipn.WindowsUserID
 	currentUserID  ipn.WindowsUserID
-	knownProfiles  map[ipn.ProfileID]*ipn.LoginProfile // always non-nil
-	currentProfile *ipn.LoginProfile                   // always non-nil
-	prefs          ipn.PrefsView                       // always Valid.
+	knownProfiles  map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
+	currentProfile ipn.LoginProfileView                   // always Valid.
+	prefs          ipn.PrefsView                          // always Valid.
 }
 }
 
 
 func (pm *profileManager) dlogf(format string, args ...any) {
 func (pm *profileManager) dlogf(format string, args ...any) {
@@ -89,7 +89,7 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
 			pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences")
 			pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences")
 			profile, err := pm.migrateFromLegacyPrefs(uid, false)
 			profile, err := pm.migrateFromLegacyPrefs(uid, false)
 			if err == nil {
 			if err == nil {
-				return profile.ID
+				return profile.ID()
 			}
 			}
 			pm.logf("failed to migrate from legacy preferences: %v", err)
 			pm.logf("failed to migrate from legacy preferences: %v", err)
 		}
 		}
@@ -98,17 +98,17 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
 
 
 	pk := ipn.StateKey(string(b))
 	pk := ipn.StateKey(string(b))
 	prof := pm.findProfileByKey(pk)
 	prof := pm.findProfileByKey(pk)
-	if prof == nil {
+	if !prof.Valid() {
 		pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
 		pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
 		return ""
 		return ""
 	}
 	}
-	return prof.ID
+	return prof.ID()
 }
 }
 
 
 // checkProfileAccess returns an [errProfileAccessDenied] if the current user
 // checkProfileAccess returns an [errProfileAccessDenied] if the current user
 // does not have access to the specified profile.
 // does not have access to the specified profile.
-func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error {
-	if pm.currentUserID != "" && profile.LocalUserID != pm.currentUserID {
+func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error {
+	if pm.currentUserID != "" && profile.LocalUserID() != pm.currentUserID {
 		return errProfileAccessDenied
 		return errProfileAccessDenied
 	}
 	}
 	return nil
 	return nil
@@ -116,21 +116,21 @@ func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error {
 
 
 // allProfiles returns all profiles accessible to the current user.
 // allProfiles returns all profiles accessible to the current user.
 // The returned profiles are sorted by Name.
 // The returned profiles are sorted by Name.
-func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
+func (pm *profileManager) allProfiles() (out []ipn.LoginProfileView) {
 	for _, p := range pm.knownProfiles {
 	for _, p := range pm.knownProfiles {
 		if pm.checkProfileAccess(p) == nil {
 		if pm.checkProfileAccess(p) == nil {
 			out = append(out, p)
 			out = append(out, p)
 		}
 		}
 	}
 	}
-	slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
-		return cmp.Compare(a.Name, b.Name)
+	slices.SortFunc(out, func(a, b ipn.LoginProfileView) int {
+		return cmp.Compare(a.Name(), b.Name())
 	})
 	})
 	return out
 	return out
 }
 }
 
 
 // matchingProfiles is like [profileManager.allProfiles], but returns only profiles
 // matchingProfiles is like [profileManager.allProfiles], but returns only profiles
 // matching the given predicate.
 // matching the given predicate.
-func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
+func (pm *profileManager) matchingProfiles(f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
 	all := pm.allProfiles()
 	all := pm.allProfiles()
 	out = all[:0]
 	out = all[:0]
 	for _, p := range all {
 	for _, p := range all {
@@ -144,11 +144,11 @@ func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out
 // findMatchingProfiles returns all profiles accessible to the current user
 // findMatchingProfiles returns all profiles accessible to the current user
 // that represent the same node/user as prefs.
 // that represent the same node/user as prefs.
 // The returned profiles are sorted by Name.
 // The returned profiles are sorted by Name.
-func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.LoginProfile {
-	return pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
-		return p.ControlURL == prefs.ControlURL() &&
-			(p.UserProfile.ID == prefs.Persist().UserProfile().ID ||
-				p.NodeID == prefs.Persist().NodeID())
+func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []ipn.LoginProfileView {
+	return pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
+		return p.ControlURL() == prefs.ControlURL() &&
+			(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
+				p.NodeID() == prefs.Persist().NodeID())
 	})
 	})
 }
 }
 
 
@@ -157,18 +157,18 @@ func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.Login
 // accessible to the current user.
 // accessible to the current user.
 func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
 func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
 	p := pm.findProfileByName(name)
 	p := pm.findProfileByName(name)
-	if p == nil {
+	if !p.Valid() {
 		return ""
 		return ""
 	}
 	}
-	return p.ID
+	return p.ID()
 }
 }
 
 
-func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
-	out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
-		return p.Name == name
+func (pm *profileManager) findProfileByName(name string) ipn.LoginProfileView {
+	out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
+		return p.Name() == name
 	})
 	})
 	if len(out) == 0 {
 	if len(out) == 0 {
-		return nil
+		return ipn.LoginProfileView{}
 	}
 	}
 	if len(out) > 1 {
 	if len(out) > 1 {
 		pm.logf("[unexpected] multiple profiles with the same name")
 		pm.logf("[unexpected] multiple profiles with the same name")
@@ -176,12 +176,12 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
 	return out[0]
 	return out[0]
 }
 }
 
 
-func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
-	out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
-		return p.Key == key
+func (pm *profileManager) findProfileByKey(key ipn.StateKey) ipn.LoginProfileView {
+	out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
+		return p.Key() == key
 	})
 	})
 	if len(out) == 0 {
 	if len(out) == 0 {
-		return nil
+		return ipn.LoginProfileView{}
 	}
 	}
 	if len(out) > 1 {
 	if len(out) > 1 {
 		pm.logf("[unexpected] multiple profiles with the same key")
 		pm.logf("[unexpected] multiple profiles with the same key")
@@ -194,8 +194,8 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
 		return nil
 		return nil
 	}
 	}
 
 
-	if pm.currentProfile.Key != "" && pm.prefs.ForceDaemon() {
-		return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
+	if pm.currentProfile.Key() != "" && pm.prefs.ForceDaemon() {
+		return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key()))
 	} else {
 	} else {
 		return pm.WriteState(ipn.ServerModeStartKey, nil)
 		return pm.WriteState(ipn.ServerModeStartKey, nil)
 	}
 	}
@@ -229,29 +229,36 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
 		existing = existing[1:]
 		existing = existing[1:]
 		for _, p := range existing {
 		for _, p := range existing {
 			// Clear the state.
 			// Clear the state.
-			if err := pm.store.WriteState(p.Key, nil); err != nil {
+			if err := pm.store.WriteState(p.Key(), nil); err != nil {
 				// We couldn't delete the state, so keep the profile around.
 				// We couldn't delete the state, so keep the profile around.
 				continue
 				continue
 			}
 			}
 			// Remove the profile, knownProfiles will be persisted
 			// Remove the profile, knownProfiles will be persisted
 			// in [profileManager.setProfilePrefs] below.
 			// in [profileManager.setProfilePrefs] below.
-			delete(pm.knownProfiles, p.ID)
+			delete(pm.knownProfiles, p.ID())
 		}
 		}
 	}
 	}
 	pm.currentProfile = cp
 	pm.currentProfile = cp
-	if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil {
+	cp, err := pm.setProfilePrefs(nil, prefsIn, np)
+	if err != nil {
 		return err
 		return err
 	}
 	}
 	return pm.setProfileAsUserDefault(cp)
 	return pm.setProfileAsUserDefault(cp)
 
 
 }
 }
 
 
-// SetProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile]
-// which is not necessarily the [profileManager.CurrentProfile]. It returns an [errProfileAccessDenied]
-// if the specified profile is not accessible by the current user.
-func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
-	if err := pm.checkProfileAccess(lp); err != nil {
-		return err
+// setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile],
+// returning a read-only view of the updated profile on success. If the specified profile is nil,
+// it defaults to the current profile. If the profile is not accessible by the current user,
+// the method returns an [errProfileAccessDenied].
+func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) (ipn.LoginProfileView, error) {
+	isCurrentProfile := lp == nil || (lp.ID != "" && lp.ID == pm.currentProfile.ID())
+	if isCurrentProfile {
+		lp = pm.CurrentProfile().AsStruct()
+	}
+
+	if err := pm.checkProfileAccess(lp.View()); err != nil {
+		return ipn.LoginProfileView{}, err
 	}
 	}
 
 
 	// An empty profile.ID indicates that the profile is new, the node info wasn't available,
 	// An empty profile.ID indicates that the profile is new, the node info wasn't available,
@@ -291,23 +298,29 @@ func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
 	lp.UserProfile = up
 	lp.UserProfile = up
 	lp.NetworkProfile = np
 	lp.NetworkProfile = np
 
 
+	// Update the current profile view to reflect the changes
+	// if the specified profile is the current profile.
+	if isCurrentProfile {
+		pm.currentProfile = lp.View()
+	}
+
 	// An empty profile.ID indicates that the node info is not available yet,
 	// An empty profile.ID indicates that the node info is not available yet,
 	// and the profile doesn't need to be saved on disk.
 	// and the profile doesn't need to be saved on disk.
 	if lp.ID != "" {
 	if lp.ID != "" {
-		pm.knownProfiles[lp.ID] = lp
+		pm.knownProfiles[lp.ID] = lp.View()
 		if err := pm.writeKnownProfiles(); err != nil {
 		if err := pm.writeKnownProfiles(); err != nil {
-			return err
+			return ipn.LoginProfileView{}, err
 		}
 		}
 		// Clone prefsIn and create a read-only view as a safety measure to
 		// Clone prefsIn and create a read-only view as a safety measure to
 		// prevent accidental preference mutations, both externally and internally.
 		// prevent accidental preference mutations, both externally and internally.
-		if err := pm.setProfilePrefsNoPermCheck(lp, prefsIn.AsStruct().View()); err != nil {
-			return err
+		if err := pm.setProfilePrefsNoPermCheck(lp.View(), prefsIn.AsStruct().View()); err != nil {
+			return ipn.LoginProfileView{}, err
 		}
 		}
 	}
 	}
-	return nil
+	return lp.View(), nil
 }
 }
 
 
-func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.ProfileID, ipn.StateKey) {
+func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.ProfileID, ipn.StateKey) {
 	var idb [2]byte
 	var idb [2]byte
 	for {
 	for {
 		rand.Read(idb[:])
 		rand.Read(idb[:])
@@ -326,14 +339,14 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
 // The method does not perform any additional checks on the specified
 // The method does not perform any additional checks on the specified
 // profile, such as verifying the caller's access rights or checking
 // profile, such as verifying the caller's access rights or checking
 // if another profile for the same node already exists.
 // if another profile for the same node already exists.
-func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error {
+func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error {
 	isCurrentProfile := pm.currentProfile == profile
 	isCurrentProfile := pm.currentProfile == profile
 	if isCurrentProfile {
 	if isCurrentProfile {
 		pm.prefs = clonedPrefs
 		pm.prefs = clonedPrefs
 		pm.updateHealth()
 		pm.updateHealth()
 	}
 	}
-	if profile.Key != "" {
-		if err := pm.writePrefsToStore(profile.Key, clonedPrefs); err != nil {
+	if profile.Key() != "" {
+		if err := pm.writePrefsToStore(profile.Key(), clonedPrefs); err != nil {
 			return err
 			return err
 		}
 		}
 	} else if !isCurrentProfile {
 	} else if !isCurrentProfile {
@@ -362,11 +375,11 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
 }
 }
 
 
 // Profiles returns the list of known profiles accessible to the current user.
 // Profiles returns the list of known profiles accessible to the current user.
-func (pm *profileManager) Profiles() []ipn.LoginProfile {
+func (pm *profileManager) Profiles() []ipn.LoginProfileView {
 	allProfiles := pm.allProfiles()
 	allProfiles := pm.allProfiles()
-	out := make([]ipn.LoginProfile, len(allProfiles))
+	out := make([]ipn.LoginProfileView, len(allProfiles))
 	for i, p := range allProfiles {
 	for i, p := range allProfiles {
-		out[i] = *p
+		out[i] = p
 	}
 	}
 	return out
 	return out
 }
 }
@@ -374,26 +387,26 @@ func (pm *profileManager) Profiles() []ipn.LoginProfile {
 // ProfileByID returns a profile with the given id, if it is accessible to the current user.
 // ProfileByID returns a profile with the given id, if it is accessible to the current user.
 // If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
 // If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
 // If the profile does not exist, it returns an [errProfileNotFound].
 // If the profile does not exist, it returns an [errProfileNotFound].
-func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfile, error) {
+func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfileView, error) {
 	kp, err := pm.profileByIDNoPermCheck(id)
 	kp, err := pm.profileByIDNoPermCheck(id)
 	if err != nil {
 	if err != nil {
-		return ipn.LoginProfile{}, err
+		return ipn.LoginProfileView{}, err
 	}
 	}
 	if err := pm.checkProfileAccess(kp); err != nil {
 	if err := pm.checkProfileAccess(kp); err != nil {
-		return ipn.LoginProfile{}, err
+		return ipn.LoginProfileView{}, err
 	}
 	}
-	return *kp, nil
+	return kp, nil
 }
 }
 
 
 // profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't
 // profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't
 // check user's access rights to the profile.
 // check user's access rights to the profile.
-func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (*ipn.LoginProfile, error) {
-	if id == pm.currentProfile.ID {
+func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (ipn.LoginProfileView, error) {
+	if id == pm.currentProfile.ID() {
 		return pm.currentProfile, nil
 		return pm.currentProfile, nil
 	}
 	}
 	kp, ok := pm.knownProfiles[id]
 	kp, ok := pm.knownProfiles[id]
 	if !ok {
 	if !ok {
-		return nil, errProfileNotFound
+		return ipn.LoginProfileView{}, errProfileNotFound
 	}
 	}
 	return kp, nil
 	return kp, nil
 }
 }
@@ -412,11 +425,11 @@ func (pm *profileManager) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error)
 	return pm.profilePrefs(kp)
 	return pm.profilePrefs(kp)
 }
 }
 
 
-func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, error) {
-	if p.ID == pm.currentProfile.ID {
+func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, error) {
+	if p.ID() == pm.currentProfile.ID() {
 		return pm.prefs, nil
 		return pm.prefs, nil
 	}
 	}
-	return pm.loadSavedPrefs(p.Key)
+	return pm.loadSavedPrefs(p.Key())
 }
 }
 
 
 // SwitchProfile switches to the profile with the given id.
 // SwitchProfile switches to the profile with the given id.
@@ -429,14 +442,14 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
 	if !ok {
 	if !ok {
 		return errProfileNotFound
 		return errProfileNotFound
 	}
 	}
-	if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() {
+	if pm.currentProfile.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() {
 		return nil
 		return nil
 	}
 	}
 
 
 	if err := pm.checkProfileAccess(kp); err != nil {
 	if err := pm.checkProfileAccess(kp); err != nil {
 		return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id)
 		return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id)
 	}
 	}
-	prefs, err := pm.loadSavedPrefs(kp.Key)
+	prefs, err := pm.loadSavedPrefs(kp.Key())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -459,8 +472,8 @@ func (pm *profileManager) SwitchToDefaultProfile() error {
 
 
 // setProfileAsUserDefault sets the specified profile as the default for the current user.
 // setProfileAsUserDefault sets the specified profile as the default for the current user.
 // It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user.
 // It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user.
-func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) error {
-	if profile.Key == "" {
+func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView) error {
+	if profile.Key() == "" {
 		// The profile has not been persisted yet; ignore it for now.
 		// The profile has not been persisted yet; ignore it for now.
 		return nil
 		return nil
 	}
 	}
@@ -468,7 +481,7 @@ func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) err
 		return errProfileAccessDenied
 		return errProfileAccessDenied
 	}
 	}
 	k := ipn.CurrentProfileKey(string(pm.currentUserID))
 	k := ipn.CurrentProfileKey(string(pm.currentUserID))
-	return pm.WriteState(k, []byte(profile.Key))
+	return pm.WriteState(k, []byte(profile.Key()))
 }
 }
 
 
 func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
 func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
@@ -507,10 +520,10 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
 	return savedPrefs.View(), nil
 	return savedPrefs.View(), nil
 }
 }
 
 
-// CurrentProfile returns the current LoginProfile.
+// CurrentProfile returns a read-only [ipn.LoginProfileView] of the current profile.
 // The value may be zero if the profile is not persisted.
 // The value may be zero if the profile is not persisted.
-func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
-	return *pm.currentProfile
+func (pm *profileManager) CurrentProfile() ipn.LoginProfileView {
+	return pm.currentProfile
 }
 }
 
 
 // errProfileNotFound is returned by methods that accept a ProfileID
 // errProfileNotFound is returned by methods that accept a ProfileID
@@ -533,7 +546,7 @@ var errProfileAccessDenied = errors.New("profile access denied")
 // recommended to call [profileManager.SwitchProfile] first.
 // recommended to call [profileManager.SwitchProfile] first.
 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.currentProfile.ID {
+	if id == pm.currentProfile.ID() {
 		return pm.deleteCurrentProfile()
 		return pm.deleteCurrentProfile()
 	}
 	}
 	kp, ok := pm.knownProfiles[id]
 	kp, ok := pm.knownProfiles[id]
@@ -550,7 +563,7 @@ func (pm *profileManager) deleteCurrentProfile() error {
 	if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
 	if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
 		return err
 		return err
 	}
 	}
-	if pm.currentProfile.ID == "" {
+	if pm.currentProfile.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
@@ -560,14 +573,14 @@ func (pm *profileManager) deleteCurrentProfile() error {
 
 
 // deleteProfileNoPermCheck is like [profileManager.DeleteProfile],
 // deleteProfileNoPermCheck is like [profileManager.DeleteProfile],
 // but it doesn't check user's access rights to the profile.
 // but it doesn't check user's access rights to the profile.
-func (pm *profileManager) deleteProfileNoPermCheck(profile *ipn.LoginProfile) error {
-	if profile.ID == pm.currentProfile.ID {
+func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
+	if profile.ID() == pm.currentProfile.ID() {
 		pm.NewProfile()
 		pm.NewProfile()
 	}
 	}
-	if err := pm.WriteState(profile.Key, nil); err != nil {
+	if err := pm.WriteState(profile.Key(), nil); err != nil {
 		return err
 		return err
 	}
 	}
-	delete(pm.knownProfiles, profile.ID)
+	delete(pm.knownProfiles, profile.ID())
 	return pm.writeKnownProfiles()
 	return pm.writeKnownProfiles()
 }
 }
 
 
@@ -578,7 +591,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
 
 
 	currentProfileDeleted := false
 	currentProfileDeleted := false
 	writeKnownProfiles := func() error {
 	writeKnownProfiles := func() error {
-		if currentProfileDeleted || pm.currentProfile.ID == "" {
+		if currentProfileDeleted || pm.currentProfile.ID() == "" {
 			pm.NewProfile()
 			pm.NewProfile()
 		}
 		}
 		return pm.writeKnownProfiles()
 		return pm.writeKnownProfiles()
@@ -589,14 +602,14 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
 			// Skip profiles we don't have access to.
 			// Skip profiles we don't have access to.
 			continue
 			continue
 		}
 		}
-		if err := pm.WriteState(kp.Key, nil); err != nil {
+		if err := pm.WriteState(kp.Key(), nil); err != nil {
 			// Write to remove references to profiles we've already deleted, but
 			// Write to remove references to profiles we've already deleted, but
 			// return the original error.
 			// return the original error.
 			writeKnownProfiles()
 			writeKnownProfiles()
 			return err
 			return err
 		}
 		}
-		delete(pm.knownProfiles, kp.ID)
-		if kp.ID == pm.currentProfile.ID {
+		delete(pm.knownProfiles, kp.ID())
+		if kp.ID() == pm.currentProfile.ID() {
 			currentProfileDeleted = true
 			currentProfileDeleted = true
 		}
 		}
 	}
 	}
@@ -633,26 +646,27 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
 
 
 	pm.prefs = defaultPrefs
 	pm.prefs = defaultPrefs
 	pm.updateHealth()
 	pm.updateHealth()
-	pm.currentProfile = &ipn.LoginProfile{LocalUserID: uid}
+	newProfile := &ipn.LoginProfile{LocalUserID: uid}
+	pm.currentProfile = newProfile.View()
 }
 }
 
 
 // newProfileWithPrefs creates a new profile with the specified prefs and assigns
 // newProfileWithPrefs creates a new profile with the specified prefs and assigns
 // the specified uid as the profile owner. If switchNow is true, it switches to the
 // the specified uid as the profile owner. If switchNow is true, it switches to the
 // newly created profile immediately. It returns the newly created profile on success,
 // newly created profile immediately. It returns the newly created profile on success,
 // or an error on failure.
 // or an error on failure.
-func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) {
+func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) {
 	metricNewProfile.Add(1)
 	metricNewProfile.Add(1)
 
 
-	profile := &ipn.LoginProfile{LocalUserID: uid}
-	if err := pm.SetProfilePrefs(profile, prefs, ipn.NetworkProfile{}); err != nil {
-		return nil, err
+	profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
+	if err != nil {
+		return ipn.LoginProfileView{}, err
 	}
 	}
 	if switchNow {
 	if switchNow {
 		pm.currentProfile = profile
 		pm.currentProfile = profile
 		pm.prefs = prefs.AsStruct().View()
 		pm.prefs = prefs.AsStruct().View()
 		pm.updateHealth()
 		pm.updateHealth()
 		if err := pm.setProfileAsUserDefault(profile); err != nil {
 		if err := pm.setProfileAsUserDefault(profile); err != nil {
-			return nil, err
+			return ipn.LoginProfileView{}, err
 		}
 		}
 	}
 	}
 	return profile, nil
 	return profile, nil
@@ -711,8 +725,8 @@ func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
 	return ipn.StateKey(autoStartKey), nil
 	return ipn.StateKey(autoStartKey), nil
 }
 }
 
 
-func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) {
-	var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
+func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]ipn.LoginProfileView, error) {
+	var knownProfiles map[ipn.ProfileID]ipn.LoginProfileView
 	prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
 	prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
 	switch err {
 	switch err {
 	case nil:
 	case nil:
@@ -720,7 +734,7 @@ func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfil
 			return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
 			return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
 		}
 		}
 	case ipn.ErrStateNotExist:
 	case ipn.ErrStateNotExist:
-		knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile)
+		knownProfiles = make(map[ipn.ProfileID]ipn.LoginProfileView)
 	default:
 	default:
 		return nil, fmt.Errorf("calling ReadState on state store: %w", err)
 		return nil, fmt.Errorf("calling ReadState on state store: %w", err)
 	}
 	}
@@ -749,17 +763,17 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
 
 
 	if stateKey != "" {
 	if stateKey != "" {
 		for _, v := range knownProfiles {
 		for _, v := range knownProfiles {
-			if v.Key == stateKey {
+			if v.Key() == stateKey {
 				pm.currentProfile = v
 				pm.currentProfile = v
 			}
 			}
 		}
 		}
-		if pm.currentProfile == nil {
+		if !pm.currentProfile.Valid() {
 			if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
 			if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
 				pm.currentUserID = ipn.WindowsUserID(suf)
 				pm.currentUserID = ipn.WindowsUserID(suf)
 			}
 			}
 			pm.NewProfile()
 			pm.NewProfile()
 		} else {
 		} else {
-			pm.currentUserID = pm.currentProfile.LocalUserID
+			pm.currentUserID = pm.currentProfile.LocalUserID()
 		}
 		}
 		prefs, err := pm.loadSavedPrefs(stateKey)
 		prefs, err := pm.loadSavedPrefs(stateKey)
 		if err != nil {
 		if err != nil {
@@ -788,18 +802,18 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
 	return pm, nil
 	return pm, nil
 }
 }
 
 
-func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (*ipn.LoginProfile, error) {
+func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (ipn.LoginProfileView, error) {
 	metricMigration.Add(1)
 	metricMigration.Add(1)
 	sentinel, prefs, err := pm.loadLegacyPrefs(uid)
 	sentinel, prefs, err := pm.loadLegacyPrefs(uid)
 	if err != nil {
 	if err != nil {
 		metricMigrationError.Add(1)
 		metricMigrationError.Add(1)
-		return nil, fmt.Errorf("load legacy prefs: %w", err)
+		return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
 	}
 	}
 	pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
 	pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
 	profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
 	profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
 	if err != nil {
 	if err != nil {
 		metricMigrationError.Add(1)
 		metricMigrationError.Add(1)
-		return nil, fmt.Errorf("migrating _daemon profile: %w", err)
+		return ipn.LoginProfileView{}, fmt.Errorf("migrating _daemon profile: %w", err)
 	}
 	}
 	pm.completeMigration(sentinel)
 	pm.completeMigration(sentinel)
 	pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
 	pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
@@ -809,8 +823,8 @@ func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNo
 
 
 func (pm *profileManager) requiresBackfill() bool {
 func (pm *profileManager) requiresBackfill() bool {
 	return pm != nil &&
 	return pm != nil &&
-		pm.currentProfile != nil &&
-		pm.currentProfile.NetworkProfile.RequiresBackfill()
+		pm.currentProfile.Valid() &&
+		pm.currentProfile.NetworkProfile().RequiresBackfill()
 }
 }
 
 
 var (
 var (

+ 22 - 22
ipn/ipnlocal/profiles_test.go

@@ -52,11 +52,11 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
 	pm.SetCurrentUserID("user1")
 	pm.SetCurrentUserID("user1")
 	newProfile(t, "user1")
 	newProfile(t, "user1")
 	cp := pm.currentProfile
 	cp := pm.currentProfile
-	pm.DeleteProfile(cp.ID)
-	if pm.currentProfile == nil {
+	pm.DeleteProfile(cp.ID())
+	if !pm.currentProfile.Valid() {
 		t.Fatal("currentProfile is nil")
 		t.Fatal("currentProfile is nil")
-	} else if pm.currentProfile.ID != "" {
-		t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID)
+	} else if pm.currentProfile.ID() != "" {
+		t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID())
 	}
 	}
 	if !pm.CurrentPrefs().Equals(defaultPrefs) {
 	if !pm.CurrentPrefs().Equals(defaultPrefs) {
 		t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
 		t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
@@ -67,10 +67,10 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 	pm.SetCurrentUserID("user1")
 	pm.SetCurrentUserID("user1")
-	if pm.currentProfile == nil {
+	if !pm.currentProfile.Valid() {
 		t.Fatal("currentProfile is nil")
 		t.Fatal("currentProfile is nil")
-	} else if pm.currentProfile.ID != "" {
-		t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID)
+	} else if pm.currentProfile.ID() != "" {
+		t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID())
 	}
 	}
 	if !pm.CurrentPrefs().Equals(defaultPrefs) {
 	if !pm.CurrentPrefs().Equals(defaultPrefs) {
 		t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
 		t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
@@ -110,8 +110,8 @@ func TestProfileList(t *testing.T) {
 			t.Fatalf("got %d profiles, want %d", len(got), len(want))
 			t.Fatalf("got %d profiles, want %d", len(got), len(want))
 		}
 		}
 		for i, w := range 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)
+			if got[i].Name() != w {
+				t.Errorf("got profile %d name %q, want %q", i, got[i].Name(), w)
 			}
 			}
 		}
 		}
 	}
 	}
@@ -129,10 +129,10 @@ func TestProfileList(t *testing.T) {
 
 
 	pm.SetCurrentUserID("user1")
 	pm.SetCurrentUserID("user1")
 	checkProfiles(t, "alice", "bob")
 	checkProfiles(t, "alice", "bob")
-	if lp := pm.findProfileByKey(carol.Key); lp != nil {
+	if lp := pm.findProfileByKey(carol.Key()); lp.Valid() {
 		t.Fatalf("found profile for user2 in user1's profile list")
 		t.Fatalf("found profile for user2 in user1's profile list")
 	}
 	}
-	if lp := pm.findProfileByName(carol.Name); lp != nil {
+	if lp := pm.findProfileByName(carol.Name()); lp.Valid() {
 		t.Fatalf("found profile for user2 in user1's profile list")
 		t.Fatalf("found profile for user2 in user1's profile list")
 	}
 	}
 
 
@@ -294,7 +294,7 @@ func TestProfileDupe(t *testing.T) {
 			profs := pm.Profiles()
 			profs := pm.Profiles()
 			var got []*persist.Persist
 			var got []*persist.Persist
 			for _, p := range profs {
 			for _, p := range profs {
-				prefs, err := pm.loadSavedPrefs(p.Key)
+				prefs, err := pm.loadSavedPrefs(p.Key())
 				if err != nil {
 				if err != nil {
 					t.Fatal(err)
 					t.Fatal(err)
 				}
 				}
@@ -328,9 +328,9 @@ func TestProfileManagement(t *testing.T) {
 	checkProfiles := func(t *testing.T) {
 	checkProfiles := func(t *testing.T) {
 		t.Helper()
 		t.Helper()
 		prof := pm.CurrentProfile()
 		prof := pm.CurrentProfile()
-		t.Logf("\tCurrentProfile = %q", prof)
-		if prof.Name != wantCurProfile {
-			t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile)
+		t.Logf("\tCurrentProfile = %q", prof.Name())
+		if prof.Name() != wantCurProfile {
+			t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile)
 		}
 		}
 		profiles := pm.Profiles()
 		profiles := pm.Profiles()
 		wantLen := len(wantProfiles)
 		wantLen := len(wantProfiles)
@@ -349,13 +349,13 @@ func TestProfileManagement(t *testing.T) {
 			t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
 			t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
 		}
 		}
 		for _, p := range profiles {
 		for _, p := range profiles {
-			got, err := pm.loadSavedPrefs(p.Key)
+			got, err := pm.loadSavedPrefs(p.Key())
 			if err != nil {
 			if err != nil {
 				t.Fatal(err)
 				t.Fatal(err)
 			}
 			}
 			// Use Hostname as a proxy for all prefs.
 			// 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())
+			if !got.Equals(wantProfiles[p.Name()]) {
+				t.Fatalf("Prefs for profile %q =\n got=%+v\nwant=%v", p.Name(), got.Pretty(), wantProfiles[p.Name()].Pretty())
 			}
 			}
 		}
 		}
 	}
 	}
@@ -422,7 +422,7 @@ func TestProfileManagement(t *testing.T) {
 	checkProfiles(t)
 	checkProfiles(t)
 
 
 	t.Logf("Delete default profile")
 	t.Logf("Delete default profile")
-	if err := pm.DeleteProfile(pm.findProfileByName("[email protected]").ID); err != nil {
+	if err := pm.DeleteProfile(pm.ProfileIDForName("[email protected]")); err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 	delete(wantProfiles, "[email protected]")
 	delete(wantProfiles, "[email protected]")
@@ -506,9 +506,9 @@ func TestProfileManagementWindows(t *testing.T) {
 	checkProfiles := func(t *testing.T) {
 	checkProfiles := func(t *testing.T) {
 		t.Helper()
 		t.Helper()
 		prof := pm.CurrentProfile()
 		prof := pm.CurrentProfile()
-		t.Logf("\tCurrentProfile = %q", prof)
-		if prof.Name != wantCurProfile {
-			t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile)
+		t.Logf("\tCurrentProfile = %q", prof.Name())
+		if prof.Name() != wantCurProfile {
+			t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile)
 		}
 		}
 		if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) {
 		if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) {
 			t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
 			t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())

+ 1 - 1
ipn/ipnlocal/serve.go

@@ -318,7 +318,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
 		bs = j
 		bs = j
 	}
 	}
 
 
-	profileID := b.pm.CurrentProfile().ID
+	profileID := b.pm.CurrentProfile().ID()
 	confKey := ipn.ServeConfigKey(profileID)
 	confKey := ipn.ServeConfigKey(profileID)
 	if err := b.store.WriteState(confKey, bs); err != nil {
 	if err := b.store.WriteState(confKey, bs); err != nil {
 		return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
 		return fmt.Errorf("writing ServeConfig to StateStore: %w", err)

+ 1 - 1
ipn/ipnlocal/serve_test.go

@@ -898,7 +898,7 @@ func newTestBackend(t *testing.T) *LocalBackend {
 	b.SetVarRoot(dir)
 	b.SetVarRoot(dir)
 
 
 	pm := must.Get(newProfileManager(new(mem.Store), logf, new(health.Tracker)))
 	pm := must.Get(newProfileManager(new(mem.Store), logf, new(health.Tracker)))
-	pm.currentProfile = &ipn.LoginProfile{ID: "id0"}
+	pm.currentProfile = (&ipn.LoginProfile{ID: "id0"}).View()
 	b.pm = pm
 	b.pm = pm
 
 
 	b.netMap = &netmap.NetworkMap{
 	b.netMap = &netmap.NetworkMap{

+ 2 - 2
ipn/localapi/localapi.go

@@ -2601,8 +2601,8 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) {
 	switch r.Method {
 	switch r.Method {
 	case httpm.GET:
 	case httpm.GET:
 		profiles := h.b.ListProfiles()
 		profiles := h.b.ListProfiles()
-		profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfile) bool {
-			return p.ID == profileID
+		profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfileView) bool {
+			return p.ID() == profileID
 		})
 		})
 		if profileIndex == -1 {
 		if profileIndex == -1 {
 			http.Error(w, "Profile not found", http.StatusNotFound)
 			http.Error(w, "Profile not found", http.StatusNotFound)