mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 21:27:31 +00:00
ipn/ipnlocal, util/winutil/policy: modify Windows profile migration to load legacy prefs from within tailscaled
I realized that a lot of the problems that we're seeing around migration and LocalBackend state can be avoided if we drive Windows pref migration entirely from within tailscaled. By doing it this way, tailscaled can automatically perform the migration as soon as the connection with the client frontend is established. Since tailscaled is already running as LocalSystem, it already has access to the user's local AppData directory. The profile manager already knows which user is connected, so we simply need to resolve the user's prefs file and read it from there. Of course, to properly migrate this information we need to also check system policies. I moved a bunch of policy resolution code out of the GUI and into a new package in util/winutil/policy. Updates #7626 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// profileManager is a wrapper around a StateStore that manages
|
||||
@@ -66,7 +65,13 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error {
|
||||
// the selected profile for the current user.
|
||||
b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid)))
|
||||
if err == ipn.ErrStateNotExist || len(b) == 0 {
|
||||
pm.NewProfile()
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := pm.migrateFromLegacyPrefs(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
pm.NewProfile()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -424,12 +429,7 @@ var defaultPrefs = func() ipn.PrefsView {
|
||||
prefs.WantRunning = false
|
||||
|
||||
prefs.ControlURL = winutil.GetPolicyString("LoginURL", "")
|
||||
|
||||
if exitNode := winutil.GetPolicyString("ExitNodeIP", ""); exitNode != "" {
|
||||
if ip, err := netip.ParseAddr(exitNode); err == nil {
|
||||
prefs.ExitNodeIP = ip
|
||||
}
|
||||
}
|
||||
prefs.ExitNodeIP = resolveExitNodeIP(netip.Addr{})
|
||||
|
||||
// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
|
||||
// backend), so this has to convert between the two conventions.
|
||||
@@ -439,6 +439,16 @@ var defaultPrefs = func() ipn.PrefsView {
|
||||
return prefs.View()
|
||||
}()
|
||||
|
||||
func resolveExitNodeIP(defIP netip.Addr) (ret netip.Addr) {
|
||||
ret = defIP
|
||||
if exitNode := winutil.GetPolicyString("ExitNodeIP", ""); exitNode != "" {
|
||||
if ip, err := netip.ParseAddr(exitNode); err == nil {
|
||||
ret = ip
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Store returns the StateStore used by the ProfileManager.
|
||||
func (pm *profileManager) Store() ipn.StateStore {
|
||||
return pm.store
|
||||
@@ -549,27 +559,16 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, goos stri
|
||||
func (pm *profileManager) migrateFromLegacyPrefs() error {
|
||||
metricMigration.Add(1)
|
||||
pm.NewProfile()
|
||||
k := ipn.LegacyGlobalDaemonStateKey
|
||||
switch {
|
||||
case runtime.GOOS == "ios":
|
||||
k = "ipn-go-bridge"
|
||||
case version.IsSandboxedMacOS():
|
||||
k = "ipn-go-bridge"
|
||||
case runtime.GOOS == "android":
|
||||
k = "ipn-android"
|
||||
}
|
||||
prefs, err := pm.loadSavedPrefs(k)
|
||||
sentinel, prefs, err := pm.loadLegacyPrefs()
|
||||
if err != nil {
|
||||
metricMigrationError.Add(1)
|
||||
return fmt.Errorf("calling ReadState on state store: %w", err)
|
||||
return err
|
||||
}
|
||||
pm.logf("migrating %q profile to new format", k)
|
||||
if err := pm.SetPrefs(prefs); err != nil {
|
||||
metricMigrationError.Add(1)
|
||||
return fmt.Errorf("migrating _daemon profile: %w", err)
|
||||
}
|
||||
// Do not delete the old state key, as we may be downgraded to an
|
||||
// older version that still relies on it.
|
||||
pm.completeMigration(sentinel)
|
||||
metricMigrationSuccess.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
37
ipn/ipnlocal/profiles_notwindows.go
Normal file
37
ipn/ipnlocal/profiles_notwindows.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
|
||||
k := ipn.LegacyGlobalDaemonStateKey
|
||||
switch {
|
||||
case runtime.GOOS == "ios":
|
||||
k = "ipn-go-bridge"
|
||||
case version.IsSandboxedMacOS():
|
||||
k = "ipn-go-bridge"
|
||||
case runtime.GOOS == "android":
|
||||
k = "ipn-android"
|
||||
}
|
||||
prefs, err := pm.loadSavedPrefs(k)
|
||||
if err != nil {
|
||||
return "", ipn.PrefsView{}, fmt.Errorf("calling ReadState on state store: %w", err)
|
||||
}
|
||||
pm.logf("migrating %q profile to new format", k)
|
||||
return "", prefs, nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) completeMigration(migrationSentinel string) {
|
||||
// Do not delete the old state key, as we may be downgraded to an
|
||||
// older version that still relies on it.
|
||||
}
|
84
ipn/ipnlocal/profiles_windows.go
Normal file
84
ipn/ipnlocal/profiles_windows.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/winutil/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyPrefsFile = "prefs"
|
||||
legacyPrefsMigrationSentinelFile = "_migrated-to-profiles"
|
||||
legacyPrefsExt = ".conf"
|
||||
)
|
||||
|
||||
var errAlreadyMigrated = errors.New("profile migration already completed")
|
||||
|
||||
func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) {
|
||||
// TODO(aaron): Ideally we'd have the impersonation token for the pipe's
|
||||
// client and use it to call SHGetKnownFolderPath, thus yielding the correct
|
||||
// path without having to make gross assumptions about directory names.
|
||||
usr, err := user.LookupId(string(uid))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if usr.HomeDir == "" {
|
||||
return "", fmt.Errorf("user %q does not have a home directory", uid)
|
||||
}
|
||||
userLegacyPrefsDir := filepath.Join(usr.HomeDir, "AppData", "Local", "Tailscale")
|
||||
return userLegacyPrefsDir, nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
|
||||
userLegacyPrefsDir, err := legacyPrefsDir(pm.currentUserID)
|
||||
if err != nil {
|
||||
return "", ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
migrationSentinel := filepath.Join(userLegacyPrefsDir, legacyPrefsMigrationSentinelFile+legacyPrefsExt)
|
||||
// verify that migration sentinel is not present
|
||||
_, err = os.Stat(migrationSentinel)
|
||||
if err == nil {
|
||||
return "", ipn.PrefsView{}, errAlreadyMigrated
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return "", ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
prefsPath := filepath.Join(userLegacyPrefsDir, legacyPrefsFile+legacyPrefsExt)
|
||||
prefs, err := ipn.LoadPrefs(prefsPath)
|
||||
if err != nil {
|
||||
return "", ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
prefs.ControlURL = policy.SelectControlURL(defaultPrefs.ControlURL(), prefs.ControlURL)
|
||||
prefs.ExitNodeIP = resolveExitNodeIP(prefs.ExitNodeIP)
|
||||
prefs.ShieldsUp = resolveShieldsUp(prefs.ShieldsUp)
|
||||
prefs.ForceDaemon = resolveForceDaemon(prefs.ForceDaemon)
|
||||
|
||||
pm.logf("migrating Windows profile to new format")
|
||||
return migrationSentinel, prefs.View(), nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) completeMigration(migrationSentinel string) {
|
||||
atomicfile.WriteFile(migrationSentinel, []byte{}, 0600)
|
||||
}
|
||||
|
||||
func resolveShieldsUp(defval bool) bool {
|
||||
pol := policy.GetPreferenceOptionPolicy("AllowIncomingConnections")
|
||||
return !pol.ShouldEnable(!defval)
|
||||
}
|
||||
|
||||
func resolveForceDaemon(defval bool) bool {
|
||||
pol := policy.GetPreferenceOptionPolicy("UnattendedMode")
|
||||
return pol.ShouldEnable(defval)
|
||||
}
|
Reference in New Issue
Block a user