ipn/ipnlocal: add support for multiple user profiles
Signed-off-by: Maisem Ali <maisem@tailscale.com>pull/6281/head
parent
c9d6a9cb4d
commit
4d330bac14
|
@ -634,27 +634,10 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts := ipn.Options{
|
bc.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
|
||||||
AuthKey: authKey,
|
AuthKey: authKey,
|
||||||
UpdatePrefs: prefs,
|
UpdatePrefs: prefs,
|
||||||
}
|
})
|
||||||
// On Windows, we still run in mostly the "legacy" way that
|
|
||||||
// predated the server's StateStore. That is, we send an empty
|
|
||||||
// StateKey and send the prefs directly. Although the Windows
|
|
||||||
// supports server mode, though, the transition to StateStore
|
|
||||||
// is only half complete. Only server mode uses it, and the
|
|
||||||
// Windows service (~tailscaled) is the one that computes the
|
|
||||||
// StateKey based on the connection identity. So for now, just
|
|
||||||
// do as the Windows GUI's always done:
|
|
||||||
if effectiveGOOS() == "windows" {
|
|
||||||
// The Windows service will set this as needed based
|
|
||||||
// on our connection's identity.
|
|
||||||
opts.StateKey = ""
|
|
||||||
opts.Prefs = prefs
|
|
||||||
}
|
|
||||||
|
|
||||||
bc.Start(opts)
|
|
||||||
if upArgs.forceReauth {
|
if upArgs.forceReauth {
|
||||||
startLoginInteractive()
|
startLoginInteractive()
|
||||||
}
|
}
|
||||||
|
|
|
@ -496,9 +496,7 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
|
||||||
|
|
||||||
bc.SetPrefs(prefs)
|
bc.SetPrefs(prefs)
|
||||||
|
|
||||||
bc.Start(ipn.Options{
|
bc.Start(ipn.Options{})
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
|
||||||
})
|
|
||||||
if forceReauth {
|
if forceReauth {
|
||||||
bc.StartLoginInteractive()
|
bc.StartLoginInteractive()
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ import (
|
||||||
"tailscale.com/cmd/tailscaled/childproc"
|
"tailscale.com/cmd/tailscaled/childproc"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
|
||||||
"tailscale.com/ipn/ipnserver"
|
"tailscale.com/ipn/ipnserver"
|
||||||
"tailscale.com/ipn/store"
|
"tailscale.com/ipn/store"
|
||||||
"tailscale.com/logpolicy"
|
"tailscale.com/logpolicy"
|
||||||
|
@ -306,7 +305,6 @@ func ipnServerOpts() (o ipnserver.Options) {
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
o.SurviveDisconnects = true
|
o.SurviveDisconnects = true
|
||||||
o.AutostartStateKey = ipn.GlobalDaemonStateKey
|
|
||||||
case "windows":
|
case "windows":
|
||||||
// Not those.
|
// Not those.
|
||||||
}
|
}
|
||||||
|
@ -452,7 +450,7 @@ func run() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("store.New: %w", err)
|
return fmt.Errorf("store.New: %w", err)
|
||||||
}
|
}
|
||||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
|
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ipnserver.New: %w", err)
|
return fmt.Errorf("ipnserver.New: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,9 +124,10 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||||
return ns.DialContextTCP(ctx, dst)
|
return ns.DialContextTCP(ctx, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, nil, ipnserver.Options{
|
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, ipnserver.Options{
|
||||||
SurviveDisconnects: true,
|
SurviveDisconnects: true,
|
||||||
LoginFlags: controlclient.LoginEphemeral,
|
LoginFlags: controlclient.LoginEphemeral,
|
||||||
|
AutostartStateKey: "wasm",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("ipnserver.New: %v", err)
|
log.Fatalf("ipnserver.New: %v", err)
|
||||||
|
@ -284,7 +285,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := i.lb.Start(ipn.Options{
|
err := i.lb.Start(ipn.Options{
|
||||||
StateKey: "wasm",
|
|
||||||
UpdatePrefs: &ipn.Prefs{
|
UpdatePrefs: &ipn.Prefs{
|
||||||
ControlURL: i.controlURL,
|
ControlURL: i.controlURL,
|
||||||
RouteAll: false,
|
RouteAll: false,
|
||||||
|
|
|
@ -590,7 +590,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||||
}
|
}
|
||||||
if nm != nil && loggedIn && synced {
|
if nm != nil && loggedIn && synced {
|
||||||
pp := c.direct.GetPersist()
|
pp := c.direct.GetPersist()
|
||||||
p = &pp
|
p = pp.AsStruct()
|
||||||
} else {
|
} else {
|
||||||
// don't send netmap status, as it's misleading when we're
|
// don't send netmap status, as it's misleading when we're
|
||||||
// not logged in.
|
// not logged in.
|
||||||
|
@ -708,7 +708,7 @@ func (c *Auto) Shutdown() {
|
||||||
// used exclusively in tests.
|
// used exclusively in tests.
|
||||||
func (c *Auto) TestOnlyNodePublicKey() key.NodePublic {
|
func (c *Auto) TestOnlyNodePublicKey() key.NodePublic {
|
||||||
priv := c.direct.GetPersist()
|
priv := c.direct.GetPersist()
|
||||||
return priv.PrivateNodeKey.Public()
|
return priv.PrivateNodeKey().Public()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Auto) TestOnlySetAuthKey(authkey string) {
|
func (c *Auto) TestOnlySetAuthKey(authkey string) {
|
||||||
|
|
|
@ -333,10 +333,10 @@ func (c *Direct) SetTKAHead(tkaHead string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Direct) GetPersist() persist.Persist {
|
func (c *Direct) GetPersist() persist.PersistView {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
return c.persist
|
return c.persist.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Direct) TryLogout(ctx context.Context) error {
|
func (c *Direct) TryLogout(ctx context.Context) error {
|
||||||
|
@ -633,6 +633,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||||
if resp.Login.LoginName != "" {
|
if resp.Login.LoginName != "" {
|
||||||
persist.LoginName = resp.Login.LoginName
|
persist.LoginName = resp.Login.LoginName
|
||||||
}
|
}
|
||||||
|
persist.UserProfile = tailcfg.UserProfile{
|
||||||
|
ID: resp.User.ID,
|
||||||
|
DisplayName: resp.Login.DisplayName,
|
||||||
|
ProfilePicURL: resp.Login.ProfilePicURL,
|
||||||
|
LoginName: resp.Login.LoginName,
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(crawshaw): RegisterResponse should be able to mechanically
|
// TODO(crawshaw): RegisterResponse should be able to mechanically
|
||||||
// communicate some extra instructions from the server:
|
// communicate some extra instructions from the server:
|
||||||
|
|
|
@ -20,13 +20,13 @@ import (
|
||||||
type State int
|
type State int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NoState = State(iota)
|
NoState State = 0
|
||||||
InUseOtherUser
|
InUseOtherUser State = 1
|
||||||
NeedsLogin
|
NeedsLogin State = 2
|
||||||
NeedsMachineAuth
|
NeedsMachineAuth State = 3
|
||||||
Stopped
|
Stopped State = 4
|
||||||
Starting
|
Starting State = 5
|
||||||
Running
|
Running State = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google
|
// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google
|
||||||
|
@ -153,21 +153,8 @@ type PartialFile struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateKey is an opaque identifier for a set of LocalBackend state
|
// StateKey is an opaque identifier for a set of LocalBackend state
|
||||||
// (preferences, private keys, etc.).
|
// (preferences, private keys, etc.). It is also used as a key for
|
||||||
//
|
// the various LoginProfiles that the instance may be signed into.
|
||||||
// The reason we need this is that the Tailscale agent may be running
|
|
||||||
// on a multi-user machine, in a context where a single daemon is
|
|
||||||
// shared by several consecutive users. Ideally we would just use the
|
|
||||||
// username of the connected frontend as the StateKey.
|
|
||||||
//
|
|
||||||
// Various platforms currently set StateKey in different ways:
|
|
||||||
//
|
|
||||||
// - the macOS/iOS GUI apps set it to "ipn-go-bridge"
|
|
||||||
// - the Android app sets it to "ipn-android"
|
|
||||||
// - on Windows, it's the empty string (in client mode) or, via
|
|
||||||
// LocalBackend.userID, a string like "user-$USER_ID" (used in
|
|
||||||
// server mode).
|
|
||||||
// - on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
|
|
||||||
//
|
//
|
||||||
// Additionally, the StateKey can be debug setting name:
|
// Additionally, the StateKey can be debug setting name:
|
||||||
//
|
//
|
||||||
|
@ -178,21 +165,10 @@ type StateKey string
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// FrontendLogID is the public logtail id used by the frontend.
|
// FrontendLogID is the public logtail id used by the frontend.
|
||||||
FrontendLogID string
|
FrontendLogID string
|
||||||
// StateKey and Prefs together define the state the backend should
|
// LegacyMigrationPrefs are used to migrate preferences from the
|
||||||
// use:
|
// frontend to the backend.
|
||||||
// - StateKey=="" && Prefs!=nil: use Prefs for internal state,
|
// If non-nil, they are imported as a new profile.
|
||||||
// don't persist changes in the backend, except for the machine key
|
LegacyMigrationPrefs *Prefs `json:"Prefs"`
|
||||||
// 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
|
|
||||||
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
|
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
|
||||||
// already stored in the backend state, *except* for the Persist
|
// already stored in the backend state, *except* for the Persist
|
||||||
// Persist member. If you just want to provide prefs, this is
|
// Persist member. If you just want to provide prefs, this is
|
||||||
|
|
|
@ -16,13 +16,13 @@ type FakeBackend struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *FakeBackend) Start(opts Options) error {
|
func (b *FakeBackend) Start(opts Options) error {
|
||||||
b.serverURL = opts.Prefs.ControlURLOrDefault()
|
b.serverURL = opts.LegacyMigrationPrefs.ControlURLOrDefault()
|
||||||
if b.notify == nil {
|
if b.notify == nil {
|
||||||
panic("FakeBackend.Start: SetNotifyCallback not called")
|
panic("FakeBackend.Start: SetNotifyCallback not called")
|
||||||
}
|
}
|
||||||
nl := NeedsLogin
|
nl := NeedsLogin
|
||||||
if b.notify != nil {
|
if b.notify != nil {
|
||||||
p := opts.Prefs.View()
|
p := opts.LegacyMigrationPrefs.View()
|
||||||
b.notify(Notify{Prefs: &p})
|
b.notify(Notify{Prefs: &p})
|
||||||
b.notify(Notify{State: &nl})
|
b.notify(Notify{State: &nl})
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,7 @@ type LocalBackend struct {
|
||||||
keyLogf logger.Logf // for printing list of peers on change
|
keyLogf logger.Logf // for printing list of peers on change
|
||||||
statsLogf logger.Logf // for printing peers stats on change
|
statsLogf logger.Logf // for printing peers stats on change
|
||||||
e wgengine.Engine
|
e wgengine.Engine
|
||||||
|
pm *profileManager
|
||||||
store ipn.StateStore
|
store ipn.StateStore
|
||||||
dialer *tsdial.Dialer // non-nil
|
dialer *tsdial.Dialer // non-nil
|
||||||
backendLogID string
|
backendLogID string
|
||||||
|
@ -138,6 +139,10 @@ type LocalBackend struct {
|
||||||
sshAtomicBool atomic.Bool
|
sshAtomicBool atomic.Bool
|
||||||
shutdownCalled bool // if Shutdown has been called
|
shutdownCalled bool // if Shutdown has been called
|
||||||
|
|
||||||
|
// lastProfileID tracks the last profile we've seen from the ProfileManager.
|
||||||
|
// It's used to detect when the user has changed their profile.
|
||||||
|
lastProfileID ipn.ProfileID
|
||||||
|
|
||||||
filterAtomic atomic.Pointer[filter.Filter]
|
filterAtomic atomic.Pointer[filter.Filter]
|
||||||
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool]
|
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool]
|
||||||
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
|
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
|
||||||
|
@ -151,9 +156,6 @@ type LocalBackend struct {
|
||||||
notify func(ipn.Notify)
|
notify func(ipn.Notify)
|
||||||
cc controlclient.Client
|
cc controlclient.Client
|
||||||
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
||||||
stateKey ipn.StateKey // computed in part from user-provided value
|
|
||||||
userID string // current controlling user ID (for Windows, primarily)
|
|
||||||
prefs ipn.PrefsView // may not be Valid.
|
|
||||||
inServerMode bool
|
inServerMode bool
|
||||||
machinePrivKey key.MachinePrivate
|
machinePrivKey key.MachinePrivate
|
||||||
nlPrivKey key.NLPrivate
|
nlPrivKey key.NLPrivate
|
||||||
|
@ -226,11 +228,16 @@ 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, loginFlags controlclient.LoginFlags) (*LocalBackend, error) {
|
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, stateKey ipn.StateKey, 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pm, err := newProfileManager(store, logf, stateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
hi := hostinfo.New()
|
hi := hostinfo.New()
|
||||||
logf.JSON(1, "Hostinfo", hi)
|
logf.JSON(1, "Hostinfo", hi)
|
||||||
envknob.LogCurrent(logf)
|
envknob.LogCurrent(logf)
|
||||||
|
@ -253,7 +260,8 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||||
e: e,
|
e: e,
|
||||||
store: store,
|
pm: pm,
|
||||||
|
store: pm.Store(),
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
backendLogID: logid,
|
backendLogID: logid,
|
||||||
state: ipn.NoState,
|
state: ipn.NoState,
|
||||||
|
@ -292,7 +300,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||||
|
|
||||||
for _, component := range debuggableComponents {
|
for _, component := range debuggableComponents {
|
||||||
key := componentStateKey(component)
|
key := componentStateKey(component)
|
||||||
if ut, err := ipn.ReadStoreInt(store, key); err == nil {
|
if ut, err := ipn.ReadStoreInt(pm.Store(), key); err == nil {
|
||||||
if until := time.Unix(ut, 0); until.After(time.Now()) {
|
if until := time.Unix(ut, 0); until.After(time.Now()) {
|
||||||
// conditional to avoid log spam at start when off
|
// conditional to avoid log spam at start when off
|
||||||
b.SetComponentDebugLogging(component, until)
|
b.SetComponentDebugLogging(component, until)
|
||||||
|
@ -450,7 +458,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||||
|
|
||||||
// If the local network configuration has changed, our filter may
|
// If the local network configuration has changed, our filter may
|
||||||
// need updating to tweak default routes.
|
// need updating to tweak default routes.
|
||||||
b.updateFilterLocked(b.netMap, b.prefs)
|
b.updateFilterLocked(b.netMap, b.pm.CurrentPrefs())
|
||||||
|
|
||||||
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
|
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
|
||||||
want := len(b.netMap.Addresses)
|
want := len(b.netMap.Addresses)
|
||||||
|
@ -520,7 +528,7 @@ func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
|
||||||
func (b *LocalBackend) Prefs() ipn.PrefsView {
|
func (b *LocalBackend) Prefs() ipn.PrefsView {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
return stripKeysFromPrefs(b.prefs)
|
return stripKeysFromPrefs(b.pm.CurrentPrefs())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns the latest status of the backend and its
|
// Status returns the latest status of the backend and its
|
||||||
|
@ -577,14 +585,14 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||||
s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||||
s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied
|
s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied
|
||||||
s.CurrentTailnet.Name = b.netMap.Domain
|
s.CurrentTailnet.Name = b.netMap.Domain
|
||||||
if b.prefs.Valid() && !b.prefs.ExitNodeID().IsZero() {
|
if prefs := b.pm.CurrentPrefs(); prefs.Valid() && !prefs.ExitNodeID().IsZero() {
|
||||||
if exitPeer, ok := b.netMap.PeerWithStableID(b.prefs.ExitNodeID()); ok {
|
if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok {
|
||||||
var online = false
|
var online = false
|
||||||
if exitPeer.Online != nil {
|
if exitPeer.Online != nil {
|
||||||
online = *exitPeer.Online
|
online = *exitPeer.Online
|
||||||
}
|
}
|
||||||
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
|
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
|
||||||
ID: b.prefs.ExitNodeID(),
|
ID: prefs.ExitNodeID(),
|
||||||
Online: online,
|
Online: online,
|
||||||
TailscaleIPs: exitPeer.Addresses,
|
TailscaleIPs: exitPeer.Addresses,
|
||||||
}
|
}
|
||||||
|
@ -628,6 +636,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||||
for id, up := range b.netMap.UserProfiles {
|
for id, up := range b.netMap.UserProfiles {
|
||||||
sb.AddUser(id, up)
|
sb.AddUser(id, up)
|
||||||
}
|
}
|
||||||
|
exitNodeID := b.pm.CurrentPrefs().ExitNodeID()
|
||||||
for _, p := range b.netMap.Peers {
|
for _, p := range b.netMap.Peers {
|
||||||
var lastSeen time.Time
|
var lastSeen time.Time
|
||||||
if p.LastSeen != nil {
|
if p.LastSeen != nil {
|
||||||
|
@ -650,7 +659,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||||
LastSeen: lastSeen,
|
LastSeen: lastSeen,
|
||||||
Online: p.Online != nil && *p.Online,
|
Online: p.Online != nil && *p.Online,
|
||||||
ShareeNode: p.Hostinfo.ShareeNode(),
|
ShareeNode: p.Hostinfo.ShareeNode(),
|
||||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID(),
|
ExitNode: p.StableID != "" && p.StableID == exitNodeID,
|
||||||
SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(),
|
SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(),
|
||||||
}
|
}
|
||||||
peerStatusFromNode(ps, p)
|
peerStatusFromNode(ps, p)
|
||||||
|
@ -796,8 +805,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||||
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs := b.prefs.AsStruct()
|
prefs := b.pm.CurrentPrefs().AsStruct()
|
||||||
stateKey := b.stateKey
|
|
||||||
netMap := b.netMap
|
netMap := b.netMap
|
||||||
interact := b.interact
|
interact := b.interact
|
||||||
|
|
||||||
|
@ -834,9 +842,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||||
prefsChanged = true
|
prefsChanged = true
|
||||||
}
|
}
|
||||||
// Prefs will be written out; this is not safe unless locked or cloned.
|
// Prefs will be written out; this is not safe unless locked or cloned.
|
||||||
if prefsChanged {
|
|
||||||
b.prefs = prefs.View()
|
|
||||||
}
|
|
||||||
if st.NetMap != nil {
|
if st.NetMap != nil {
|
||||||
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
|
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
|
||||||
if err := b.tkaSyncIfNeeded(st.NetMap); err != nil {
|
if err := b.tkaSyncIfNeeded(st.NetMap); err != nil {
|
||||||
|
@ -858,23 +863,22 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||||
b.tkaFilterNetmapLocked(st.NetMap)
|
b.tkaFilterNetmapLocked(st.NetMap)
|
||||||
}
|
}
|
||||||
b.setNetMapLocked(st.NetMap)
|
b.setNetMapLocked(st.NetMap)
|
||||||
b.updateFilterLocked(st.NetMap, b.prefs)
|
b.updateFilterLocked(st.NetMap, prefs.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefsChanged {
|
||||||
|
if err := b.pm.SetPrefs(prefs.View()); err != nil {
|
||||||
|
b.logf("Failed to save new controlclient state: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
userID := b.userID
|
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
// Now complete the lock-free parts of what we started while locked.
|
// Now complete the lock-free parts of what we started while locked.
|
||||||
if prefsChanged {
|
if prefsChanged {
|
||||||
if stateKey != "" {
|
|
||||||
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
|
|
||||||
b.logf("Failed to save new controlclient state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.writeServerModeStartState(userID, prefs.View())
|
|
||||||
|
|
||||||
p := prefs.View()
|
p := prefs.View()
|
||||||
b.send(ipn.Notify{Prefs: &p})
|
b.send(ipn.Notify{Prefs: &p})
|
||||||
}
|
}
|
||||||
|
|
||||||
if st.NetMap != nil {
|
if st.NetMap != nil {
|
||||||
if netMap != nil {
|
if netMap != nil {
|
||||||
diff := st.NetMap.ConciseDiffFrom(netMap)
|
diff := st.NetMap.ConciseDiffFrom(netMap)
|
||||||
|
@ -1064,8 +1068,7 @@ func (b *LocalBackend) startIsNoopLocked(opts ipn.Options) bool {
|
||||||
return b.state == ipn.Running &&
|
return b.state == ipn.Running &&
|
||||||
b.hostinfo != nil &&
|
b.hostinfo != nil &&
|
||||||
b.hostinfo.FrontendLogID == opts.FrontendLogID &&
|
b.hostinfo.FrontendLogID == opts.FrontendLogID &&
|
||||||
b.stateKey == opts.StateKey &&
|
opts.LegacyMigrationPrefs == nil &&
|
||||||
opts.Prefs == nil &&
|
|
||||||
opts.UpdatePrefs == nil &&
|
opts.UpdatePrefs == nil &&
|
||||||
opts.AuthKey == ""
|
opts.AuthKey == ""
|
||||||
}
|
}
|
||||||
|
@ -1081,29 +1084,30 @@ func (b *LocalBackend) startIsNoopLocked(opts ipn.Options) bool {
|
||||||
// actually a supported operation (it should be, but it's very unclear
|
// actually a supported operation (it should be, but it's very unclear
|
||||||
// from the following whether or not that is a safe transition).
|
// from the following whether or not that is a safe transition).
|
||||||
func (b *LocalBackend) Start(opts ipn.Options) error {
|
func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||||
if opts.Prefs == nil && opts.StateKey == "" {
|
if opts.LegacyMigrationPrefs == nil && !b.pm.CurrentPrefs().Valid() {
|
||||||
return errors.New("no state key or prefs provided")
|
return errors.New("no prefs provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Prefs != nil {
|
if opts.LegacyMigrationPrefs != nil {
|
||||||
b.logf("Start: %v", opts.Prefs.Pretty())
|
b.logf("Start: %v", opts.LegacyMigrationPrefs.Pretty())
|
||||||
} else {
|
} else {
|
||||||
b.logf("Start")
|
b.logf("Start")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
|
profileID := b.pm.CurrentProfile().ID
|
||||||
|
|
||||||
// The iOS client sends a "Start" whenever its UI screen comes
|
// The iOS client sends a "Start" whenever its UI screen comes
|
||||||
// up, just because it wants a netmap. That should be fixed,
|
// up, just because it wants a netmap. That should be fixed,
|
||||||
// but meanwhile we can make Start cheaper here for such a
|
// but meanwhile we can make Start cheaper here for such a
|
||||||
// case and not restart the world (which takes a few seconds).
|
// case and not restart the world (which takes a few seconds).
|
||||||
// Instead, just send a notify with the state that iOS needs.
|
// Instead, just send a notify with the state that iOS needs.
|
||||||
if b.startIsNoopLocked(opts) {
|
if b.startIsNoopLocked(opts) && profileID == b.lastProfileID {
|
||||||
b.logf("Start: already running; sending notify")
|
b.logf("Start: already running; sending notify")
|
||||||
nm := b.netMap
|
nm := b.netMap
|
||||||
state := b.state
|
state := b.state
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
p := b.prefs
|
p := b.pm.CurrentPrefs()
|
||||||
b.send(ipn.Notify{
|
b.send(ipn.Notify{
|
||||||
State: &state,
|
State: &state,
|
||||||
NetMap: nm,
|
NetMap: nm,
|
||||||
|
@ -1139,26 +1143,24 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||||
b.hostinfo = hostinfo
|
b.hostinfo = hostinfo
|
||||||
b.state = ipn.NoState
|
b.state = ipn.NoState
|
||||||
|
|
||||||
if err := b.loadStateLocked(opts.StateKey, opts.Prefs); err != nil {
|
if err := b.migrateStateLocked(opts.LegacyMigrationPrefs); err != nil {
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
return fmt.Errorf("loading requested state: %v", err)
|
return fmt.Errorf("loading requested state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.UpdatePrefs != nil {
|
if opts.UpdatePrefs != nil {
|
||||||
newPrefs := opts.UpdatePrefs
|
oldPrefs := b.pm.CurrentPrefs()
|
||||||
newPrefs.Persist = b.prefs.Persist()
|
newPrefs := opts.UpdatePrefs.Clone()
|
||||||
b.prefs = newPrefs.View()
|
newPrefs.Persist = oldPrefs.Persist()
|
||||||
|
pv := newPrefs.View()
|
||||||
if opts.StateKey != "" {
|
if err := b.pm.SetPrefs(pv); err != nil {
|
||||||
if err := b.store.WriteState(opts.StateKey, b.prefs.ToBytes()); err != nil {
|
b.logf("failed to save UpdatePrefs state: %v", err)
|
||||||
b.logf("failed to save UpdatePrefs state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
b.setAtomicValuesFromPrefs(b.prefs)
|
b.setAtomicValuesFromPrefs(pv)
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantRunning := b.prefs.WantRunning()
|
prefs := b.pm.CurrentPrefs()
|
||||||
|
wantRunning := prefs.WantRunning()
|
||||||
if wantRunning {
|
if wantRunning {
|
||||||
if err := b.initMachineKeyLocked(); err != nil {
|
if err := b.initMachineKeyLocked(); err != nil {
|
||||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||||
|
@ -1168,17 +1170,20 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||||
return fmt.Errorf("initNLKeyLocked: %w", err)
|
return fmt.Errorf("initNLKeyLocked: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loggedOut := b.prefs.LoggedOut()
|
loggedOut := prefs.LoggedOut()
|
||||||
|
|
||||||
b.inServerMode = b.prefs.ForceDaemon()
|
b.inServerMode = prefs.ForceDaemon()
|
||||||
b.serverURL = b.prefs.ControlURLOrDefault()
|
b.serverURL = prefs.ControlURLOrDefault()
|
||||||
if b.inServerMode || runtime.GOOS == "windows" {
|
if b.inServerMode || runtime.GOOS == "windows" {
|
||||||
b.logf("Start: serverMode=%v", b.inServerMode)
|
b.logf("Start: serverMode=%v", b.inServerMode)
|
||||||
}
|
}
|
||||||
b.applyPrefsToHostinfo(hostinfo, b.prefs)
|
b.applyPrefsToHostinfo(hostinfo, prefs)
|
||||||
|
|
||||||
b.setNetMapLocked(nil)
|
b.setNetMapLocked(nil)
|
||||||
persistv := b.prefs.Persist()
|
persistv := prefs.Persist()
|
||||||
|
if persistv == nil {
|
||||||
|
persistv = new(persist.Persist)
|
||||||
|
}
|
||||||
b.updateFilterLocked(nil, ipn.PrefsView{})
|
b.updateFilterLocked(nil, ipn.PrefsView{})
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
@ -1206,10 +1211,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||||
discoPublic := b.e.DiscoPublicKey()
|
discoPublic := b.e.DiscoPublicKey()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if persistv == nil {
|
|
||||||
// let controlclient initialize it
|
|
||||||
persistv = &persist.Persist{}
|
|
||||||
}
|
|
||||||
|
|
||||||
isNetstack := wgengine.IsNetstackRouter(b.e)
|
isNetstack := wgengine.IsNetstackRouter(b.e)
|
||||||
debugFlags := controlDebugFlags
|
debugFlags := controlDebugFlags
|
||||||
|
@ -1272,10 +1273,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||||
|
|
||||||
b.e.SetNetInfoCallback(b.setNetInfo)
|
b.e.SetNetInfoCallback(b.setNetInfo)
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
prefs := b.prefs
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
blid := b.backendLogID
|
blid := b.backendLogID
|
||||||
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
||||||
b.send(ipn.Notify{BackendLogID: &blid})
|
b.send(ipn.Notify{BackendLogID: &blid})
|
||||||
|
@ -1779,8 +1776,8 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var legacyMachineKey key.MachinePrivate
|
var legacyMachineKey key.MachinePrivate
|
||||||
if b.prefs.Persist() != nil {
|
if p := b.pm.CurrentPrefs().Persist(); p != nil {
|
||||||
legacyMachineKey = b.prefs.Persist().LegacyFrontendPrivateMachineKey
|
legacyMachineKey = p.LegacyFrontendPrivateMachineKey
|
||||||
}
|
}
|
||||||
|
|
||||||
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
|
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
|
||||||
|
@ -1804,11 +1801,6 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||||
// have a legacy machine key, use that. Otherwise generate a
|
// have a legacy machine key, use that. Otherwise generate a
|
||||||
// new one.
|
// new one.
|
||||||
if !legacyMachineKey.IsZero() {
|
if !legacyMachineKey.IsZero() {
|
||||||
if b.stateKey == "" {
|
|
||||||
b.logf("using frontend-provided legacy machine key")
|
|
||||||
} else {
|
|
||||||
b.logf("using legacy machine key from state key %q", b.stateKey)
|
|
||||||
}
|
|
||||||
b.machinePrivKey = legacyMachineKey
|
b.machinePrivKey = legacyMachineKey
|
||||||
} else {
|
} else {
|
||||||
b.logf("generating new machine key")
|
b.logf("generating new machine key")
|
||||||
|
@ -1865,110 +1857,23 @@ func (b *LocalBackend) initNLKeyLocked() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeServerModeStartState stores the ServerModeStartKey value based on the current
|
// migrateStateLocked migrates state from the frontend to the backend.
|
||||||
// user and prefs. If userID is blank or prefs is blank, no work is done.
|
// It is a no-op if prefs is nil
|
||||||
//
|
// b.mu must be held.
|
||||||
// b.mu may either be held or not.
|
func (b *LocalBackend) migrateStateLocked(prefs *ipn.Prefs) (err error) {
|
||||||
func (b *LocalBackend) writeServerModeStartState(userID string, prefs ipn.PrefsView) {
|
if prefs == nil && !b.pm.CurrentPrefs().Valid() {
|
||||||
if userID == "" || !prefs.Valid() {
|
return fmt.Errorf("no prefs provided and no current profile")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if prefs.ForceDaemon() {
|
|
||||||
stateKey := ipn.StateKey("user-" + userID)
|
|
||||||
if err := b.store.WriteState(ipn.ServerModeStartKey, []byte(stateKey)); err != nil {
|
|
||||||
b.logf("WriteState error: %v", err)
|
|
||||||
}
|
|
||||||
// It's important we do this here too, even if it looks
|
|
||||||
// redundant with the one in the 'if stateKey != ""'
|
|
||||||
// check block above. That one won't fire in the case
|
|
||||||
// where the Windows client started up in client mode.
|
|
||||||
// This happens when we transition into server mode:
|
|
||||||
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
|
|
||||||
b.logf("WriteState error: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := b.store.WriteState(ipn.ServerModeStartKey, nil); err != nil {
|
|
||||||
b.logf("WriteState error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadStateLocked sets b.prefs and b.stateKey based on a complex
|
|
||||||
// combination of key, prefs, and legacyPath. b.mu must be held when
|
|
||||||
// calling.
|
|
||||||
func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err error) {
|
|
||||||
if prefs == nil && key == "" {
|
|
||||||
panic("state key and prefs are both unset")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimistically set stateKey (for initMachineKeyLocked's
|
|
||||||
// logging), but revert it if we return an error so a later SetPrefs
|
|
||||||
// call can't pick it up if it's bogus.
|
|
||||||
b.stateKey = key
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
b.stateKey = ""
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if key == "" {
|
|
||||||
// Frontend owns the state, we just need to obey it.
|
|
||||||
//
|
|
||||||
// If the frontend (e.g. on Windows) supplied the
|
|
||||||
// optional/legacy machine key then it's used as the
|
|
||||||
// value instead of making up a new one.
|
|
||||||
b.logf("using frontend prefs: %s", prefs.Pretty())
|
|
||||||
b.prefs = prefs.Clone().View()
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
|
||||||
b.writeServerModeStartState(b.userID, b.prefs)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefs != nil {
|
if prefs != nil {
|
||||||
// Backend owns the state, but frontend is trying to migrate
|
// Backend owns the state, but frontend is trying to migrate
|
||||||
// state into the backend.
|
// state into the backend.
|
||||||
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
|
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
|
||||||
if err := b.store.WriteState(key, prefs.ToBytes()); err != nil {
|
if err := b.pm.SetPrefs(prefs.View()); err != nil {
|
||||||
return fmt.Errorf("store.WriteState: %v", err)
|
return fmt.Errorf("store.WriteState: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := b.store.ReadState(key)
|
b.setAtomicValuesFromPrefs(b.pm.CurrentPrefs())
|
||||||
switch {
|
|
||||||
case errors.Is(err, ipn.ErrStateNotExist):
|
|
||||||
prefs := ipn.NewPrefs()
|
|
||||||
prefs.WantRunning = false
|
|
||||||
b.logf("using backend prefs; created empty state for %q: %s", key, prefs.Pretty())
|
|
||||||
b.prefs = prefs.View()
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
|
||||||
return nil
|
|
||||||
case err != nil:
|
|
||||||
return fmt.Errorf("backend prefs: store.ReadState(%q): %v", key, err)
|
|
||||||
}
|
|
||||||
prefs, err = ipn.PrefsFromBytes(bs)
|
|
||||||
if err != nil {
|
|
||||||
b.logf("using backend prefs for %q", key)
|
|
||||||
return fmt.Errorf("PrefsFromBytes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore any old stored preferences for https://login.tailscale.com
|
|
||||||
// as the control server that would override the new default of
|
|
||||||
// controlplane.tailscale.com.
|
|
||||||
// This makes sure that mobile clients go through the new
|
|
||||||
// frontends where we're (2021-10-02) doing battery
|
|
||||||
// optimization work ahead of turning down the old backends.
|
|
||||||
if prefs != nil && prefs.ControlURL != "" &&
|
|
||||||
prefs.ControlURL != ipn.DefaultControlURL &&
|
|
||||||
ipn.IsLoginServerSynonym(prefs.ControlURL) {
|
|
||||||
prefs.ControlURL = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
b.logf("using backend prefs for %q: %s", key, prefs.Pretty())
|
|
||||||
b.prefs = prefs.View()
|
|
||||||
|
|
||||||
b.setAtomicValuesFromPrefs(b.prefs)
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2010,8 +1915,8 @@ func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) {
|
||||||
b.shouldInterceptTCPPortAtomic.Store(f)
|
b.shouldInterceptTCPPortAtomic.Store(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
|
// setAtomicValuesFromPrefs populates sshAtomicBool, containsViaIPFuncAtomic
|
||||||
// from the prefs p, which may be nil.
|
// and shouldInterceptTCPPortAtomic from the prefs p, which may be !Valid().
|
||||||
func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) {
|
func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) {
|
||||||
b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD())
|
b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD())
|
||||||
|
|
||||||
|
@ -2020,6 +1925,7 @@ func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) {
|
||||||
b.setTCPPortsIntercepted(nil)
|
b.setTCPPortsIntercepted(nil)
|
||||||
} else {
|
} else {
|
||||||
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix)))
|
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix)))
|
||||||
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2175,15 +2081,16 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
if !b.prefs.Valid() || b.netMap == nil {
|
p := b.pm.CurrentPrefs()
|
||||||
|
if !p.Valid() || b.netMap == nil {
|
||||||
return false // default to safest setting
|
return false // default to safest setting
|
||||||
}
|
}
|
||||||
return !b.prefs.ShieldsUp() && b.netMap.CollectServices
|
return !p.ShieldsUp() && b.netMap.CollectServices
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) SetCurrentUserID(uid string) {
|
func (b *LocalBackend) SetCurrentUserID(uid string) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
b.userID = uid
|
b.pm.SetCurrentUser(uid)
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2248,7 +2155,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
|
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
|
||||||
if !b.prefs.Valid() || !b.prefs.RunSSH() {
|
if p := b.pm.CurrentPrefs(); !p.Valid() || !p.RunSSH() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||||
|
@ -2274,10 +2181,11 @@ func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) isDefaultServerLocked() bool {
|
func (b *LocalBackend) isDefaultServerLocked() bool {
|
||||||
if !b.prefs.Valid() {
|
prefs := b.pm.CurrentPrefs()
|
||||||
|
if !prefs.Valid() {
|
||||||
return true // assume true until set otherwise
|
return true // assume true until set otherwise
|
||||||
}
|
}
|
||||||
return b.prefs.ControlURLOrDefault() == ipn.DefaultControlURL
|
return prefs.ControlURLOrDefault() == ipn.DefaultControlURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||||
|
@ -2287,8 +2195,8 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||||
b.egg = true
|
b.egg = true
|
||||||
go b.doSetHostinfoFilterServices(b.hostinfo.Clone())
|
go b.doSetHostinfoFilterServices(b.hostinfo.Clone())
|
||||||
}
|
}
|
||||||
p0 := b.prefs
|
p0 := b.pm.CurrentPrefs()
|
||||||
p1 := b.prefs.AsStruct()
|
p1 := b.pm.CurrentPrefs().AsStruct()
|
||||||
p1.ApplyEdits(mp)
|
p1.ApplyEdits(mp)
|
||||||
if err := b.checkPrefsLocked(p1); err != nil {
|
if err := b.checkPrefsLocked(p1); err != nil {
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
@ -2330,66 +2238,61 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||||
// It returns a readonly copy of the new prefs.
|
// It returns a readonly copy of the new prefs.
|
||||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn.PrefsView {
|
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn.PrefsView {
|
||||||
netMap := b.netMap
|
netMap := b.netMap
|
||||||
stateKey := b.stateKey
|
b.setAtomicValuesFromPrefs(newp.View())
|
||||||
oldp := b.prefs
|
|
||||||
newp.Persist = oldp.Persist() // caller isn't allowed to override this
|
|
||||||
|
|
||||||
|
oldp := b.pm.CurrentPrefs()
|
||||||
|
if oldp.Valid() {
|
||||||
|
newp.Persist = oldp.Persist().Clone() // caller isn't allowed to override this
|
||||||
|
}
|
||||||
// findExitNodeIDLocked returns whether it updated b.prefs, but
|
// findExitNodeIDLocked returns whether it updated b.prefs, but
|
||||||
// everything in this function treats b.prefs as completely new
|
// everything in this function treats b.prefs as completely new
|
||||||
// anyway. No-op if no exit node resolution is needed.
|
// anyway. No-op if no exit node resolution is needed.
|
||||||
findExitNodeIDLocked(newp, netMap)
|
findExitNodeIDLocked(newp, netMap)
|
||||||
b.prefs = newp.View()
|
|
||||||
b.setAtomicValuesFromPrefs(b.prefs)
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
|
||||||
b.inServerMode = b.prefs.ForceDaemon()
|
|
||||||
// We do this to avoid holding the lock while doing everything else.
|
// We do this to avoid holding the lock while doing everything else.
|
||||||
|
b.inServerMode = newp.ForceDaemon
|
||||||
|
|
||||||
oldHi := b.hostinfo
|
oldHi := b.hostinfo
|
||||||
newHi := oldHi.Clone()
|
newHi := oldHi.Clone()
|
||||||
b.applyPrefsToHostinfo(newHi, b.prefs)
|
b.applyPrefsToHostinfo(newHi, newp.View())
|
||||||
b.hostinfo = newHi
|
b.hostinfo = newHi
|
||||||
hostInfoChanged := !oldHi.Equal(newHi)
|
hostInfoChanged := !oldHi.Equal(newHi)
|
||||||
userID := b.userID
|
|
||||||
cc := b.cc
|
cc := b.cc
|
||||||
|
|
||||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||||
if caller == "SetPrefs" {
|
if caller == "SetPrefs" {
|
||||||
b.logf("SetPrefs: %v", b.prefs.Pretty())
|
b.logf("SetPrefs: %v", newp.Pretty())
|
||||||
}
|
}
|
||||||
b.updateFilterLocked(netMap, b.prefs)
|
b.updateFilterLocked(netMap, newp.View())
|
||||||
|
|
||||||
if oldp.ShouldSSHBeRunning() && !b.prefs.ShouldSSHBeRunning() {
|
if oldp.ShouldSSHBeRunning() && !newp.ShouldSSHBeRunning() {
|
||||||
if b.sshServer != nil {
|
if b.sshServer != nil {
|
||||||
go b.sshServer.Shutdown()
|
go b.sshServer.Shutdown()
|
||||||
b.sshServer = nil
|
b.sshServer = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs := b.prefs // We can grab the view before unlocking. It can't be mutated.
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
if stateKey != "" {
|
|
||||||
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
|
|
||||||
b.logf("failed to save new controlclient state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.writeServerModeStartState(userID, prefs)
|
|
||||||
|
|
||||||
if netMap != nil {
|
if netMap != nil {
|
||||||
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
|
up := netMap.UserProfiles[netMap.User]
|
||||||
if prefs.Persist() == nil {
|
if login := up.LoginName; login != "" {
|
||||||
|
if newp.Persist == nil {
|
||||||
b.logf("active login: %s", login)
|
b.logf("active login: %s", login)
|
||||||
} else if prefs.Persist().LoginName != login {
|
} else {
|
||||||
// Corp issue 461: sometimes the wrong prefs are
|
if newp.Persist.LoginName != login {
|
||||||
// logged; the frontend isn't always getting
|
b.logf("active login: %q (changed from %q)", login, newp.Persist.LoginName)
|
||||||
// notified (to update its prefs/persist) on
|
newp.Persist.LoginName = login
|
||||||
// account switch. Log this while we figure it
|
}
|
||||||
// out.
|
newp.Persist.UserProfile = up
|
||||||
b.logf("active login: %q ([unexpected] corp#461, not %q)", prefs.Persist().LoginName, login)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldp.ShieldsUp() != prefs.ShieldsUp() || hostInfoChanged {
|
prefs := newp.View()
|
||||||
|
if err := b.pm.SetPrefs(prefs); err != nil {
|
||||||
|
b.logf("failed to save new controlclient state: %v", err)
|
||||||
|
}
|
||||||
|
b.lastProfileID = b.pm.CurrentProfile().ID
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
|
||||||
b.doSetHostinfoFilterServices(newHi)
|
b.doSetHostinfoFilterServices(newHi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2397,12 +2300,12 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
|
||||||
b.e.SetDERPMap(netMap.DERPMap)
|
b.e.SetDERPMap(netMap.DERPMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !oldp.WantRunning() && prefs.WantRunning() {
|
if !oldp.WantRunning() && newp.WantRunning {
|
||||||
b.logf("transitioning to running; doing Login...")
|
b.logf("transitioning to running; doing Login...")
|
||||||
cc.Login(nil, controlclient.LoginDefault)
|
cc.Login(nil, controlclient.LoginDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldp.WantRunning() != prefs.WantRunning() {
|
if oldp.WantRunning() != newp.WantRunning {
|
||||||
b.stateMachine()
|
b.stateMachine()
|
||||||
} else {
|
} else {
|
||||||
b.authReconfig()
|
b.authReconfig()
|
||||||
|
@ -2536,7 +2439,7 @@ func (b *LocalBackend) blockEngineUpdates(block bool) {
|
||||||
func (b *LocalBackend) authReconfig() {
|
func (b *LocalBackend) authReconfig() {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
blocked := b.blocked
|
blocked := b.blocked
|
||||||
prefs := b.prefs
|
prefs := b.pm.CurrentPrefs()
|
||||||
nm := b.netMap
|
nm := b.netMap
|
||||||
hasPAC := b.prevIfState.HasPAC()
|
hasPAC := b.prevIfState.HasPAC()
|
||||||
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
|
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
|
||||||
|
@ -3139,9 +3042,15 @@ func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs ipn.Pref
|
||||||
// happen".
|
// happen".
|
||||||
func (b *LocalBackend) enterState(newState ipn.State) {
|
func (b *LocalBackend) enterState(newState ipn.State) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
|
b.enterStateLockedOnEntry(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterStateLockedOnEntry is like enterState but requires b.mu be held to call
|
||||||
|
// it, but it unlocks b.mu when done.
|
||||||
|
func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
|
||||||
oldState := b.state
|
oldState := b.state
|
||||||
b.state = newState
|
b.state = newState
|
||||||
prefs := b.prefs
|
prefs := b.pm.CurrentPrefs()
|
||||||
netMap := b.netMap
|
netMap := b.netMap
|
||||||
activeLogin := b.activeLogin
|
activeLogin := b.activeLogin
|
||||||
authURL := b.authURL
|
authURL := b.authURL
|
||||||
|
@ -3157,12 +3066,12 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||||
|
|
||||||
// prefs may change irrespective of state; WantRunning should be explicitly
|
// prefs may change irrespective of state; WantRunning should be explicitly
|
||||||
// set before potential early return even if the state is unchanged.
|
// set before potential early return even if the state is unchanged.
|
||||||
health.SetIPNState(newState.String(), prefs.WantRunning())
|
health.SetIPNState(newState.String(), prefs.Valid() && prefs.WantRunning())
|
||||||
if oldState == newState {
|
if oldState == newState {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)",
|
b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)",
|
||||||
oldState, newState, prefs.WantRunning, netMap != nil)
|
oldState, newState, prefs.WantRunning(), netMap != nil)
|
||||||
b.send(ipn.Notify{State: &newState})
|
b.send(ipn.Notify{State: &newState})
|
||||||
|
|
||||||
switch newState {
|
switch newState {
|
||||||
|
@ -3189,6 +3098,8 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||||
addrs = append(addrs, addr.Addr().String())
|
addrs = append(addrs, addr.Addr().String())
|
||||||
}
|
}
|
||||||
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
|
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
|
||||||
|
case ipn.NoState:
|
||||||
|
// Do nothing.
|
||||||
default:
|
default:
|
||||||
b.logf("[unexpected] unknown newState %#v", newState)
|
b.logf("[unexpected] unknown newState %#v", newState)
|
||||||
}
|
}
|
||||||
|
@ -3199,8 +3110,8 @@ func (b *LocalBackend) hasNodeKey() bool {
|
||||||
// we can't use b.Prefs(), because it strips the keys, oops!
|
// we can't use b.Prefs(), because it strips the keys, oops!
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
p := b.pm.CurrentPrefs()
|
||||||
return b.prefs.Valid() && b.prefs.Persist() != nil && !b.prefs.Persist().PrivateNodeKey.IsZero()
|
return p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
// nextState returns the state the backend seems to be in, based on
|
// nextState returns the state the backend seems to be in, based on
|
||||||
|
@ -3209,15 +3120,20 @@ func (b *LocalBackend) nextState() ipn.State {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
b.assertClientLocked()
|
b.assertClientLocked()
|
||||||
var (
|
var (
|
||||||
cc = b.cc
|
cc = b.cc
|
||||||
netMap = b.netMap
|
netMap = b.netMap
|
||||||
state = b.state
|
state = b.state
|
||||||
blocked = b.blocked
|
blocked = b.blocked
|
||||||
wantRunning = b.prefs.WantRunning()
|
st = b.engineStatus
|
||||||
loggedOut = b.prefs.LoggedOut()
|
keyExpired = b.keyExpired
|
||||||
st = b.engineStatus
|
|
||||||
keyExpired = b.keyExpired
|
wantRunning = false
|
||||||
|
loggedOut = false
|
||||||
)
|
)
|
||||||
|
if p := b.pm.CurrentPrefs(); p.Valid() {
|
||||||
|
wantRunning = p.WantRunning()
|
||||||
|
loggedOut = p.LoggedOut()
|
||||||
|
}
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -3328,15 +3244,13 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||||
go b.cc.Shutdown()
|
go b.cc.Shutdown()
|
||||||
b.cc = nil
|
b.cc = nil
|
||||||
}
|
}
|
||||||
b.stateKey = ""
|
|
||||||
b.userID = ""
|
|
||||||
b.setNetMapLocked(nil)
|
b.setNetMapLocked(nil)
|
||||||
b.prefs = new(ipn.Prefs).View()
|
b.pm.Reset()
|
||||||
b.keyExpired = false
|
b.keyExpired = false
|
||||||
b.authURL = ""
|
b.authURL = ""
|
||||||
b.authURLSticky = ""
|
b.authURLSticky = ""
|
||||||
b.activeLogin = ""
|
b.activeLogin = ""
|
||||||
b.setAtomicValuesFromPrefs(b.prefs)
|
b.setAtomicValuesFromPrefs(ipn.PrefsView{})
|
||||||
b.setTCPPortsIntercepted(nil)
|
b.setTCPPortsIntercepted(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3434,10 +3348,19 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||||
b.dialer.SetNetMap(nm)
|
b.dialer.SetNetMap(nm)
|
||||||
var login string
|
var login string
|
||||||
if nm != nil {
|
if nm != nil {
|
||||||
login = nm.UserProfiles[nm.User].LoginName
|
up := nm.UserProfiles[nm.User]
|
||||||
|
login = up.LoginName
|
||||||
if login == "" {
|
if login == "" {
|
||||||
login = "<missing-profile>"
|
login = "<missing-profile>"
|
||||||
}
|
}
|
||||||
|
if cp := b.pm.CurrentProfile(); cp.ID != "" && cp.UserProfile.ID == 0 {
|
||||||
|
// Migration to profiles: we didn't use to persist
|
||||||
|
// the UserProfile, so if we don't have one, fill it
|
||||||
|
// in from the NetworkMap.
|
||||||
|
prefs := b.pm.CurrentPrefs().AsStruct()
|
||||||
|
prefs.Persist.UserProfile = up
|
||||||
|
b.pm.SetPrefs(prefs.View())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.netMap = nm
|
b.netMap = nm
|
||||||
if login != b.activeLogin {
|
if login != b.activeLogin {
|
||||||
|
@ -3459,7 +3382,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||||
}
|
}
|
||||||
b.capFileSharing = fs
|
b.capFileSharing = fs
|
||||||
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||||
if nm == nil {
|
if nm == nil {
|
||||||
b.nodeByAddr = nil
|
b.nodeByAddr = nil
|
||||||
return
|
return
|
||||||
|
@ -3498,17 +3421,16 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||||
// the ports that tailscaled should handle as a function of b.netMap and b.prefs.
|
// the ports that tailscaled should handle as a function of b.netMap and b.prefs.
|
||||||
//
|
//
|
||||||
// b.mu must be held.
|
// b.mu must be held.
|
||||||
func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked() {
|
func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.PrefsView) {
|
||||||
handlePorts := make([]uint16, 0, 4)
|
handlePorts := make([]uint16, 0, 4)
|
||||||
|
|
||||||
prefs := b.prefs
|
|
||||||
if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() {
|
if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() {
|
||||||
handlePorts = append(handlePorts, 22)
|
handlePorts = append(handlePorts, 22)
|
||||||
}
|
}
|
||||||
|
|
||||||
nm := b.netMap
|
nm := b.netMap
|
||||||
if nm != nil && nm.SelfNode != nil {
|
if nm != nil && nm.SelfNode != nil {
|
||||||
profileID := fmt.Sprintf("node-%s", nm.SelfNode.StableID) // TODO(maisem,bradfitz): something else?
|
profileID := b.pm.CurrentProfile().ID
|
||||||
confKey := ipn.ServeConfigKey(profileID)
|
confKey := ipn.ServeConfigKey(profileID)
|
||||||
if confj, err := b.store.ReadState(confKey); err == nil {
|
if confj, err := b.store.ReadState(confKey); err == nil {
|
||||||
if !b.lastServeConfJSON.Equal(mem.B(confj)) {
|
if !b.lastServeConfJSON.Equal(mem.B(confj)) {
|
||||||
|
@ -3546,7 +3468,7 @@ func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error {
|
||||||
if nm.SelfNode == nil {
|
if nm.SelfNode == nil {
|
||||||
return errors.New("netMap SelfNode is nil")
|
return errors.New("netMap SelfNode is nil")
|
||||||
}
|
}
|
||||||
profileID := fmt.Sprintf("node-%s", nm.SelfNode.StableID) // TODO(maisem,bradfitz): something else?
|
profileID := b.pm.CurrentProfile().ID
|
||||||
confKey := ipn.ServeConfigKey(profileID)
|
confKey := ipn.ServeConfigKey(profileID)
|
||||||
|
|
||||||
var bs []byte
|
var bs []byte
|
||||||
|
@ -3561,7 +3483,7 @@ func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error {
|
||||||
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
|
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -3579,10 +3501,11 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
|
||||||
func (b *LocalBackend) operatorUserName() string {
|
func (b *LocalBackend) operatorUserName() string {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
if !b.prefs.Valid() {
|
prefs := b.pm.CurrentPrefs()
|
||||||
|
if !prefs.Valid() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return b.prefs.OperatorUser()
|
return prefs.OperatorUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
||||||
|
@ -3605,8 +3528,8 @@ func (b *LocalBackend) OperatorUserID() string {
|
||||||
// in the test harness.
|
// in the test harness.
|
||||||
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeKey key.NodePublic) {
|
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeKey key.NodePublic) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
prefs := b.prefs
|
|
||||||
machinePrivKey := b.machinePrivKey
|
machinePrivKey := b.machinePrivKey
|
||||||
|
prefs := b.pm.CurrentPrefs()
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
if !prefs.Valid() || machinePrivKey.IsZero() {
|
if !prefs.Valid() || machinePrivKey.IsZero() {
|
||||||
|
@ -3723,8 +3646,8 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
cc := b.ccAuto
|
cc := b.ccAuto
|
||||||
if b.prefs.Valid() {
|
if prefs := b.pm.CurrentPrefs(); prefs.Valid() {
|
||||||
req.NodeKey = b.prefs.Persist().PublicNodeKey()
|
req.NodeKey = prefs.Persist().PrivateNodeKey.Public()
|
||||||
}
|
}
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
if cc == nil {
|
if cc == nil {
|
||||||
|
@ -3836,11 +3759,11 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
|
||||||
func (b *LocalBackend) OfferingExitNode() bool {
|
func (b *LocalBackend) OfferingExitNode() bool {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
if !b.prefs.Valid() {
|
if !b.pm.CurrentPrefs().Valid() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var def4, def6 bool
|
var def4, def6 bool
|
||||||
ar := b.prefs.AdvertiseRoutes()
|
ar := b.pm.CurrentPrefs().AdvertiseRoutes()
|
||||||
for i := 0; i < ar.Len(); i++ {
|
for i := 0; i < ar.Len(); i++ {
|
||||||
r := ar.At(i)
|
r := ar.At(i)
|
||||||
if r.Bits() != 0 {
|
if r.Bits() != 0 {
|
||||||
|
@ -4027,7 +3950,8 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||||
func (b *LocalBackend) tailscaleSSHEnabled() bool {
|
func (b *LocalBackend) tailscaleSSHEnabled() bool {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
return b.prefs.Valid() && b.prefs.RunSSH()
|
p := b.pm.CurrentPrefs()
|
||||||
|
return p.Valid() && p.RunSSH()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
||||||
|
@ -4121,7 +4045,7 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error {
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -4132,3 +4056,75 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error {
|
||||||
func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool {
|
func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool {
|
||||||
return b.shouldInterceptTCPPortAtomic.Load()(port)
|
return b.shouldInterceptTCPPortAtomic.Load()(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwitchProfile switches to the profile with the given id.
|
||||||
|
// It will restart the backend on success.
|
||||||
|
// If the profile is not known, it returns an errProfileNotFound.
|
||||||
|
func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
|
||||||
|
if b.CurrentProfile().ID == profile {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
if err := b.pm.SwitchProfile(profile); err != nil {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.resetForProfileChangeLockedOnEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetForProfileChangeLockedOnEntry resets the backend for a profile change.
|
||||||
|
func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error {
|
||||||
|
b.setNetMapLocked(nil) // Reset netmap.
|
||||||
|
// Reset the NetworkMap in the engine
|
||||||
|
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
||||||
|
b.enterStateLockedOnEntry(ipn.NoState) // Reset state.
|
||||||
|
return b.Start(ipn.Options{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProfile deletes a profile with the given ID.
|
||||||
|
// If the profile is not known, it is a no-op.
|
||||||
|
func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
needToRestart := b.pm.CurrentProfile().ID == p
|
||||||
|
if err := b.pm.DeleteProfile(p); err != nil {
|
||||||
|
if err == errProfileNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !needToRestart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.resetForProfileChangeLockedOnEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentProfile returns the current LoginProfile.
|
||||||
|
// The value may be zero if the profile is not persisted.
|
||||||
|
func (b *LocalBackend) CurrentProfile() ipn.LoginProfile {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.pm.CurrentProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProfile creates and switches to the new profile.
|
||||||
|
func (b *LocalBackend) NewProfile() error {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.pm.NewProfile()
|
||||||
|
return b.resetForProfileChangeLockedOnEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProfiles returns a list of all LoginProfiles.
|
||||||
|
func (b *LocalBackend) ListProfiles() []ipn.LoginProfile {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.pm.Profiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentUser returns the current server mode user ID. It is only non-empty on
|
||||||
|
// Windows where we have a multi-user system.
|
||||||
|
func (b *LocalBackend) CurrentUser() string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.pm.CurrentUser()
|
||||||
|
}
|
||||||
|
|
|
@ -489,7 +489,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, 0)
|
lb, err := NewLocalBackend(logf, "logid", store, "default", nil, eng, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewLocalBackend: %v", err)
|
t.Fatalf("NewLocalBackend: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -498,9 +498,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||||
Transport: panicOnUseTransport{}, // validate we don't send HTTP requests
|
Transport: panicOnUseTransport{}, // validate we don't send HTTP requests
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := lb.Start(ipn.Options{
|
if err := lb.Start(ipn.Options{}); err != nil {
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Start: %v", err)
|
t.Fatalf("Start: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,14 +55,12 @@ func TestLocalLogLines(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Cleanup(e.Close)
|
t.Cleanup(e.Close)
|
||||||
|
|
||||||
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e, 0)
|
lb, err := NewLocalBackend(logf, idA.String(), store, "", nil, e, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer lb.Shutdown()
|
defer lb.Shutdown()
|
||||||
|
|
||||||
// custom adjustments for required non-nil fields
|
|
||||||
lb.prefs = ipn.NewPrefs().View()
|
|
||||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||||
lb.keyLogf = logListen.Logf
|
lb.keyLogf = logListen.Logf
|
||||||
|
|
|
@ -109,7 +109,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error {
|
||||||
b.mu.Lock() // take mu to protect access to synchronized fields.
|
b.mu.Lock() // take mu to protect access to synchronized fields.
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
ourNodeKey := b.prefs.Persist().PublicNodeKey()
|
ourNodeKey := b.pm.CurrentPrefs().Persist().PublicNodeKey()
|
||||||
|
|
||||||
isEnabled := b.tka != nil
|
isEnabled := b.tka != nil
|
||||||
wantEnabled := nm.TKAEnabled
|
wantEnabled := nm.TKAEnabled
|
||||||
|
@ -362,8 +362,8 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
|
||||||
|
|
||||||
var ourNodeKey key.NodePublic
|
var ourNodeKey key.NodePublic
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
if b.prefs.Valid() {
|
if p := b.pm.CurrentPrefs(); p.Valid() {
|
||||||
ourNodeKey = b.prefs.Persist().PublicNodeKey()
|
ourNodeKey = p.Persist().PublicNodeKey()
|
||||||
}
|
}
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
if ourNodeKey.IsZero() {
|
if ourNodeKey.IsZero() {
|
||||||
|
@ -465,7 +465,8 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err)
|
return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err)
|
||||||
}
|
}
|
||||||
return b.prefs.Persist().PublicNodeKey(), sig, nil
|
|
||||||
|
return b.pm.CurrentPrefs().Persist().PublicNodeKey(), sig, nil
|
||||||
}(nodeKey, rotationPublic)
|
}(nodeKey, rotationPublic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -518,7 +519,7 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ourNodeKey := b.prefs.Persist().PublicNodeKey()
|
ourNodeKey := b.pm.CurrentPrefs().Persist().PublicNodeKey()
|
||||||
head := b.tka.authority.Head()
|
head := b.tka.authority.Head()
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
resp, err := b.tkaDoSyncSend(ourNodeKey, head, aums, true)
|
resp, err := b.tkaDoSyncSend(ourNodeKey, head, aums, true)
|
||||||
|
@ -553,8 +554,8 @@ func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
|
||||||
)
|
)
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
if b.prefs.Valid() {
|
if p := b.pm.CurrentPrefs(); p.Valid() {
|
||||||
ourNodeKey = b.prefs.Persist().PublicNodeKey()
|
ourNodeKey = p.Persist().PublicNodeKey()
|
||||||
}
|
}
|
||||||
if b.tka == nil {
|
if b.tka == nil {
|
||||||
err = errNetworkLockNotActive
|
err = errNetworkLockNotActive
|
||||||
|
|
|
@ -20,12 +20,14 @@ import (
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tka"
|
"tailscale.com/tka"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
"tailscale.com/types/persist"
|
"tailscale.com/types/persist"
|
||||||
"tailscale.com/types/tkatype"
|
"tailscale.com/types/tkatype"
|
||||||
|
"tailscale.com/util/must"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
||||||
|
@ -117,14 +119,17 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||||
temp := t.TempDir()
|
temp := t.TempDir()
|
||||||
|
|
||||||
cc := fakeControlClient(t, client)
|
cc := fakeControlClient(t, client)
|
||||||
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
|
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||||
|
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||||
|
}).View()))
|
||||||
b := LocalBackend{
|
b := LocalBackend{
|
||||||
varRoot: temp,
|
varRoot: temp,
|
||||||
cc: cc,
|
cc: cc,
|
||||||
ccAuto: cc,
|
ccAuto: cc,
|
||||||
logf: t.Logf,
|
logf: t.Logf,
|
||||||
prefs: (&ipn.Prefs{
|
pm: pm,
|
||||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
store: pm.Store(),
|
||||||
}).View(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||||
|
@ -210,6 +215,10 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
cc := fakeControlClient(t, client)
|
cc := fakeControlClient(t, client)
|
||||||
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
|
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||||
|
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||||
|
}).View()))
|
||||||
b := LocalBackend{
|
b := LocalBackend{
|
||||||
varRoot: temp,
|
varRoot: temp,
|
||||||
cc: cc,
|
cc: cc,
|
||||||
|
@ -219,9 +228,8 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||||
authority: authority,
|
authority: authority,
|
||||||
storage: chonk,
|
storage: chonk,
|
||||||
},
|
},
|
||||||
prefs: (&ipn.Prefs{
|
pm: pm,
|
||||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
store: pm.Store(),
|
||||||
}).View(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that the wrong disablement secret does not shut down the authority.
|
// Test that the wrong disablement secret does not shut down the authority.
|
||||||
|
@ -456,18 +464,21 @@ func TestTKASync(t *testing.T) {
|
||||||
|
|
||||||
// Setup the client.
|
// Setup the client.
|
||||||
cc := fakeControlClient(t, client)
|
cc := fakeControlClient(t, client)
|
||||||
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
|
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||||
|
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||||
|
}).View()))
|
||||||
b := LocalBackend{
|
b := LocalBackend{
|
||||||
varRoot: temp,
|
varRoot: temp,
|
||||||
cc: cc,
|
cc: cc,
|
||||||
ccAuto: cc,
|
ccAuto: cc,
|
||||||
logf: t.Logf,
|
logf: t.Logf,
|
||||||
|
pm: pm,
|
||||||
|
store: pm.Store(),
|
||||||
tka: &tkaState{
|
tka: &tkaState{
|
||||||
authority: nodeAuthority,
|
authority: nodeAuthority,
|
||||||
storage: nodeStorage,
|
storage: nodeStorage,
|
||||||
},
|
},
|
||||||
prefs: (&ipn.Prefs{
|
|
||||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
|
||||||
}).View(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, lets trigger a sync.
|
// Finally, lets trigger a sync.
|
||||||
|
@ -607,6 +618,11 @@ func TestTKADisable(t *testing.T) {
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
cc := fakeControlClient(t, client)
|
cc := fakeControlClient(t, client)
|
||||||
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
|
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||||
|
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||||
|
}).View()))
|
||||||
|
|
||||||
b := LocalBackend{
|
b := LocalBackend{
|
||||||
varRoot: temp,
|
varRoot: temp,
|
||||||
cc: cc,
|
cc: cc,
|
||||||
|
@ -616,9 +632,8 @@ func TestTKADisable(t *testing.T) {
|
||||||
authority: authority,
|
authority: authority,
|
||||||
storage: chonk,
|
storage: chonk,
|
||||||
},
|
},
|
||||||
prefs: (&ipn.Prefs{
|
pm: pm,
|
||||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
store: pm.Store(),
|
||||||
}).View(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that we get an error for an incorrect disablement secret.
|
// Test that we get an error for an incorrect disablement secret.
|
||||||
|
@ -688,7 +703,10 @@ func TestTKASign(t *testing.T) {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
|
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||||
|
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||||
|
}).View()))
|
||||||
cc := fakeControlClient(t, client)
|
cc := fakeControlClient(t, client)
|
||||||
b := LocalBackend{
|
b := LocalBackend{
|
||||||
varRoot: temp,
|
varRoot: temp,
|
||||||
|
@ -699,9 +717,8 @@ func TestTKASign(t *testing.T) {
|
||||||
authority: authority,
|
authority: authority,
|
||||||
storage: chonk,
|
storage: chonk,
|
||||||
},
|
},
|
||||||
prefs: (&ipn.Prefs{
|
pm: pm,
|
||||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
store: pm.Store(),
|
||||||
}).View(),
|
|
||||||
nlPrivKey: nlPriv,
|
nlPrivKey: nlPriv,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,11 @@ import (
|
||||||
|
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/must"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
"tailscale.com/wgengine/filter"
|
"tailscale.com/wgengine/filter"
|
||||||
)
|
)
|
||||||
|
@ -585,20 +587,23 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||||
|
|
||||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||||
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
h.ps = &peerAPIServer{
|
h.ps = &peerAPIServer{
|
||||||
b: &LocalBackend{
|
b: &LocalBackend{
|
||||||
e: eng,
|
e: eng,
|
||||||
|
pm: pm,
|
||||||
|
store: pm.Store(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if h.ps.b.OfferingExitNode() {
|
if h.ps.b.OfferingExitNode() {
|
||||||
t.Fatal("unexpectedly offering exit node")
|
t.Fatal("unexpectedly offering exit node")
|
||||||
}
|
}
|
||||||
h.ps.b.prefs = (&ipn.Prefs{
|
h.ps.b.pm.SetPrefs((&ipn.Prefs{
|
||||||
AdvertiseRoutes: []netip.Prefix{
|
AdvertiseRoutes: []netip.Prefix{
|
||||||
netip.MustParsePrefix("0.0.0.0/0"),
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
netip.MustParsePrefix("::/0"),
|
netip.MustParsePrefix("::/0"),
|
||||||
},
|
},
|
||||||
}).View()
|
}).View())
|
||||||
if !h.ps.b.OfferingExitNode() {
|
if !h.ps.b.OfferingExitNode() {
|
||||||
t.Fatal("unexpectedly not offering exit node")
|
t.Fatal("unexpectedly not offering exit node")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,439 @@
|
||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/strs"
|
||||||
|
"tailscale.com/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// profileManager is a wrapper around a StateStore that manages
|
||||||
|
// multiple profiles and the current profile.
|
||||||
|
type profileManager struct {
|
||||||
|
store ipn.StateStore
|
||||||
|
logf logger.Logf
|
||||||
|
|
||||||
|
currentUserID string // only used on Windows
|
||||||
|
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
|
||||||
|
currentProfile *ipn.LoginProfile
|
||||||
|
prefs ipn.PrefsView
|
||||||
|
|
||||||
|
// isNewProfile is a sentinel value that indicates that the
|
||||||
|
// current profile is new and has not been saved to disk yet.
|
||||||
|
// It is reset to false after a call to SetPrefs with a filled
|
||||||
|
// in LoginName.
|
||||||
|
isNewProfile bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentUser returns the current user ID. It is only non-empty on
|
||||||
|
// Windows where we have a multi-user system.
|
||||||
|
func (pm *profileManager) CurrentUser() string {
|
||||||
|
return pm.currentUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCurrentUser sets the current user ID. The uid is only non-empty
|
||||||
|
// on Windows where we have a multi-user system.
|
||||||
|
func (pm *profileManager) SetCurrentUser(uid string) error {
|
||||||
|
if pm.currentUserID == uid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cpk := ipn.CurrentProfileKey(uid)
|
||||||
|
if b, err := pm.store.ReadState(cpk); err == nil {
|
||||||
|
pk := ipn.StateKey(string(b))
|
||||||
|
prefs, err := pm.loadSavedPrefs(pk)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pm.currentProfile = pm.findProfileByKey(pk)
|
||||||
|
pm.prefs = prefs
|
||||||
|
pm.isNewProfile = false
|
||||||
|
} else if err == ipn.ErrStateNotExist {
|
||||||
|
pm.NewProfile()
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pm.currentUserID = uid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
|
||||||
|
for _, p := range pm.knownProfiles {
|
||||||
|
if p.Name == name {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
|
||||||
|
for _, p := range pm.knownProfiles {
|
||||||
|
if p.Key == key {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) setUnattendedModeAsConfigured() error {
|
||||||
|
if pm.currentUserID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if pm.prefs.ForceDaemon() {
|
||||||
|
return pm.store.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
|
||||||
|
} else {
|
||||||
|
return pm.store.WriteState(ipn.ServerModeStartKey, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset unloads the current profile, if any.
|
||||||
|
func (pm *profileManager) Reset() {
|
||||||
|
pm.currentUserID = ""
|
||||||
|
pm.NewProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrefs sets the current profile's prefs to the provided value.
|
||||||
|
// It also saves the prefs to the StateStore. It stores a copy of the
|
||||||
|
// provided prefs, which may be accessed via CurrentPrefs.
|
||||||
|
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
|
||||||
|
prefs := prefsIn.AsStruct().View()
|
||||||
|
ps := prefs.Persist()
|
||||||
|
if ps == nil || ps.LoginName == "" {
|
||||||
|
return pm.setPrefsLocked(prefs)
|
||||||
|
}
|
||||||
|
up := ps.UserProfile
|
||||||
|
if up.LoginName == "" {
|
||||||
|
up.LoginName = ps.LoginName
|
||||||
|
}
|
||||||
|
if up.DisplayName == "" {
|
||||||
|
up.DisplayName = up.LoginName
|
||||||
|
}
|
||||||
|
cp := pm.currentProfile
|
||||||
|
if pm.isNewProfile {
|
||||||
|
pm.isNewProfile = false
|
||||||
|
cp.ID, cp.Key = newUnusedID(pm.knownProfiles)
|
||||||
|
cp.Name = ps.LoginName
|
||||||
|
cp.UserProfile = ps.UserProfile
|
||||||
|
cp.LocalUserID = pm.currentUserID
|
||||||
|
} else {
|
||||||
|
cp.UserProfile = ps.UserProfile
|
||||||
|
}
|
||||||
|
pm.knownProfiles[cp.ID] = cp
|
||||||
|
if err := pm.writeKnownProfiles(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := pm.setAsUserSelectedProfileLocked(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := pm.setPrefsLocked(prefs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.ProfileID, ipn.StateKey) {
|
||||||
|
var idb [2]byte
|
||||||
|
for {
|
||||||
|
rand.Read(idb[:])
|
||||||
|
id := ipn.ProfileID(fmt.Sprintf("%x", idb))
|
||||||
|
if _, ok := knownProfiles[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return id, ipn.StateKey("profile-" + id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setPrefsLocked sets the current profile's prefs to the provided value.
|
||||||
|
// It also saves the prefs to the StateStore, if the current profile
|
||||||
|
// is not new.
|
||||||
|
func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error {
|
||||||
|
pm.prefs = clonedPrefs
|
||||||
|
if pm.isNewProfile {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := pm.writePrefsToStore(pm.currentProfile.Key, pm.prefs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pm.setUnattendedModeAsConfigured()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsView) error {
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := pm.store.WriteState(key, prefs.ToBytes()); err != nil {
|
||||||
|
pm.logf("WriteState(%q): %v", key, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profiles returns the list of known profiles.
|
||||||
|
func (pm *profileManager) Profiles() []ipn.LoginProfile {
|
||||||
|
var profiles []ipn.LoginProfile
|
||||||
|
for _, p := range pm.knownProfiles {
|
||||||
|
if p.LocalUserID == pm.currentUserID {
|
||||||
|
profiles = append(profiles, *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(profiles, func(a, b ipn.LoginProfile) bool {
|
||||||
|
return a.Name < b.Name
|
||||||
|
})
|
||||||
|
return profiles
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchProfile switches to the profile with the given id.
|
||||||
|
// If the profile is not known, it returns an errProfileNotFound.
|
||||||
|
func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
|
||||||
|
kp, ok := pm.knownProfiles[id]
|
||||||
|
if !ok {
|
||||||
|
return errProfileNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if kp.LocalUserID != pm.currentUserID {
|
||||||
|
return fmt.Errorf("profile %q is not owned by current user", id)
|
||||||
|
}
|
||||||
|
prefs, err := pm.loadSavedPrefs(kp.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pm.prefs = prefs
|
||||||
|
pm.currentProfile = kp
|
||||||
|
pm.isNewProfile = false
|
||||||
|
return pm.setAsUserSelectedProfileLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) setAsUserSelectedProfileLocked() error {
|
||||||
|
k := ipn.CurrentProfileKey(pm.currentUserID)
|
||||||
|
return pm.store.WriteState(k, []byte(pm.currentProfile.Key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
||||||
|
bs, err := pm.store.ReadState(key)
|
||||||
|
if err != nil {
|
||||||
|
if err == ipn.ErrStateNotExist {
|
||||||
|
return emptyPrefs, nil
|
||||||
|
}
|
||||||
|
return ipn.PrefsView{}, err
|
||||||
|
}
|
||||||
|
savedPrefs, err := ipn.PrefsFromBytes(bs)
|
||||||
|
if err != nil {
|
||||||
|
return ipn.PrefsView{}, fmt.Errorf("PrefsFromBytes: %v", err)
|
||||||
|
}
|
||||||
|
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty())
|
||||||
|
|
||||||
|
// Ignore any old stored preferences for https://login.tailscale.com
|
||||||
|
// as the control server that would override the new default of
|
||||||
|
// controlplane.tailscale.com.
|
||||||
|
if savedPrefs.ControlURL != "" &&
|
||||||
|
savedPrefs.ControlURL != ipn.DefaultControlURL &&
|
||||||
|
ipn.IsLoginServerSynonym(savedPrefs.ControlURL) {
|
||||||
|
savedPrefs.ControlURL = ""
|
||||||
|
}
|
||||||
|
return savedPrefs.View(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentProfile returns the name and ID of the current profile, or "" if the profile
|
||||||
|
// is not named.
|
||||||
|
func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
|
||||||
|
return *pm.currentProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
// errProfileNotFound is returned by methods that accept a ProfileID.
|
||||||
|
var errProfileNotFound = errors.New("profile not found")
|
||||||
|
|
||||||
|
// DeleteProfile removes the profile with the given id. It returns
|
||||||
|
// errProfileNotFound if the profile does not exist.
|
||||||
|
// If the profile is the current profile, it is the equivalent of
|
||||||
|
// calling NewProfile() followed by DeleteProfile(id). This is
|
||||||
|
// useful for deleting the last profile. In other cases, it is
|
||||||
|
// recommended to call SwitchProfile() first.
|
||||||
|
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
|
||||||
|
kp, ok := pm.knownProfiles[id]
|
||||||
|
if !ok {
|
||||||
|
return errProfileNotFound
|
||||||
|
}
|
||||||
|
if kp.ID == pm.currentProfile.ID {
|
||||||
|
pm.NewProfile()
|
||||||
|
}
|
||||||
|
if err := pm.store.WriteState(kp.Key, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(pm.knownProfiles, id)
|
||||||
|
return pm.writeKnownProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) writeKnownProfiles() error {
|
||||||
|
b, err := json.Marshal(pm.knownProfiles)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pm.store.WriteState(ipn.KnownProfilesStateKey, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProfile creates and switches to a new unnamed profile. The new profile is
|
||||||
|
// not persisted until SetPrefs is called with a logged-in user.
|
||||||
|
func (pm *profileManager) NewProfile() {
|
||||||
|
pm.prefs = emptyPrefs
|
||||||
|
pm.isNewProfile = true
|
||||||
|
pm.currentProfile = &ipn.LoginProfile{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// emptyPrefs is the default prefs for a new profile.
|
||||||
|
var emptyPrefs = func() ipn.PrefsView {
|
||||||
|
prefs := ipn.NewPrefs()
|
||||||
|
prefs.WantRunning = false
|
||||||
|
return prefs.View()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Store returns the StateStore used by the ProfileManager.
|
||||||
|
func (pm *profileManager) Store() ipn.StateStore {
|
||||||
|
return pm.store
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentPrefs returns a read-only view of the current prefs.
|
||||||
|
func (pm *profileManager) CurrentPrefs() ipn.PrefsView {
|
||||||
|
return pm.prefs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadStartupPrefsForTest reads the startup prefs from disk. It is only used for testing.
|
||||||
|
func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsView, error) {
|
||||||
|
pm, err := newProfileManager(store, logf, "")
|
||||||
|
if err != nil {
|
||||||
|
return ipn.PrefsView{}, err
|
||||||
|
}
|
||||||
|
return pm.CurrentPrefs(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newProfileManager creates a new ProfileManager using the provided StateStore.
|
||||||
|
// It also loads the list of known profiles from the StateStore.
|
||||||
|
// If a state key is provided, it will be used to load the current profile.
|
||||||
|
func newProfileManager(store ipn.StateStore, logf logger.Logf, stateKey ipn.StateKey) (*profileManager, error) {
|
||||||
|
return newProfileManagerWithGOOS(store, logf, stateKey, runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
|
||||||
|
startKey := ipn.CurrentProfileStateKey
|
||||||
|
if goos == "windows" {
|
||||||
|
// When tailscaled runs on Windows it is not typically run unattended.
|
||||||
|
// So we can't use the profile mechanism to load the profile at startup.
|
||||||
|
startKey = ipn.ServerModeStartKey
|
||||||
|
}
|
||||||
|
autoStartKey, err := store.ReadState(startKey)
|
||||||
|
if err != nil && err != ipn.ErrStateNotExist {
|
||||||
|
return "", fmt.Errorf("calling ReadState on state store: %w", err)
|
||||||
|
}
|
||||||
|
return ipn.StateKey(autoStartKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) {
|
||||||
|
var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
|
||||||
|
prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
if err := json.Unmarshal(prfB, &knownProfiles); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
|
||||||
|
}
|
||||||
|
case ipn.ErrStateNotExist:
|
||||||
|
knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("calling ReadState on state store: %w", err)
|
||||||
|
}
|
||||||
|
return knownProfiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, stateKey ipn.StateKey, goos string) (*profileManager, error) {
|
||||||
|
if stateKey == "" {
|
||||||
|
var err error
|
||||||
|
stateKey, err = readAutoStartKey(store, goos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
knownProfiles, err := readKnownProfiles(store)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pm := &profileManager{
|
||||||
|
store: store,
|
||||||
|
knownProfiles: knownProfiles,
|
||||||
|
logf: logf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if stateKey != "" {
|
||||||
|
for _, v := range knownProfiles {
|
||||||
|
if v.Key == stateKey {
|
||||||
|
pm.currentProfile = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pm.currentProfile == nil {
|
||||||
|
if suf, ok := strs.CutPrefix(string(stateKey), "user-"); ok {
|
||||||
|
pm.currentUserID = suf
|
||||||
|
}
|
||||||
|
pm.NewProfile()
|
||||||
|
} else {
|
||||||
|
pm.currentUserID = pm.currentProfile.LocalUserID
|
||||||
|
}
|
||||||
|
prefs, err := pm.loadSavedPrefs(stateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := pm.setPrefsLocked(prefs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if len(knownProfiles) == 0 && goos != "windows" {
|
||||||
|
// No known profiles, try a migration.
|
||||||
|
if err := pm.migrateFromLegacyPrefs(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pm.NewProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *profileManager) migrateFromLegacyPrefs() error {
|
||||||
|
pm.NewProfile()
|
||||||
|
k := ipn.LegacyGlobalDaemonStateKey
|
||||||
|
switch {
|
||||||
|
case runtime.GOOS == "ios":
|
||||||
|
k = "ipn-go-bridge"
|
||||||
|
case version.IsSandboxedMacOS():
|
||||||
|
k = "ipn-go-bridge"
|
||||||
|
case runtime.GOOS == "android":
|
||||||
|
k = "ipn-android"
|
||||||
|
}
|
||||||
|
prefs, err := pm.loadSavedPrefs(k)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("calling ReadState on state store: %w", err)
|
||||||
|
}
|
||||||
|
pm.logf("migrating %q profile to new format", k)
|
||||||
|
if err := pm.SetPrefs(prefs); err != nil {
|
||||||
|
return fmt.Errorf("migrating _daemon profile: %w", err)
|
||||||
|
}
|
||||||
|
// Do not delete the old state key, as we may be downgraded to an
|
||||||
|
// older version that still relies on it.
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/persist"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestProfileManagement tests creating, loading, and switching profiles.
|
||||||
|
func TestProfileManagement(t *testing.T) {
|
||||||
|
store := new(mem.Store)
|
||||||
|
|
||||||
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "", "linux")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantCurProfile := ""
|
||||||
|
wantProfiles := map[string]ipn.PrefsView{
|
||||||
|
"": emptyPrefs,
|
||||||
|
}
|
||||||
|
checkProfiles := func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
prof := pm.CurrentProfile()
|
||||||
|
t.Logf("\tCurrentProfile = %q", prof)
|
||||||
|
if prof.Name != wantCurProfile {
|
||||||
|
t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile)
|
||||||
|
}
|
||||||
|
profiles := pm.Profiles()
|
||||||
|
wantLen := len(wantProfiles)
|
||||||
|
if _, ok := wantProfiles[""]; ok {
|
||||||
|
wantLen--
|
||||||
|
}
|
||||||
|
if len(profiles) != wantLen {
|
||||||
|
t.Fatalf("Profiles = %v; want %v", profiles, wantProfiles)
|
||||||
|
}
|
||||||
|
p := pm.CurrentPrefs()
|
||||||
|
if !p.Valid() {
|
||||||
|
t.Fatalf("CurrentPrefs = %v; want valid", p)
|
||||||
|
}
|
||||||
|
if !p.Equals(wantProfiles[wantCurProfile]) {
|
||||||
|
t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
|
||||||
|
}
|
||||||
|
for _, p := range profiles {
|
||||||
|
got, err := pm.loadSavedPrefs(p.Key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Use Hostname as a proxy for all prefs.
|
||||||
|
if got.Hostname() != wantProfiles[p.Name].Hostname() {
|
||||||
|
t.Fatalf("Prefs for profile %q = %v; want %v", p, got.Pretty(), wantProfiles[p.Name].Pretty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPrefs := func(t *testing.T, loginName string) ipn.PrefsView {
|
||||||
|
p := pm.CurrentPrefs().AsStruct()
|
||||||
|
p.Persist = &persist.Persist{
|
||||||
|
LoginName: loginName,
|
||||||
|
}
|
||||||
|
if err := pm.SetPrefs(p.View()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return p.View()
|
||||||
|
}
|
||||||
|
t.Logf("Check initial state from empty store")
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
t.Logf("Set prefs for default profile")
|
||||||
|
wantProfiles["user@1.example.com"] = setPrefs(t, "user@1.example.com")
|
||||||
|
wantCurProfile = "user@1.example.com"
|
||||||
|
delete(wantProfiles, "")
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
t.Logf("Create new profile")
|
||||||
|
pm.NewProfile()
|
||||||
|
wantCurProfile = ""
|
||||||
|
wantProfiles[""] = emptyPrefs
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
t.Logf("Set prefs for test profile")
|
||||||
|
wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com")
|
||||||
|
wantCurProfile = "user@2.example.com"
|
||||||
|
delete(wantProfiles, "")
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
t.Logf("Recreate profile manager from store")
|
||||||
|
// Recreate the profile manager to ensure that it can load the profiles
|
||||||
|
// from the store at startup.
|
||||||
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "linux")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
t.Logf("Delete default profile")
|
||||||
|
if err := pm.DeleteProfile(pm.findProfileByName("user@1.example.com").ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
delete(wantProfiles, "user@1.example.com")
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
t.Logf("Recreate profile manager from store after deleting default profile")
|
||||||
|
// Recreate the profile manager to ensure that it can load the profiles
|
||||||
|
// from the store at startup.
|
||||||
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "linux")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProfileManagementWindows tests going into and out of Unattended mode on
|
||||||
|
// Windows.
|
||||||
|
func TestProfileManagementWindows(t *testing.T) {
|
||||||
|
store := new(mem.Store)
|
||||||
|
|
||||||
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "", "windows")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantCurProfile := ""
|
||||||
|
wantProfiles := map[string]ipn.PrefsView{
|
||||||
|
"": emptyPrefs,
|
||||||
|
}
|
||||||
|
checkProfiles := func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
prof := pm.CurrentProfile()
|
||||||
|
t.Logf("\tCurrentProfile = %q", prof)
|
||||||
|
if prof.Name != wantCurProfile {
|
||||||
|
t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile)
|
||||||
|
}
|
||||||
|
if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) {
|
||||||
|
t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPrefs := func(t *testing.T, loginName string, forceDaemon bool) ipn.PrefsView {
|
||||||
|
p := pm.CurrentPrefs().AsStruct()
|
||||||
|
p.ForceDaemon = forceDaemon
|
||||||
|
p.Persist = &persist.Persist{
|
||||||
|
LoginName: loginName,
|
||||||
|
}
|
||||||
|
if err := pm.SetPrefs(p.View()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return p.View()
|
||||||
|
}
|
||||||
|
t.Logf("Check initial state from empty store")
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
t.Logf("Set user1 as logged in user")
|
||||||
|
if err := pm.SetCurrentUser("user1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
t.Logf("Save prefs for user1")
|
||||||
|
wantProfiles["default"] = setPrefs(t, "default", false)
|
||||||
|
wantCurProfile = "default"
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
t.Logf("Create new profile")
|
||||||
|
pm.NewProfile()
|
||||||
|
wantCurProfile = ""
|
||||||
|
wantProfiles[""] = emptyPrefs
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
t.Logf("Save as test profile")
|
||||||
|
wantProfiles["test"] = setPrefs(t, "test", false)
|
||||||
|
wantCurProfile = "test"
|
||||||
|
checkProfiles(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Recreate profile manager from store, should reset prefs")
|
||||||
|
// Recreate the profile manager to ensure that it can load the profiles
|
||||||
|
// from the store at startup.
|
||||||
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "windows")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantCurProfile = ""
|
||||||
|
wantProfiles[""] = emptyPrefs
|
||||||
|
checkProfiles(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
t.Logf("Set user1 as current user")
|
||||||
|
if err := pm.SetCurrentUser("user1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantCurProfile = "test"
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
{
|
||||||
|
t.Logf("set unattended mode")
|
||||||
|
wantProfiles["test"] = setPrefs(t, "test", true)
|
||||||
|
}
|
||||||
|
if pm.CurrentUser() != "user1" {
|
||||||
|
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUser(), "user1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate the profile manager to ensure that it starts with test profile.
|
||||||
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "windows")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
checkProfiles(t)
|
||||||
|
if pm.CurrentUser() != "user1" {
|
||||||
|
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUser(), "user1")
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/must"
|
"tailscale.com/util/must"
|
||||||
)
|
)
|
||||||
|
@ -49,7 +50,8 @@ type fakeSSHServer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetSSHUsernames(t *testing.T) {
|
func TestGetSSHUsernames(t *testing.T) {
|
||||||
b := new(LocalBackend)
|
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||||
|
b := &LocalBackend{pm: pm, store: pm.Store()}
|
||||||
b.sshServer = fakeSSHServer{}
|
b.sshServer = fakeSSHServer{}
|
||||||
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
|
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -97,7 +97,7 @@ type mockControl struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
calls []string
|
calls []string
|
||||||
authBlocked bool
|
authBlocked bool
|
||||||
persist persist.Persist
|
persist *persist.Persist
|
||||||
machineKey key.MachinePrivate
|
machineKey key.MachinePrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ func (cc *mockControl) populateKeys() (newKeys bool) {
|
||||||
newKeys = true
|
newKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if cc.persist.PrivateNodeKey.IsZero() {
|
if cc.persist != nil && cc.persist.PrivateNodeKey.IsZero() {
|
||||||
cc.logf("Generating a new nodekey.")
|
cc.logf("Generating a new nodekey.")
|
||||||
cc.persist.OldPrivateNodeKey = cc.persist.PrivateNodeKey
|
cc.persist.OldPrivateNodeKey = cc.persist.PrivateNodeKey
|
||||||
cc.persist.PrivateNodeKey = key.NewNode()
|
cc.persist.PrivateNodeKey = key.NewNode()
|
||||||
|
@ -142,7 +142,7 @@ func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netma
|
||||||
s := controlclient.Status{
|
s := controlclient.Status{
|
||||||
URL: url,
|
URL: url,
|
||||||
NetMap: nm,
|
NetMap: nm,
|
||||||
Persist: &cc.persist,
|
Persist: cc.persist,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
if loginFinished {
|
if loginFinished {
|
||||||
|
@ -290,7 +290,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Cleanup(e.Close)
|
t.Cleanup(e.Close)
|
||||||
|
|
||||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -303,7 +303,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
cc.opts = opts
|
cc.opts = opts
|
||||||
cc.logfActual = opts.Logf
|
cc.logfActual = opts.Logf
|
||||||
cc.authBlocked = true
|
cc.authBlocked = true
|
||||||
cc.persist = cc.opts.Persist
|
cc.persist = &cc.opts.Persist
|
||||||
cc.mu.Unlock()
|
cc.mu.Unlock()
|
||||||
|
|
||||||
cc.logf("ccGen: new mockControl.")
|
cc.logf("ccGen: new mockControl.")
|
||||||
|
@ -335,7 +335,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
// but not ask it to do anything yet.
|
// but not ask it to do anything yet.
|
||||||
t.Logf("\n\nStart")
|
t.Logf("\n\nStart")
|
||||||
notifies.expect(2)
|
notifies.expect(2)
|
||||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||||
{
|
{
|
||||||
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
||||||
cc.assertCalls("New", "unpause")
|
cc.assertCalls("New", "unpause")
|
||||||
|
@ -360,7 +360,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
// events as the first time, so UIs always know what to expect.
|
// events as the first time, so UIs always know what to expect.
|
||||||
t.Logf("\n\nStart2")
|
t.Logf("\n\nStart2")
|
||||||
notifies.expect(2)
|
notifies.expect(2)
|
||||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||||
{
|
{
|
||||||
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
||||||
cc.assertCalls("Shutdown", "unpause", "New", "unpause")
|
cc.assertCalls("Shutdown", "unpause", "New", "unpause")
|
||||||
|
@ -552,7 +552,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
t.Logf("\n\nFastpath Start()")
|
t.Logf("\n\nFastpath Start()")
|
||||||
notifies.expect(1)
|
notifies.expect(1)
|
||||||
b.state = ipn.Running
|
b.state = ipn.Running
|
||||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||||
{
|
{
|
||||||
nn := notifies.drain(1)
|
nn := notifies.drain(1)
|
||||||
cc.assertCalls()
|
cc.assertCalls()
|
||||||
|
@ -662,7 +662,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
// The frontend restarts!
|
// The frontend restarts!
|
||||||
t.Logf("\n\nStart3")
|
t.Logf("\n\nStart3")
|
||||||
notifies.expect(2)
|
notifies.expect(2)
|
||||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||||
{
|
{
|
||||||
// BUG: We already called Shutdown(), no need to do it again.
|
// BUG: We already called Shutdown(), no need to do it again.
|
||||||
// BUG: don't unpause because we're not logged in.
|
// BUG: don't unpause because we're not logged in.
|
||||||
|
@ -722,7 +722,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
// One more restart, this time with a valid key, but WantRunning=false.
|
// One more restart, this time with a valid key, but WantRunning=false.
|
||||||
t.Logf("\n\nStart4")
|
t.Logf("\n\nStart4")
|
||||||
notifies.expect(2)
|
notifies.expect(2)
|
||||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||||
{
|
{
|
||||||
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
||||||
// b.Shutdown() explicitly ourselves.
|
// b.Shutdown() explicitly ourselves.
|
||||||
|
@ -844,7 +844,7 @@ func TestStateMachine(t *testing.T) {
|
||||||
// logged in and WantRunning.
|
// logged in and WantRunning.
|
||||||
t.Logf("\n\nStart5")
|
t.Logf("\n\nStart5")
|
||||||
notifies.expect(1)
|
notifies.expect(1)
|
||||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||||
{
|
{
|
||||||
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
||||||
// b.Shutdown() ourselves.
|
// b.Shutdown() ourselves.
|
||||||
|
@ -920,27 +920,26 @@ func TestStateMachine(t *testing.T) {
|
||||||
|
|
||||||
func TestEditPrefsHasNoKeys(t *testing.T) {
|
func TestEditPrefsHasNoKeys(t *testing.T) {
|
||||||
logf := tstest.WhileTestRunningLogger(t)
|
logf := tstest.WhileTestRunningLogger(t)
|
||||||
store := new(testStateStorage)
|
|
||||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||||
}
|
}
|
||||||
t.Cleanup(e.Close)
|
t.Cleanup(e.Close)
|
||||||
|
|
||||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
b, err := NewLocalBackend(logf, "logid", new(mem.Store), "", nil, e, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewLocalBackend: %v", err)
|
t.Fatalf("NewLocalBackend: %v", err)
|
||||||
}
|
}
|
||||||
b.hostinfo = &tailcfg.Hostinfo{OS: "testos"}
|
b.hostinfo = &tailcfg.Hostinfo{OS: "testos"}
|
||||||
b.prefs = (&ipn.Prefs{
|
b.pm.SetPrefs((&ipn.Prefs{
|
||||||
Persist: &persist.Persist{
|
Persist: &persist.Persist{
|
||||||
PrivateNodeKey: key.NewNode(),
|
PrivateNodeKey: key.NewNode(),
|
||||||
OldPrivateNodeKey: key.NewNode(),
|
OldPrivateNodeKey: key.NewNode(),
|
||||||
|
|
||||||
LegacyFrontendPrivateMachineKey: key.NewMachine(),
|
LegacyFrontendPrivateMachineKey: key.NewMachine(),
|
||||||
},
|
},
|
||||||
}).View()
|
}).View())
|
||||||
if b.prefs.Persist().PrivateNodeKey.IsZero() {
|
if b.pm.CurrentPrefs().Persist().PrivateNodeKey.IsZero() {
|
||||||
t.Fatalf("PrivateNodeKey not set")
|
t.Fatalf("PrivateNodeKey not set")
|
||||||
}
|
}
|
||||||
p, err := b.EditPrefs(&ipn.MaskedPrefs{
|
p, err := b.EditPrefs(&ipn.MaskedPrefs{
|
||||||
|
@ -1005,7 +1004,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(mem.Store), nil, eng, 0)
|
b, err := NewLocalBackend(logf, "logid", new(mem.Store), "", nil, eng, 0)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
cc := newMockControl(t)
|
cc := newMockControl(t)
|
||||||
|
@ -1030,7 +1029,7 @@ func TestWGEngineStatusRace(t *testing.T) {
|
||||||
wantState(ipn.NoState)
|
wantState(ipn.NoState)
|
||||||
|
|
||||||
// Start the backend.
|
// Start the backend.
|
||||||
err = b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey})
|
err = b.Start(ipn.Options{})
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
wantState(ipn.NeedsLogin)
|
wantState(ipn.NeedsLogin)
|
||||||
|
|
||||||
|
|
|
@ -101,8 +101,7 @@ type Server struct {
|
||||||
// being run in "client mode" that requires an active GUI
|
// being run in "client mode" that requires an active GUI
|
||||||
// connection (such as on Windows by default). Even if this
|
// connection (such as on Windows by default). Even if this
|
||||||
// is true, the ForceDaemon pref can override this.
|
// is true, the ForceDaemon pref can override this.
|
||||||
resetOnZero bool
|
resetOnZero bool
|
||||||
autostartStateKey ipn.StateKey
|
|
||||||
|
|
||||||
bsMu sync.Mutex // lock order: bsMu, then mu
|
bsMu sync.Mutex // lock order: bsMu, then mu
|
||||||
bs *ipn.BackendServer
|
bs *ipn.BackendServer
|
||||||
|
@ -685,26 +684,6 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||||
}()
|
}()
|
||||||
logf("Listening on %v", ln.Addr())
|
logf("Listening on %v", ln.Addr())
|
||||||
|
|
||||||
var serverModeUser *user.User
|
|
||||||
if opts.AutostartStateKey == "" {
|
|
||||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
|
||||||
if err != nil && err != ipn.ErrStateNotExist {
|
|
||||||
return fmt.Errorf("calling ReadState on state store: %w", err)
|
|
||||||
}
|
|
||||||
key := string(autoStartKey)
|
|
||||||
if strings.HasPrefix(key, "user-") {
|
|
||||||
uid := strings.TrimPrefix(key, "user-")
|
|
||||||
u, err := lookupUserFromID(logf, uid)
|
|
||||||
if err != nil {
|
|
||||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
|
||||||
} else {
|
|
||||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
|
||||||
serverModeUser = u
|
|
||||||
}
|
|
||||||
opts.AutostartStateKey = ipn.StateKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||||
|
|
||||||
|
@ -745,7 +724,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
|
server, err := New(logf, logid, store, eng, dialer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -761,8 +740,8 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||||
// New returns a new Server.
|
// New returns a new Server.
|
||||||
//
|
//
|
||||||
// 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, opts Options) (*Server, error) {
|
||||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, opts.LoginFlags)
|
b, err := ipnlocal.NewLocalBackend(logf, logid, store, opts.AutostartStateKey, dialer, eng, opts.LoginFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NewLocalBackend: %v", err)
|
return nil, fmt.Errorf("NewLocalBackend: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -808,32 +787,23 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AutostartStateKey == "" {
|
var serverModeUser *user.User
|
||||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
if uid := b.CurrentUser(); uid != "" {
|
||||||
if err != nil && err != ipn.ErrStateNotExist {
|
u, err := lookupUserFromID(logf, uid)
|
||||||
return nil, fmt.Errorf("calling ReadState on store: %w", err)
|
if err != nil {
|
||||||
}
|
logf("ipnserver: found server mode auto-start key; failed to load user: %v", err)
|
||||||
key := string(autoStartKey)
|
} else {
|
||||||
if strings.HasPrefix(key, "user-") {
|
logf("ipnserver: found server mode auto-start key (user %s)", u.Username)
|
||||||
uid := strings.TrimPrefix(key, "user-")
|
serverModeUser = u
|
||||||
u, err := lookupUserFromID(logf, uid)
|
|
||||||
if err != nil {
|
|
||||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
|
||||||
} else {
|
|
||||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
|
||||||
serverModeUser = u
|
|
||||||
}
|
|
||||||
opts.AutostartStateKey = ipn.StateKey(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server := &Server{
|
server := &Server{
|
||||||
b: b,
|
b: b,
|
||||||
backendLogID: logid,
|
backendLogID: logid,
|
||||||
logf: logf,
|
logf: logf,
|
||||||
resetOnZero: !opts.SurviveDisconnects,
|
resetOnZero: !opts.SurviveDisconnects,
|
||||||
serverModeUser: serverModeUser,
|
serverModeUser: serverModeUser,
|
||||||
autostartStateKey: opts.AutostartStateKey,
|
|
||||||
}
|
}
|
||||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||||
return server, nil
|
return server, nil
|
||||||
|
@ -859,11 +829,11 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||||
ln.Close()
|
ln.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if s.autostartStateKey != "" {
|
if s.b.Prefs().Valid() {
|
||||||
s.bs.GotCommand(ctx, &ipn.Command{
|
s.bs.GotCommand(ctx, &ipn.Command{
|
||||||
Version: version.Long,
|
Version: version.Long,
|
||||||
Start: &ipn.StartArgs{
|
Start: &ipn.StartArgs{
|
||||||
Opts: ipn.Options{StateKey: s.autostartStateKey},
|
Opts: ipn.Options{},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
18
ipn/prefs.go
18
ipn/prefs.go
|
@ -681,3 +681,21 @@ func SavePrefs(filename string, p *Prefs) {
|
||||||
log.Printf("SavePrefs: %v\n", err)
|
log.Printf("SavePrefs: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfileID is an auto-generated system-wide unique identifier for a login
|
||||||
|
// profile. It is a 4 character hex string like "1ab3".
|
||||||
|
type ProfileID string
|
||||||
|
|
||||||
|
// LoginProfile represents a single login profile as managed
|
||||||
|
// by the ProfileManager.
|
||||||
|
type LoginProfile struct {
|
||||||
|
ID ProfileID
|
||||||
|
Name string
|
||||||
|
Key StateKey
|
||||||
|
|
||||||
|
UserProfile tailcfg.UserProfile
|
||||||
|
|
||||||
|
// LocalUserID is the user ID of the user who created this profile.
|
||||||
|
// It is only relevant on Windows where we have a multi-user system.
|
||||||
|
LocalUserID string
|
||||||
|
}
|
||||||
|
|
32
ipn/store.go
32
ipn/store.go
|
@ -19,14 +19,21 @@ const (
|
||||||
// in its key.NodePrivate.MarshalText representation.
|
// in its key.NodePrivate.MarshalText representation.
|
||||||
MachineKeyStateKey = StateKey("_machinekey")
|
MachineKeyStateKey = StateKey("_machinekey")
|
||||||
|
|
||||||
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
// LegacyGlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
||||||
// loads on startup.
|
// loads on startup.
|
||||||
//
|
//
|
||||||
// We have to support multiple state keys for other OSes (Windows in
|
// We have to support multiple state keys for other OSes (Windows in
|
||||||
// particular), but right now Unix daemons run with a single
|
// particular), but right now Unix daemons run with a single
|
||||||
// node-global state. To keep open the option of having per-user state
|
// node-global state. To keep open the option of having per-user state
|
||||||
// later, the global state key doesn't look like a username.
|
// later, the global state key doesn't look like a username.
|
||||||
GlobalDaemonStateKey = StateKey("_daemon")
|
//
|
||||||
|
// As of 2022-10-21, it has been superseded by profiles and is no longer
|
||||||
|
// written to disk. It is only read at startup when there are no profiles,
|
||||||
|
// to migrate the state to the "default" profile.
|
||||||
|
// The existing state is left on disk in case the user downgrades to an
|
||||||
|
// older version of Tailscale that doesn't support profiles. We can
|
||||||
|
// remove this in a future release.
|
||||||
|
LegacyGlobalDaemonStateKey = StateKey("_daemon")
|
||||||
|
|
||||||
// ServerModeStartKey's value, if non-empty, is the value of a
|
// ServerModeStartKey's value, if non-empty, is the value of a
|
||||||
// StateKey containing the prefs to start with which to start the
|
// StateKey containing the prefs to start with which to start the
|
||||||
|
@ -40,8 +47,27 @@ const (
|
||||||
// NLKeyStateKey is the key under which we store the node's
|
// NLKeyStateKey is the key under which we store the node's
|
||||||
// network-lock node key, in its key.NLPrivate.MarshalText representation.
|
// network-lock node key, in its key.NLPrivate.MarshalText representation.
|
||||||
NLKeyStateKey = StateKey("_nl-node-key")
|
NLKeyStateKey = StateKey("_nl-node-key")
|
||||||
|
|
||||||
|
// KnownProfilesStateKey is the key under which we store the list of
|
||||||
|
// known profiles. The value is a JSON-encoded []LoginProfile.
|
||||||
|
KnownProfilesStateKey = StateKey("_profiles")
|
||||||
|
|
||||||
|
// CurrentProfileStateKey is the key under which we store the current
|
||||||
|
// profile.
|
||||||
|
CurrentProfileStateKey = StateKey("_current-profile")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CurrentProfileID returns the StateKey that stores the
|
||||||
|
// current profile ID. The value is a JSON-encoded LoginProfile.
|
||||||
|
// If the userID is empty, the key returned is CurrentProfileStateKey,
|
||||||
|
// otherwise it is "_current/"+userID.
|
||||||
|
func CurrentProfileKey(userID string) StateKey {
|
||||||
|
if userID == "" {
|
||||||
|
return CurrentProfileStateKey
|
||||||
|
}
|
||||||
|
return StateKey("_current/" + userID)
|
||||||
|
}
|
||||||
|
|
||||||
// StateStore persists state, and produces it back on request.
|
// StateStore persists state, and produces it back on request.
|
||||||
type StateStore interface {
|
type StateStore interface {
|
||||||
// ReadState returns the bytes associated with ID. Returns (nil,
|
// ReadState returns the bytes associated with ID. Returns (nil,
|
||||||
|
@ -67,7 +93,7 @@ func PutStoreInt(store StateStore, id StateKey, val int64) error {
|
||||||
|
|
||||||
// ServeConfigKey returns a StateKey that stores the
|
// ServeConfigKey returns a StateKey that stores the
|
||||||
// JSON-encoded ServeConfig for a config profile.
|
// JSON-encoded ServeConfig for a config profile.
|
||||||
func ServeConfigKey(profileID string) StateKey {
|
func ServeConfigKey(profileID ProfileID) StateKey {
|
||||||
return StateKey("_serve/" + profileID)
|
return StateKey("_serve/" + profileID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -507,7 +507,7 @@ func TestSSH(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
lb, err := ipnlocal.NewLocalBackend(logf, "",
|
lb, err := ipnlocal.NewLocalBackend(logf, "",
|
||||||
new(mem.Store),
|
new(mem.Store), "",
|
||||||
new(tsdial.Dialer),
|
new(tsdial.Dialer),
|
||||||
eng, 0)
|
eng, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -324,7 +324,7 @@ func (s *Server) start() (reterr error) {
|
||||||
if s.Ephemeral {
|
if s.Ephemeral {
|
||||||
loginFlags = controlclient.LoginEphemeral
|
loginFlags = controlclient.LoginEphemeral
|
||||||
}
|
}
|
||||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, s.Store, s.dialer, eng, loginFlags)
|
lb, err := ipnlocal.NewLocalBackend(logf, logid, s.Store, "", s.dialer, eng, loginFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -340,7 +340,6 @@ func (s *Server) start() (reterr error) {
|
||||||
prefs.WantRunning = true
|
prefs.WantRunning = true
|
||||||
authKey := s.getAuthKey()
|
authKey := s.getAuthKey()
|
||||||
err = lb.Start(ipn.Options{
|
err = lb.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
|
||||||
UpdatePrefs: prefs,
|
UpdatePrefs: prefs,
|
||||||
AuthKey: authKey,
|
AuthKey: authKey,
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
|
|
||||||
"go4.org/mem"
|
"go4.org/mem"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnlocal"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/ipn/store"
|
"tailscale.com/ipn/store"
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
|
@ -659,15 +660,11 @@ func (n *testNode) diskPrefs() *ipn.Prefs {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("reading prefs, NewFileStore: %v", err)
|
t.Fatalf("reading prefs, NewFileStore: %v", err)
|
||||||
}
|
}
|
||||||
prefBytes, err := fs.ReadState(ipn.GlobalDaemonStateKey)
|
p, err := ipnlocal.ReadStartupPrefsForTest(t.Logf, fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("reading prefs, ReadState: %v", err)
|
t.Fatalf("reading prefs, ReadDiskPrefsForTest: %v", err)
|
||||||
}
|
}
|
||||||
p := new(ipn.Prefs)
|
return p.AsStruct()
|
||||||
if err := json.Unmarshal(prefBytes, p); err != nil {
|
|
||||||
t.Fatalf("reading prefs, JSON unmarshal: %v", err)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AwaitResponding waits for n's tailscaled to be up enough to be
|
// AwaitResponding waits for n's tailscaled to be up enough to be
|
||||||
|
|
|
@ -8,6 +8,7 @@ package persist
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/structs"
|
"tailscale.com/types/structs"
|
||||||
)
|
)
|
||||||
|
@ -34,6 +35,7 @@ type Persist struct {
|
||||||
OldPrivateNodeKey key.NodePrivate // needed to request key rotation
|
OldPrivateNodeKey key.NodePrivate // needed to request key rotation
|
||||||
Provider string
|
Provider string
|
||||||
LoginName string
|
LoginName string
|
||||||
|
UserProfile tailcfg.UserProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicNodeKey returns the public key for the node key.
|
// PublicNodeKey returns the public key for the node key.
|
||||||
|
@ -53,7 +55,8 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||||
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||||
p.Provider == p2.Provider &&
|
p.Provider == p2.Provider &&
|
||||||
p.LoginName == p2.LoginName
|
p.LoginName == p2.LoginName &&
|
||||||
|
p.UserProfile == p2.UserProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Persist) Pretty() string {
|
func (p *Persist) Pretty() string {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
package persist
|
package persist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/structs"
|
"tailscale.com/types/structs"
|
||||||
)
|
)
|
||||||
|
@ -30,4 +31,5 @@ var _PersistCloneNeedsRegeneration = Persist(struct {
|
||||||
OldPrivateNodeKey key.NodePrivate
|
OldPrivateNodeKey key.NodePrivate
|
||||||
Provider string
|
Provider string
|
||||||
LoginName string
|
LoginName string
|
||||||
|
UserProfile tailcfg.UserProfile
|
||||||
}{})
|
}{})
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPersistEqual(t *testing.T) {
|
func TestPersistEqual(t *testing.T) {
|
||||||
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
|
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName", "UserProfile"}
|
||||||
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
|
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
|
||||||
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||||
have, persistHandles)
|
have, persistHandles)
|
||||||
|
@ -92,6 +93,25 @@ func TestPersistEqual(t *testing.T) {
|
||||||
&Persist{LoginName: "foo@tailscale.com"},
|
&Persist{LoginName: "foo@tailscale.com"},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
&Persist{UserProfile: tailcfg.UserProfile{
|
||||||
|
ID: tailcfg.UserID(3),
|
||||||
|
}},
|
||||||
|
&Persist{UserProfile: tailcfg.UserProfile{
|
||||||
|
ID: tailcfg.UserID(3),
|
||||||
|
}},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Persist{UserProfile: tailcfg.UserProfile{
|
||||||
|
ID: tailcfg.UserID(3),
|
||||||
|
}},
|
||||||
|
&Persist{UserProfile: tailcfg.UserProfile{
|
||||||
|
ID: tailcfg.UserID(3),
|
||||||
|
DisplayName: "foo",
|
||||||
|
}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
if got := test.a.Equals(test.b); got != test.want {
|
if got := test.a.Equals(test.b); got != test.want {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/structs"
|
"tailscale.com/types/structs"
|
||||||
)
|
)
|
||||||
|
@ -68,6 +69,7 @@ func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNo
|
||||||
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
|
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
|
||||||
func (v PersistView) Provider() string { return v.ж.Provider }
|
func (v PersistView) Provider() string { return v.ж.Provider }
|
||||||
func (v PersistView) LoginName() string { return v.ж.LoginName }
|
func (v PersistView) LoginName() string { return v.ж.LoginName }
|
||||||
|
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
|
||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
var _PersistViewNeedsRegeneration = Persist(struct {
|
var _PersistViewNeedsRegeneration = Persist(struct {
|
||||||
|
@ -77,4 +79,5 @@ var _PersistViewNeedsRegeneration = Persist(struct {
|
||||||
OldPrivateNodeKey key.NodePrivate
|
OldPrivateNodeKey key.NodePrivate
|
||||||
Provider string
|
Provider string
|
||||||
LoginName string
|
LoginName string
|
||||||
|
UserProfile tailcfg.UserProfile
|
||||||
}{})
|
}{})
|
||||||
|
|
|
@ -293,8 +293,7 @@ func TestShouldProcessInbound(t *testing.T) {
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
|
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
|
||||||
}
|
}
|
||||||
i.lb.Start(ipn.Options{
|
i.lb.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
LegacyMigrationPrefs: prefs,
|
||||||
UpdatePrefs: prefs,
|
|
||||||
})
|
})
|
||||||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||||||
|
|
||||||
|
@ -326,8 +325,7 @@ func TestShouldProcessInbound(t *testing.T) {
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:200/120"),
|
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:200/120"),
|
||||||
}
|
}
|
||||||
i.lb.Start(ipn.Options{
|
i.lb.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
LegacyMigrationPrefs: prefs,
|
||||||
UpdatePrefs: prefs,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
want: false,
|
want: false,
|
||||||
|
@ -345,8 +343,7 @@ func TestShouldProcessInbound(t *testing.T) {
|
||||||
prefs := ipn.NewPrefs()
|
prefs := ipn.NewPrefs()
|
||||||
prefs.RunSSH = true
|
prefs.RunSSH = true
|
||||||
i.lb.Start(ipn.Options{
|
i.lb.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
LegacyMigrationPrefs: prefs,
|
||||||
UpdatePrefs: prefs,
|
|
||||||
})
|
})
|
||||||
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||||||
return addr.String() == "100.101.102.104" // Dst, above
|
return addr.String() == "100.101.102.104" // Dst, above
|
||||||
|
@ -367,8 +364,7 @@ func TestShouldProcessInbound(t *testing.T) {
|
||||||
prefs := ipn.NewPrefs()
|
prefs := ipn.NewPrefs()
|
||||||
prefs.RunSSH = false // default, but to be explicit
|
prefs.RunSSH = false // default, but to be explicit
|
||||||
i.lb.Start(ipn.Options{
|
i.lb.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
LegacyMigrationPrefs: prefs,
|
||||||
UpdatePrefs: prefs,
|
|
||||||
})
|
})
|
||||||
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||||||
return addr.String() == "100.101.102.104" // Dst, above
|
return addr.String() == "100.101.102.104" // Dst, above
|
||||||
|
@ -427,8 +423,7 @@ func TestShouldProcessInbound(t *testing.T) {
|
||||||
netip.MustParsePrefix("10.0.0.1/24"),
|
netip.MustParsePrefix("10.0.0.1/24"),
|
||||||
}
|
}
|
||||||
i.lb.Start(ipn.Options{
|
i.lb.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
LegacyMigrationPrefs: prefs,
|
||||||
UpdatePrefs: prefs,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// As if we were running on Linux where netstack isn't used.
|
// As if we were running on Linux where netstack isn't used.
|
||||||
|
@ -458,7 +453,7 @@ func TestShouldProcessInbound(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Cleanup(e.Close)
|
t.Cleanup(e.Close)
|
||||||
|
|
||||||
lb, err := ipnlocal.NewLocalBackend(logf, "logid", new(mem.Store), new(tsdial.Dialer), e, 0)
|
lb, err := ipnlocal.NewLocalBackend(logf, "logid", new(mem.Store), "", new(tsdial.Dialer), e, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewLocalBackend: %v", err)
|
t.Fatalf("NewLocalBackend: %v", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue