diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 47babf497..ddeb2ab6b 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -20,6 +20,7 @@ import ( "net/http/pprof" "os" "os/signal" + "path/filepath" "runtime" "runtime/debug" "strconv" @@ -78,6 +79,7 @@ var args struct { debug string port uint16 statepath string + statedir string socketpath string birdSocketPath string verbose int @@ -114,7 +116,8 @@ func main() { flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`) flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`) flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select") - flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM") + flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM. If empty and --statedir is provided, the default is /tailscaled.state") + flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.") flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket") flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket") flag.BoolVar(&printVersion, "version", false, "print version information and exit") @@ -202,6 +205,16 @@ func trySynologyMigration(p string) error { return nil } +func statePathOrDefault() string { + if args.statepath != "" { + return args.statepath + } + if args.statedir != "" { + return filepath.Join(args.statedir, "tailscaled.state") + } + return "" +} + func ipnServerOpts() (o ipnserver.Options) { // Allow changing the OS-specific IPN behavior for tests // so we can e.g. test Windows-specific behaviors on Linux. @@ -211,8 +224,17 @@ func ipnServerOpts() (o ipnserver.Options) { } o.Port = 41112 - o.StatePath = args.statepath + o.StatePath = statePathOrDefault() o.SocketPath = args.socketpath // even for goos=="windows", for tests + o.VarRoot = args.statedir + + // If an absolute --state is provided but not --statedir, try to derive + // a state directory. + if o.VarRoot == "" && filepath.IsAbs(args.statepath) { + if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "tailscale") { + o.VarRoot = dir + } + } switch goos { default: @@ -261,10 +283,10 @@ func run() error { return nil } - if args.statepath == "" { - log.Fatalf("--state is required") + if args.statepath == "" && args.statedir == "" { + log.Fatalf("--statedir (or at least --state) is required") } - if err := trySynologyMigration(args.statepath); err != nil { + if err := trySynologyMigration(statePathOrDefault()); err != nil { log.Printf("error in synology migration: %v", err) } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index d3e118a58..d9434acfc 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -96,6 +96,7 @@ type LocalBackend struct { gotPortPollRes chan struct{} // closed upon first readPoller result serverURL string // tailcontrol URL newDecompressor func() (controlclient.Decompressor, error) + varRoot string // or empty if SetVarRoot never called filterHash deephash.Sum @@ -1998,34 +1999,29 @@ func normalizeResolver(cfg dnstype.Resolver) dnstype.Resolver { return cfg } +// SetVarRoot sets the root directory of Tailscale's writable +// storage area . (e.g. "/var/lib/tailscale") +// +// It should only be called before the LocalBackend is used. +func (b *LocalBackend) SetVarRoot(dir string) { + b.varRoot = dir +} + // TailscaleVarRoot returns the root directory of Tailscale's writable // storage area. (e.g. "/var/lib/tailscale") // // It returns an empty string if there's no configured or discovered // location. func (b *LocalBackend) TailscaleVarRoot() string { + if b.varRoot != "" { + return b.varRoot + } switch runtime.GOOS { case "ios", "android": dir, _ := paths.AppSharedDir.Load().(string) return dir } - // Temporary (2021-09-27) transitional fix for #2927 (Synology - // cert dir) on the way towards a more complete fix - // (#2932). It fixes any case where the state file is provided - // to tailscaled explicitly when it's not in the default - // location. - if fs, ok := b.store.(*ipn.FileStore); ok { - if fp := fs.Path(); fp != "" { - if dir := filepath.Dir(fp); strings.EqualFold(filepath.Base(dir), "tailscale") { - return dir - } - } - } - stateFile := paths.DefaultTailscaledStateFile() - if stateFile == "" { - return "" - } - return filepath.Dir(stateFile) + return "" } func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string { diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 2c70387dc..180f1e624 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -61,8 +61,25 @@ type Options struct { Port int // StatePath is the path to the stored agent state. + // It should be an absolute path to a file. + // + // Special cases: + // + // * empty string means to use an in-memory store + // * if the string begins with "kube:", the suffix + // is a Kubernetes secret name + // * if the string begins with "arn:", the value is + // an AWS ARN for an SSM. StatePath string + // VarRoot is the the Tailscale daemon's private writable + // directory (usually "/var/lib/tailscale" on Linux) that + // contains the "tailscaled.state" file, the "certs" directory + // for TLS certs, and the "files" directory for incoming + // Taildrop files before they're moved to a user directory. + // If empty, Taildrop and TLS certs don't function. + VarRoot string + // AutostartStateKey, if non-empty, immediately starts the agent // using the given StateKey. If empty, the agent stays idle and // waits for a frontend to start it. @@ -744,6 +761,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi if err != nil { return nil, fmt.Errorf("NewLocalBackend: %v", err) } + b.SetVarRoot(opts.VarRoot) b.SetDecompressor(func() (controlclient.Decompressor, error) { return smallzstd.NewDecoder(nil) })