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 "" }