summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRaj Singh <raj@tailscale.com>2026-03-31 12:50:16 -0500
committerRaj Singh <raj@tailscale.com>2026-03-31 12:51:43 -0500
commitd88ee20a98ca42675604225bb42d40cb9cb3551a (patch)
treef6737d8c0db8e10daa5ea690aa36f7b1fd2a1c3c
parent70afbac0534e64bac192ce677d4359328ee9890f (diff)
downloadtailscale-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.go5
-rw-r--r--control/controlclient/direct.go16
-rw-r--r--ipn/ipnlocal/local.go16
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.