From 280c84e46aac2790deccc4a7625a676b5805cc18 Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Tue, 14 Sep 2021 19:29:07 -0700 Subject: [PATCH] ipn/ipnserver, paths, logpolicy: move Window config files out of %LocalAppData% C:\WINDOWS\system32\config\systemprofile\AppData\Local\ is frequently cleared for almost any reason: Windows updates, System Restore, even various System Cleaner utilities. The server-state.conf file in AppData\Local could be deleted at any time, which would break login until the node is removed from the Admin Panel allowing it to create a new key. Carefully copy any AppData state to ProgramData at startup. If copying the state fails, continue to use AppData so at least there will be connectivity. If there is no state, use ProgramData. We also migrate the log.conf file. Very old versions of Tailscale named the EXE tailscale-ipn, so the log conf was tailscale-ipn.log.conf and more recent versions preserved this filename and cmdName in logs. In this migration we always update the filename to c:\ProgramData\Tailscale\tailscaled.log.conf Updates https://github.com/tailscale/tailscale/issues/2856 Signed-off-by: Denton Gentry --- ipn/ipnserver/server.go | 38 +++++++++++++++++++++++++---- log/filelogger/log.go | 2 +- logpolicy/logpolicy.go | 33 +++++++++++++++++++------ paths/migrate.go | 54 +++++++++++++++++++++++++++++++++++++++++ paths/paths.go | 2 +- 5 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 paths/migrate.go diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index e1966d70d..1d3ab7de5 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -20,6 +20,7 @@ import ( "os/exec" "os/signal" "os/user" + "path/filepath" "runtime" "strconv" "strings" @@ -37,6 +38,7 @@ import ( "tailscale.com/log/filelogger" "tailscale.com/logtail/backoff" "tailscale.com/net/netstat" + "tailscale.com/paths" "tailscale.com/safesocket" "tailscale.com/smallzstd" "tailscale.com/types/logger" @@ -581,6 +583,28 @@ func (s *server) writeToClients(n ipn.Notify) { } } +// tryWindowsAppDataMigration attempts to copy the Windows state file +// from its old location to the new location. (Issue 2856) +// +// Tailscale 1.14 and before stored state under %LocalAppData% +// (usually "C:\WINDOWS\system32\config\systemprofile\AppData\Local" +// when tailscaled.exe is running as a non-user system service). +// However it is frequently cleared for almost any reason: Windows +// updates, System Restore, even various System Cleaner utilities. +// +// Returns a string of the path to use for the state file. +// This will be a fallback %LocalAppData% path if migration fails, +// a %ProgramData% path otherwise. +func tryWindowsAppDataMigration(path string) string { + if path != paths.DefaultTailscaledStateFile() { + // If they're specifying a non-default path, just trust that they know + // what they are doing. + return path + } + oldFile := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf") + return paths.TryConfigFileMigration(oldFile, path) +} + // Run runs a Tailscale backend service. // The getEngine func is called repeatedly, once per connection, until it returns an engine successfully. func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error { @@ -614,23 +638,27 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() ( var store ipn.StateStore if opts.StatePath != "" { const kubePrefix = "kube:" + path := opts.StatePath switch { - case strings.HasPrefix(opts.StatePath, kubePrefix): - secretName := strings.TrimPrefix(opts.StatePath, kubePrefix) + case strings.HasPrefix(path, kubePrefix): + secretName := strings.TrimPrefix(path, kubePrefix) store, err = ipn.NewKubeStore(secretName) if err != nil { return fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err) } default: - store, err = ipn.NewFileStore(opts.StatePath) + if runtime.GOOS == "windows" { + path = tryWindowsAppDataMigration(path) + } + store, err = ipn.NewFileStore(path) if err != nil { - return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err) + return fmt.Errorf("ipn.NewFileStore(%q): %v", path, err) } } if opts.AutostartStateKey == "" { autoStartKey, err := store.ReadState(ipn.ServerModeStartKey) if err != nil && err != ipn.ErrStateNotExist { - return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err) + return fmt.Errorf("calling ReadState on %s: %w", path, err) } key := string(autoStartKey) if strings.HasPrefix(key, "user-") { diff --git a/log/filelogger/log.go b/log/filelogger/log.go index d33e691f8..789284276 100644 --- a/log/filelogger/log.go +++ b/log/filelogger/log.go @@ -36,7 +36,7 @@ func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf { if logf == nil { panic("nil logf") } - dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "Logs") + dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs") if err := os.MkdirAll(dir, 0700); err != nil { log.Printf("failed to create local log directory; not writing logs to disk: %v", err) diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index aa6a7ffb2..59bcc46ba 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -372,15 +372,34 @@ func New(collection string) *Policy { cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName)) - // The Windows service previously ran as tailscale-ipn.exe, so - // let's keep using that log base name if it exists. if runtime.GOOS == "windows" && cmdName == "tailscaled" { - const oldCmdName = "tailscale-ipn" - oldPath := filepath.Join(dir, oldCmdName+".log.conf") - if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() { - cfgPath = oldPath - cmdName = oldCmdName + // Tailscale 1.14 and before stored state under %LocalAppData% + // (usually "C:\WINDOWS\system32\config\systemprofile\AppData\Local" + // when tailscaled.exe is running as a non-user system service). + // However it is frequently cleared for almost any reason: Windows + // updates, System Restore, even various System Cleaner utilities. + // + // The Windows service previously ran as tailscale-ipn.exe, so + // machines which ran very old versions might still have their + // log conf named %LocalAppData%\tailscale-ipn.log.conf + // + // Machines which started using Tailscale more recently will have + // %LocalAppData%\tailscaled.log.conf + // + // Attempt to migrate the log conf to C:\ProgramData\Tailscale + oldDir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale") + + oldPath := filepath.Join(oldDir, "tailscaled.log.conf") + if fi, err := os.Stat(oldPath); err != nil || !fi.Mode().IsRegular() { + // *Only* if tailscaled.log.conf does not exist, + // check for tailscale-ipn.log.conf + oldPathOldCmd := filepath.Join(oldDir, "tailscale-ipn.log.conf") + if fi, err := os.Stat(oldPathOldCmd); err == nil && fi.Mode().IsRegular() { + oldPath = oldPathOldCmd + } } + + cfgPath = paths.TryConfigFileMigration(oldPath, cfgPath) } var oldc *Config diff --git a/paths/migrate.go b/paths/migrate.go new file mode 100644 index 000000000..d4fcb3bba --- /dev/null +++ b/paths/migrate.go @@ -0,0 +1,54 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paths + +import ( + "log" + "os" + "path/filepath" +) + +// TryConfigFileMigration carefully copies the contents of oldFile to +// newFile, returning the path which should be used to read the config. +// - if newFile already exists, don't modify it just return its path +// - if neither oldFile nor newFile exist, return newFile for a fresh +// default config to be written to. +// - if oldFile exists but copying to newFile fails, return oldFile so +// there will at least be some config to work with. +func TryConfigFileMigration(oldFile, newFile string) string { + _, err := os.Stat(newFile) + if err == nil { + // Common case for a system which has already been migrated. + return newFile + } + if !os.IsNotExist(err) { + log.Printf("TryConfigFileMigration failed; new file: %v", err) + return newFile + } + + contents, err := os.ReadFile(oldFile) + if err != nil { + // Common case for a new user. + return newFile + } + + os.MkdirAll(filepath.Dir(newFile), 0700) + err = os.WriteFile(newFile, contents, 0600) + if err != nil { + removeErr := os.Remove(newFile) + if removeErr != nil { + log.Printf("TryConfigFileMigration failed; write newFile no cleanup: %v, remove err: %v", + err, removeErr) + return oldFile + } + log.Printf("TryConfigFileMigration failed; write newFile: %v", err) + return oldFile + } + + log.Printf("TryConfigFileMigration: successfully migrated: from %v to %v", + oldFile, newFile) + + return newFile +} diff --git a/paths/paths.go b/paths/paths.go index 9f36af03c..1215ebb2f 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -55,7 +55,7 @@ func DefaultTailscaledStateFile() string { return f() } if runtime.GOOS == "windows" { - return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf") + return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "server-state.conf") } return "" }