diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index dc3ebd300..fe01a202c 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -79,6 +79,7 @@ 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 @@ -176,6 +177,12 @@ 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 @@ -322,6 +329,7 @@ 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()) @@ -646,6 +654,14 @@ 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 845317c4a..267eba301 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2629,6 +2629,7 @@ 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, @@ -3630,6 +3631,21 @@ 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.