Skip to content

Commit

Permalink
ipn/ipnlocal: add support for multiple profiles
Browse files Browse the repository at this point in the history
Currently all user state is stored in ipn.Prefs, which is then persisted
into the specified ipn.StateStore under a single key "_daemon" except in
the case of Windows. Windows kind of already supports multiple profiles
as multiple users can log in the node and go into "Unattended" mode.

On Windows we do this by storing the desired prefs in the StateStore under
`user-<user-id>` and then store another `server-mode-start-key` which only
has `user-<user-id>` in the value. This allows tailscaled to discover that
it needs to run in `Unattended` mode and it also informs tailscaled as to
which prefs it needs to load. Say if the prefs are for UserID 123, we would
store the actual prefs in `user-123` and store `user-123` in
`server-mode-start-key`.

For the generic support for multiple profiles, we leave the Windows behavior
intact and unchanged. Instead we store profiles in a new format
`profile-<profile-name>` and add another key `_current-profile` which would
then point to `profile-<profile-name>`.

The primary reason for the different keys is to not accidentally start a
a profile on Windows while also allowing multiple profiles.

By adding a new ProfileManager we also simplify some of the state
machine from LocalBackend as it no longer needs to own prefs and also write it to disk.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
  • Loading branch information
maisem committed Oct 25, 2022
1 parent a471681 commit 17e9a06
Show file tree
Hide file tree
Showing 27 changed files with 857 additions and 323 deletions.
2 changes: 0 additions & 2 deletions cmd/tailscale/cli/up.go
Expand Up @@ -632,7 +632,6 @@ func runUp(ctx context.Context, args []string) (retErr error) {
return err
}
opts := ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
AuthKey: authKey,
UpdatePrefs: prefs,
}
Expand All @@ -647,7 +646,6 @@ func runUp(ctx context.Context, args []string) (retErr error) {
if effectiveGOOS() == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
opts.Prefs = prefs
}

Expand Down
4 changes: 1 addition & 3 deletions cmd/tailscale/cli/web.go
Expand Up @@ -496,9 +496,7 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU

bc.SetPrefs(prefs)

bc.Start(ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
})
bc.Start(ipn.Options{})
if forceReauth {
bc.StartLoginInteractive()
}
Expand Down
9 changes: 6 additions & 3 deletions cmd/tailscaled/tailscaled.go
Expand Up @@ -34,7 +34,7 @@ import (
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
"tailscale.com/logpolicy"
Expand Down Expand Up @@ -307,7 +307,6 @@ func ipnServerOpts() (o ipnserver.Options) {
fallthrough
default:
o.SurviveDisconnects = true
o.AutostartStateKey = ipn.GlobalDaemonStateKey
case "windows":
// Not those.
}
Expand Down Expand Up @@ -453,7 +452,11 @@ func run() error {
if err != nil {
return fmt.Errorf("store.New: %w", err)
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
pm, err := ipnlocal.NewProfileManager(store, logf, "")
if err != nil {
return fmt.Errorf("ipnlocal.NewProfileManager: %w", err)
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), pm, e, dialer, opts)
if err != nil {
return fmt.Errorf("ipnserver.New: %w", err)
}
Expand Down
7 changes: 5 additions & 2 deletions cmd/tsconnect/wasm/wasm_js.go
Expand Up @@ -124,7 +124,11 @@ func newIPN(jsConfig js.Value) map[string]any {
return ns.DialContextTCP(ctx, dst)
}

srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, nil, ipnserver.Options{
pm, err := ipnlocal.NewProfileManager(store, logf, "wasm")
if err != nil {
log.Fatalf("ipnlocal.NewProfileManager: %v", err)
}
srv, err := ipnserver.New(logf, lpc.PublicID.String(), pm, eng, dialer, ipnserver.Options{
SurviveDisconnects: true,
LoginFlags: controlclient.LoginEphemeral,
})
Expand Down Expand Up @@ -284,7 +288,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {

go func() {
err := i.lb.Start(ipn.Options{
StateKey: "wasm",
UpdatePrefs: &ipn.Prefs{
ControlURL: i.controlURL,
RouteAll: false,
Expand Down
4 changes: 2 additions & 2 deletions control/controlclient/auto.go
Expand Up @@ -585,7 +585,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
}
if nm != nil && loggedIn && synced {
pp := c.direct.GetPersist()
p = &pp
p = pp.AsStruct()
} else {
// don't send netmap status, as it's misleading when we're
// not logged in.
Expand Down Expand Up @@ -703,7 +703,7 @@ func (c *Auto) Shutdown() {
// used exclusively in tests.
func (c *Auto) TestOnlyNodePublicKey() key.NodePublic {
priv := c.direct.GetPersist()
return priv.PrivateNodeKey.Public()
return priv.PrivateNodeKey().Public()
}

func (c *Auto) TestOnlySetAuthKey(authkey string) {
Expand Down
4 changes: 2 additions & 2 deletions control/controlclient/direct.go
Expand Up @@ -317,10 +317,10 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
return true
}

func (c *Direct) GetPersist() persist.Persist {
func (c *Direct) GetPersist() persist.PersistView {
c.mu.Lock()
defer c.mu.Unlock()
return c.persist
return c.persist.View()
}

func (c *Direct) TryLogout(ctx context.Context) error {
Expand Down
20 changes: 5 additions & 15 deletions ipn/backend.go
Expand Up @@ -178,21 +178,11 @@ type StateKey string
type Options struct {
// FrontendLogID is the public logtail id used by the frontend.
FrontendLogID string
// StateKey and Prefs together define the state the backend should
// use:
// - StateKey=="" && Prefs!=nil: use Prefs for internal state,
// don't persist changes in the backend, except for the machine key
// for migration purposes.
// - StateKey!="" && Prefs==nil: load the given backend-side
// state and use/update that.
// - StateKey!="" && Prefs!=nil: like the previous case, but do
// an initial overwrite of backend state with Prefs.
//
// NOTE(apenwarr): The above means that this Prefs field does not do
// what you probably think it does. It will overwrite your encryption
// keys. Do not use unless you know what you're doing.
StateKey StateKey
Prefs *Prefs
// Prefs is the initial preferences to use. If nil, the current
// profile's preferences are loaded from the store.
// If non-nil, the Prefs are used as-is, and the state store is
// updated to match.
Prefs *Prefs
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
// already stored in the backend state, *except* for the Persist
// Persist member. If you just want to provide prefs, this is
Expand Down

0 comments on commit 17e9a06

Please sign in to comment.