diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index 931e3ce07..a63046613 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -18,6 +18,7 @@ import ( "net" "net/http" "os" + "os/exec" "path/filepath" "runtime" "strconv" @@ -101,9 +102,9 @@ func (l logWriter) Write(buf []byte) (int, error) { // logsDir returns the directory to use for log configuration and // buffer storage. func logsDir() string { - systemdCacheDir := os.Getenv("CACHE_DIRECTORY") - if systemdCacheDir != "" { - return systemdCacheDir + systemdStateDir := os.Getenv("STATE_DIRECTORY") + if systemdStateDir != "" { + return systemdStateDir } cacheDir, err := os.UserCacheDir() @@ -125,7 +126,6 @@ func logsDir() string { if err != nil { panic("no safe place found to store log state") } - return tmp } @@ -138,6 +138,155 @@ func runningUnderSystemd() bool { return false } +// tryFixLogStateLocation is a temporary fixup for +// https://github.com/tailscale/tailscale/issues/247 . We accidentally +// wrote logging state files to /, and then later to $CACHE_DIRECTORY +// (which is incorrect because the log ID is not reconstructible if +// deleted - it's state, not cache data). +// +// If log state for cmdname exists in / or $CACHE_DIRECTORY, and no +// log state for that command exists in dir, then the log state is +// moved from whereever it does exist, into dir. Leftover logs state +// in / and $CACHE_DIRECTORY is deleted. +func tryFixLogStateLocation(dir, cmdname string) { + if cmdname == "" { + log.Printf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale") + return + } + if dir == "/" { + // Trying to store things in / still. That's a bug, but don't + // abort hard. + log.Printf("[unexpected] storing logging config in /, please file a bug at https://github.com/tailscale/tailscale") + return + } + if os.Getuid() != 0 { + // Only root could have written log configs to weird places. + return + } + switch runtime.GOOS { + case "linux", "freebsd", "openbsd": + // These are the OSes where we might have written stuff into + // root. Others use different logic to find the logs storage + // dir. + default: + return + } + + // We stored logs in 2 incorrect places: either /, or CACHE_DIR + // (aka /var/cache/tailscale). We want to move files into the + // provided dir, preferring those in CACHE_DIR over those in / if + // both exist. If files already exist in dir, don't + // overwrite. Finally, once we've maybe moved files around, we + // want to delete leftovers in / and CACHE_DIR, to clean up after + // our past selves. + + files := []string{ + fmt.Sprintf("%s.log.conf", cmdname), + fmt.Sprintf("%s.log1.txt", cmdname), + fmt.Sprintf("%s.log2.txt", cmdname), + } + + // checks if any of the files above exist in d. + checkExists := func(d string) (bool, error) { + for _, file := range files { + p := filepath.Join(d, file) + _, err := os.Stat(p) + if os.IsNotExist(err) { + continue + } else if err != nil { + return false, fmt.Errorf("stat %q: %w", p, err) + } + return true, nil + } + return false, nil + } + // move files from d into dir, if they exist. + moveFiles := func(d string) error { + for _, file := range files { + src := filepath.Join(d, file) + _, err := os.Stat(src) + if os.IsNotExist(err) { + continue + } else if err != nil { + return fmt.Errorf("stat %q: %v", src, err) + } + dst := filepath.Join(dir, file) + bs, err := exec.Command("mv", src, dst).CombinedOutput() + if err != nil { + return fmt.Errorf("mv %q %q: %v (%s)", src, dst, err, bs) + } + } + return nil + } + + existsInRoot, err := checkExists("/") + if err != nil { + log.Printf("checking for configs in /: %v", err) + return + } + existsInCache := false + cacheDir := os.Getenv("CACHE_DIRECTORY") + if cacheDir != "" { + existsInCache, err = checkExists("/var/cache/tailscale") + if err != nil { + log.Printf("checking for configs in %s: %v", cacheDir, err) + } + } + existsInDest, err := checkExists(dir) + if err != nil { + log.Printf("checking for configs in %s: %v", dir, err) + return + } + + switch { + case !existsInRoot && !existsInCache: + // No leftover files, nothing to do. + return + case existsInDest: + // Already have "canonical" configs, just delete any remnants + // (below). + case existsInCache: + // CACHE_DIRECTORY takes precedence over /, move files from + // there. + if err := moveFiles(cacheDir); err != nil { + log.Print(err) + return + } + case existsInRoot: + // Files from root is better than nothing. + if err := moveFiles("/"); err != nil { + log.Print(err) + return + } + } + + // If moving succeeded, or we didn't need to move files, try to + // delete any leftover files, but it's okay if we can't delete + // them for some reason. + dirs := []string{} + if existsInCache { + dirs = append(dirs, cacheDir) + } + if existsInRoot { + dirs = append(dirs, "/") + } + for _, d := range dirs { + for _, file := range files { + p := filepath.Join(d, file) + _, err := os.Stat(p) + if os.IsNotExist(err) { + continue + } else if err != nil { + log.Printf("stat %q: %v", p, err) + return + } + if err := os.Remove(p); err != nil { + log.Printf("rm %q: %v", p, err) + } + } + } +} + // New returns a new log policy (a logger and its instance ID) for a // given collection name. func New(collection string) *Policy { @@ -155,6 +304,9 @@ func New(collection string) *Policy { console := log.New(stderrWriter{}, "", lflags) dir := logsDir() + + tryFixLogStateLocation(dir, version.CmdName()) + cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", version.CmdName())) var oldc *Config data, err := ioutil.ReadFile(cfgPath)