diff options
| author | Raj Singh <raj@tailscale.com> | 2026-03-31 12:50:16 -0500 |
|---|---|---|
| committer | Raj Singh <raj@tailscale.com> | 2026-03-31 12:51:43 -0500 |
| commit | d88ee20a98ca42675604225bb42d40cb9cb3551a (patch) | |
| tree | f6737d8c0db8e10daa5ea690aa36f7b1fd2a1c3c | |
| parent | 70afbac0534e64bac192ce677d4359328ee9890f (diff) | |
| download | tailscale-rajsingh/persist-nodekey-before-register.tar.xz tailscale-rajsingh/persist-nodekey-before-register.zip | |
controlclient: persist node key after login, don't wait for netmaprajsingh/persist-nodekey-before-register
sendStatus only included Persist in the Status when a netmap was
available (nm != nil && loggedIn && inMapPoll). This meant the node
key was not written to the state store until the first map response
arrived. If the process was killed after a successful registration
but before the first netmap, the key was lost and a fresh one
generated on restart.
Decouple persist from the netmap gate so it flows to the state store
as soon as login succeeds.
Fixes #19149
Signed-off-by: Raj Singh <raj@tailscale.com>
| -rw-r--r-- | control/controlclient/auto.go | 5 | ||||
| -rw-r--r-- | control/controlclient/direct.go | 16 | ||||
| -rw-r--r-- | ipn/ipnlocal/local.go | 16 |
3 files changed, 3 insertions, 34 deletions
diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index b087e1444..d7da21932 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -625,9 +625,10 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM c.logf("[v1] sendStatus: %s: loggedIn=%v inMapPoll=%v", who, loggedIn, inMapPoll) var p persist.PersistView - if nm != nil && loggedIn && inMapPoll { + if loggedIn { p = c.direct.GetPersist() - } else { + } + if !(nm != nil && loggedIn && inMapPoll) { // don't send netmap status, as it's misleading when we're // not logged in. nm = nil diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index cbbf2bdfe..3b4e6ba9b 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -79,7 +79,6 @@ type Direct struct { autoUpdatePub *eventbus.Publisher[AutoUpdate] controlTimePub *eventbus.Publisher[ControlTime] getMachinePrivKey func() (key.MachinePrivate, error) - persistState func(persist.PersistView) // or nil; called before RegisterRequest to persist new node key debugFlags []string pinger Pinger popBrowser func(url string) // or nil @@ -177,12 +176,6 @@ type Options struct { // attempted. It is used to allow the client to clean up any resources or complete any // tasks that are dependent on a live client. Shutdown func() - - // PersistState, if non-nil, is called with an updated Persist after - // generating a new node key but before sending the RegisterRequest. - // This allows the caller to write the key to durable storage so that - // a crash during registration doesn't lose the key. - PersistState func(persist.PersistView) } // ControlDialPlanner is the interface optionally supplied when creating a @@ -338,7 +331,6 @@ func NewDirect(opts Options) (*Direct, error) { dialer: opts.Dialer, dnsCache: dnsCache, dialPlan: opts.DialPlan, - persistState: opts.PersistState, } c.discoPubKey = opts.DiscoPublicKey c.closedCtx, c.closeCtx = context.WithCancel(context.Background()) @@ -665,14 +657,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new persist.NetworkLockKey = key.NewNLPrivate() } - // Persist the new key before the RegisterRequest so a crash - // mid-HTTP doesn't lose it. - if c.persistState != nil && !tryingNewKey.IsZero() && (persist.PrivateNodeKey.IsZero() || tryingNewKey.Public() != persist.PrivateNodeKey.Public()) { - preHTTPPersist := persist - preHTTPPersist.PrivateNodeKey = tryingNewKey - c.persistState(preHTTPPersist.View()) - } - nlPub := persist.NetworkLockKey.Public() if tryingNewKey.IsZero() { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3d5fc12fa..edeb2967b 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2639,7 +2639,6 @@ func (b *LocalBackend) startLocked(opts ipn.Options) error { // but it won't take effect until the next Start. cc, err := b.getNewControlClientFuncLocked()(controlclient.Options{ GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(), - PersistState: b.createPersistStateFunc(), Logf: logger.WithPrefix(b.logf, "control: "), Persist: *persistv, ServerURL: serverURL, @@ -3641,21 +3640,6 @@ func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePriva } } -// createPersistStateFunc returns a function that writes an updated Persist -// directly to the state store, bypassing setPrefsNoPermCheck to avoid -// triggering hooks for state not yet confirmed by the control server. -func (b *LocalBackend) createPersistStateFunc() func(persist.PersistView) { - return func(p persist.PersistView) { - b.mu.Lock() - defer b.mu.Unlock() - prefs := b.pm.CurrentPrefs().AsStruct() - prefs.Persist = p.AsStruct() - if err := b.pm.writePrefsToStore(b.pm.currentProfile.Key(), prefs.View()); err != nil { - b.logf("persist node key before register: %v", err) - } - } -} - // initMachineKeyLocked is called to initialize b.machinePrivKey. // // b.prefs must already be initialized. |
