diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 80743e522..4526dc3f9 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -28,6 +28,7 @@ import ( "time" "inet.af/netaddr" + "tailscale.com/control/controlclient" "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/ipn/ipnserver" @@ -123,7 +124,7 @@ 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(), "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.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. 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") @@ -238,8 +239,19 @@ func ipnServerOpts() (o ipnserver.Options) { o.VarRoot = dir } } + if strings.HasPrefix(statePathOrDefault(), "mem:") { + // Register as an ephemeral node. + o.LoginFlags = controlclient.LoginEphemeral + } switch goos { + case "js": + // The js/wasm client has no state storage so for now + // treat all interactive logins as ephemeral. + // TODO(bradfitz): if we start using browser LocalStorage + // or something, then rethink this. + o.LoginFlags = controlclient.LoginEphemeral + fallthrough default: o.SurviveDisconnects = true o.AutostartStateKey = ipn.GlobalDaemonStateKey diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 1862988d9..f134c22f5 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -136,6 +136,7 @@ type LocalBackend struct { prevIfState *interfaces.State peerAPIServer *peerAPIServer // or nil peerAPIListeners []*peerAPIListener + loginFlags controlclient.LoginFlags incomingFiles map[*incomingFile]bool // directFileRoot, if non-empty, means to write received files // directly to this directory, without staging them in an @@ -166,7 +167,7 @@ type clientGen func(controlclient.Options) (controlclient.Client, error) // but is not actually running. // // If dialer is nil, a new one is made. -func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine) (*LocalBackend, error) { +func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine, loginFlags controlclient.LoginFlags) (*LocalBackend, error) { if e == nil { panic("ipn.NewLocalBackend: engine must not be nil") } @@ -199,6 +200,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale state: ipn.NoState, portpoll: portpoll, gotPortPollRes: make(chan struct{}), + loginFlags: loginFlags, } // Default filter blocks everything and logs nothing, until Start() is called. @@ -1569,13 +1571,14 @@ func (b *LocalBackend) InServerMode() bool { } // Login implements Backend. +// As of 2022-02-17, this is only exists for tests. func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) { b.mu.Lock() b.assertClientLocked() cc := b.cc b.mu.Unlock() - cc.Login(token, controlclient.LoginInteractive) + cc.Login(token, b.loginFlags|controlclient.LoginInteractive) } // StartLoginInteractive implements Backend. It requests a new @@ -1594,15 +1597,7 @@ func (b *LocalBackend) StartLoginInteractive() { if url != "" { b.popBrowserAuthNow() } else { - flags := controlclient.LoginInteractive - if runtime.GOOS == "js" { - // The js/wasm client has no state storage so for now - // treat all interactive logins as ephemeral. - // TODO(bradfitz): if we start using browser LocalStorage - // or something, then rethink this. - flags |= controlclient.LoginEphemeral - } - cc.Login(nil, flags) + cc.Login(nil, b.loginFlags|controlclient.LoginInteractive) } } diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 280476048..94d5a28fe 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -463,7 +463,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) { t.Fatalf("NewFakeUserspaceEngine: %v", err) } t.Cleanup(eng.Close) - lb, err := NewLocalBackend(logf, "logid", store, nil, eng) + lb, err := NewLocalBackend(logf, "logid", store, nil, eng, 0) if err != nil { t.Fatalf("NewLocalBackend: %v", err) } diff --git a/ipn/ipnlocal/loglines_test.go b/ipn/ipnlocal/loglines_test.go index 56435a2ba..a34ae2f58 100644 --- a/ipn/ipnlocal/loglines_test.go +++ b/ipn/ipnlocal/loglines_test.go @@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) { } t.Cleanup(e.Close) - lb, err := NewLocalBackend(logf, idA.String(), store, nil, e) + lb, err := NewLocalBackend(logf, idA.String(), store, nil, e, 0) if err != nil { t.Fatal(err) } diff --git a/ipn/ipnlocal/state_test.go b/ipn/ipnlocal/state_test.go index 6cccd4b95..4632a0e39 100644 --- a/ipn/ipnlocal/state_test.go +++ b/ipn/ipnlocal/state_test.go @@ -293,7 +293,7 @@ func TestStateMachine(t *testing.T) { cc := newMockControl(t) t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020 - b, err := NewLocalBackend(logf, "logid", store, nil, e) + b, err := NewLocalBackend(logf, "logid", store, nil, e, 0) if err != nil { t.Fatalf("NewLocalBackend: %v", err) } @@ -954,7 +954,7 @@ func TestWGEngineStatusRace(t *testing.T) { eng, err := wgengine.NewFakeUserspaceEngine(logf, 0) c.Assert(err, qt.IsNil) t.Cleanup(eng.Close) - b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng) + b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng, 0) c.Assert(err, qt.IsNil) cc := newMockControl(t) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index c6ee17138..6c1a30dbc 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -85,6 +85,9 @@ type Options struct { // the actual definition of "disconnect" is when the // connection count transitions from 1 to 0. SurviveDisconnects bool + + // LoginFlags specifies the LoginFlags to pass to the client. + LoginFlags controlclient.LoginFlags } // Server is an IPN backend and its set of 0 or more active localhost @@ -660,6 +663,8 @@ func tryWindowsAppDataMigration(logf logger.Logf, path string) string { // Special cases: // // * empty string means to use an in-memory store +// * if the string begins with "mem:", the suffix +// is ignored and an in-memory store is used. // * if the string begins with "kube:", the suffix // is a Kubernetes secret name // * if the string begins with "arn:", the value is @@ -668,9 +673,12 @@ func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) { if path == "" { return &ipn.MemoryStore{}, nil } + const memPrefix = "mem:" const kubePrefix = "kube:" const arnPrefix = "arn:" switch { + case strings.HasPrefix(path, memPrefix): + return &ipn.MemoryStore{}, nil case strings.HasPrefix(path, kubePrefix): secretName := strings.TrimPrefix(path, kubePrefix) store, err := ipn.NewKubeStore(secretName) @@ -797,7 +805,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State // // To start it, use the Server.Run method. func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, dialer *tsdial.Dialer, serverModeUser *user.User, opts Options) (*Server, error) { - b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng) + b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, opts.LoginFlags) if err != nil { return nil, fmt.Errorf("NewLocalBackend: %v", err) } diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 6956f4df6..1b73098f0 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -54,6 +54,10 @@ type Server struct { // log.Printf is used. Logf logger.Logf + // Ephemeral, if true, specifies that the instance should register + // as an Ephemeral node (https://tailscale.com/kb/1111/ephemeral-nodes/). + Emphemeral bool + initOnce sync.Once initErr error lb *ipnlocal.LocalBackend @@ -173,7 +177,11 @@ func (s *Server) start() error { } logid := "tslib-TODO" - lb, err := ipnlocal.NewLocalBackend(logf, logid, store, s.dialer, eng) + loginFlags := controlclient.LoginDefault + if s.Emphemeral { + loginFlags = controlclient.LoginEphemeral + } + lb, err := ipnlocal.NewLocalBackend(logf, logid, store, s.dialer, eng, loginFlags) if err != nil { return fmt.Errorf("NewLocalBackend: %v", err) } diff --git a/tstest/integration/tailscaled_deps_test_darwin.go b/tstest/integration/tailscaled_deps_test_darwin.go index 34305bee8..577a66937 100644 --- a/tstest/integration/tailscaled_deps_test_darwin.go +++ b/tstest/integration/tailscaled_deps_test_darwin.go @@ -13,6 +13,7 @@ import ( // process and can cache a prior success when a dependency changes. _ "inet.af/netaddr" _ "tailscale.com/chirp" + _ "tailscale.com/control/controlclient" _ "tailscale.com/derp/derphttp" _ "tailscale.com/envknob" _ "tailscale.com/ipn" diff --git a/tstest/integration/tailscaled_deps_test_freebsd.go b/tstest/integration/tailscaled_deps_test_freebsd.go index 34305bee8..577a66937 100644 --- a/tstest/integration/tailscaled_deps_test_freebsd.go +++ b/tstest/integration/tailscaled_deps_test_freebsd.go @@ -13,6 +13,7 @@ import ( // process and can cache a prior success when a dependency changes. _ "inet.af/netaddr" _ "tailscale.com/chirp" + _ "tailscale.com/control/controlclient" _ "tailscale.com/derp/derphttp" _ "tailscale.com/envknob" _ "tailscale.com/ipn" diff --git a/tstest/integration/tailscaled_deps_test_linux.go b/tstest/integration/tailscaled_deps_test_linux.go index 34305bee8..577a66937 100644 --- a/tstest/integration/tailscaled_deps_test_linux.go +++ b/tstest/integration/tailscaled_deps_test_linux.go @@ -13,6 +13,7 @@ import ( // process and can cache a prior success when a dependency changes. _ "inet.af/netaddr" _ "tailscale.com/chirp" + _ "tailscale.com/control/controlclient" _ "tailscale.com/derp/derphttp" _ "tailscale.com/envknob" _ "tailscale.com/ipn" diff --git a/tstest/integration/tailscaled_deps_test_openbsd.go b/tstest/integration/tailscaled_deps_test_openbsd.go index 34305bee8..577a66937 100644 --- a/tstest/integration/tailscaled_deps_test_openbsd.go +++ b/tstest/integration/tailscaled_deps_test_openbsd.go @@ -13,6 +13,7 @@ import ( // process and can cache a prior success when a dependency changes. _ "inet.af/netaddr" _ "tailscale.com/chirp" + _ "tailscale.com/control/controlclient" _ "tailscale.com/derp/derphttp" _ "tailscale.com/envknob" _ "tailscale.com/ipn" diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go index b365fcbd5..5a938122d 100644 --- a/tstest/integration/tailscaled_deps_test_windows.go +++ b/tstest/integration/tailscaled_deps_test_windows.go @@ -16,6 +16,7 @@ import ( _ "golang.org/x/sys/windows/svc/mgr" _ "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" _ "inet.af/netaddr" + _ "tailscale.com/control/controlclient" _ "tailscale.com/derp/derphttp" _ "tailscale.com/envknob" _ "tailscale.com/ipn"