cmd/tailscaled: add `-state=mem:` to support creation of an ephemeral node.

RELNOTE=`tailscaled --state=mem:` registers as an ephemeral node and
does not store state to disk.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/3978/head
Maisem Ali 2 years ago committed by Maisem Ali
parent 823d970d60
commit f9a50779e2

@ -28,6 +28,7 @@ import (
"time" "time"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnserver" "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.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.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.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:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM. If empty and --statedir is provided, the default is <statedir>/tailscaled.state") flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' 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 <statedir>/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.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.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird 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 o.VarRoot = dir
} }
} }
if strings.HasPrefix(statePathOrDefault(), "mem:") {
// Register as an ephemeral node.
o.LoginFlags = controlclient.LoginEphemeral
}
switch goos { 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: default:
o.SurviveDisconnects = true o.SurviveDisconnects = true
o.AutostartStateKey = ipn.GlobalDaemonStateKey o.AutostartStateKey = ipn.GlobalDaemonStateKey

@ -136,6 +136,7 @@ type LocalBackend struct {
prevIfState *interfaces.State prevIfState *interfaces.State
peerAPIServer *peerAPIServer // or nil peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
incomingFiles map[*incomingFile]bool incomingFiles map[*incomingFile]bool
// directFileRoot, if non-empty, means to write received files // directFileRoot, if non-empty, means to write received files
// directly to this directory, without staging them in an // 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. // but is not actually running.
// //
// If dialer is nil, a new one is made. // 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 { if e == nil {
panic("ipn.NewLocalBackend: engine must not be 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, state: ipn.NoState,
portpoll: portpoll, portpoll: portpoll,
gotPortPollRes: make(chan struct{}), gotPortPollRes: make(chan struct{}),
loginFlags: loginFlags,
} }
// Default filter blocks everything and logs nothing, until Start() is called. // Default filter blocks everything and logs nothing, until Start() is called.
@ -1569,13 +1571,14 @@ func (b *LocalBackend) InServerMode() bool {
} }
// Login implements Backend. // Login implements Backend.
// As of 2022-02-17, this is only exists for tests.
func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) { func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) {
b.mu.Lock() b.mu.Lock()
b.assertClientLocked() b.assertClientLocked()
cc := b.cc cc := b.cc
b.mu.Unlock() b.mu.Unlock()
cc.Login(token, controlclient.LoginInteractive) cc.Login(token, b.loginFlags|controlclient.LoginInteractive)
} }
// StartLoginInteractive implements Backend. It requests a new // StartLoginInteractive implements Backend. It requests a new
@ -1594,15 +1597,7 @@ func (b *LocalBackend) StartLoginInteractive() {
if url != "" { if url != "" {
b.popBrowserAuthNow() b.popBrowserAuthNow()
} else { } else {
flags := controlclient.LoginInteractive cc.Login(nil, b.loginFlags|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)
} }
} }

@ -463,7 +463,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
t.Fatalf("NewFakeUserspaceEngine: %v", err) t.Fatalf("NewFakeUserspaceEngine: %v", err)
} }
t.Cleanup(eng.Close) t.Cleanup(eng.Close)
lb, err := NewLocalBackend(logf, "logid", store, nil, eng) lb, err := NewLocalBackend(logf, "logid", store, nil, eng, 0)
if err != nil { if err != nil {
t.Fatalf("NewLocalBackend: %v", err) t.Fatalf("NewLocalBackend: %v", err)
} }

@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
} }
t.Cleanup(e.Close) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -293,7 +293,7 @@ func TestStateMachine(t *testing.T) {
cc := newMockControl(t) cc := newMockControl(t)
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020 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 { if err != nil {
t.Fatalf("NewLocalBackend: %v", err) t.Fatalf("NewLocalBackend: %v", err)
} }
@ -954,7 +954,7 @@ func TestWGEngineStatusRace(t *testing.T) {
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0) eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
t.Cleanup(eng.Close) 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) c.Assert(err, qt.IsNil)
cc := newMockControl(t) cc := newMockControl(t)

@ -85,6 +85,9 @@ type Options struct {
// the actual definition of "disconnect" is when the // the actual definition of "disconnect" is when the
// connection count transitions from 1 to 0. // connection count transitions from 1 to 0.
SurviveDisconnects bool 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 // 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: // Special cases:
// //
// * empty string means to use an in-memory store // * 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 // * if the string begins with "kube:", the suffix
// is a Kubernetes secret name // is a Kubernetes secret name
// * if the string begins with "arn:", the value is // * 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 == "" { if path == "" {
return &ipn.MemoryStore{}, nil return &ipn.MemoryStore{}, nil
} }
const memPrefix = "mem:"
const kubePrefix = "kube:" const kubePrefix = "kube:"
const arnPrefix = "arn:" const arnPrefix = "arn:"
switch { switch {
case strings.HasPrefix(path, memPrefix):
return &ipn.MemoryStore{}, nil
case strings.HasPrefix(path, kubePrefix): case strings.HasPrefix(path, kubePrefix):
secretName := strings.TrimPrefix(path, kubePrefix) secretName := strings.TrimPrefix(path, kubePrefix)
store, err := ipn.NewKubeStore(secretName) 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. // 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) { 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 { if err != nil {
return nil, fmt.Errorf("NewLocalBackend: %v", err) return nil, fmt.Errorf("NewLocalBackend: %v", err)
} }

@ -54,6 +54,10 @@ type Server struct {
// log.Printf is used. // log.Printf is used.
Logf logger.Logf 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 initOnce sync.Once
initErr error initErr error
lb *ipnlocal.LocalBackend lb *ipnlocal.LocalBackend
@ -173,7 +177,11 @@ func (s *Server) start() error {
} }
logid := "tslib-TODO" 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 { if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err) return fmt.Errorf("NewLocalBackend: %v", err)
} }

@ -13,6 +13,7 @@ import (
// process and can cache a prior success when a dependency changes. // process and can cache a prior success when a dependency changes.
_ "inet.af/netaddr" _ "inet.af/netaddr"
_ "tailscale.com/chirp" _ "tailscale.com/chirp"
_ "tailscale.com/control/controlclient"
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"

@ -13,6 +13,7 @@ import (
// process and can cache a prior success when a dependency changes. // process and can cache a prior success when a dependency changes.
_ "inet.af/netaddr" _ "inet.af/netaddr"
_ "tailscale.com/chirp" _ "tailscale.com/chirp"
_ "tailscale.com/control/controlclient"
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"

@ -13,6 +13,7 @@ import (
// process and can cache a prior success when a dependency changes. // process and can cache a prior success when a dependency changes.
_ "inet.af/netaddr" _ "inet.af/netaddr"
_ "tailscale.com/chirp" _ "tailscale.com/chirp"
_ "tailscale.com/control/controlclient"
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"

@ -13,6 +13,7 @@ import (
// process and can cache a prior success when a dependency changes. // process and can cache a prior success when a dependency changes.
_ "inet.af/netaddr" _ "inet.af/netaddr"
_ "tailscale.com/chirp" _ "tailscale.com/chirp"
_ "tailscale.com/control/controlclient"
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"

@ -16,6 +16,7 @@ import (
_ "golang.org/x/sys/windows/svc/mgr" _ "golang.org/x/sys/windows/svc/mgr"
_ "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" _ "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
_ "inet.af/netaddr" _ "inet.af/netaddr"
_ "tailscale.com/control/controlclient"
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"

Loading…
Cancel
Save