diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index fcfd6ebf8..4bbd97876 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2148,6 +2148,20 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) { return nil } +// clearMachineKeyLocked is called to clear the persisted and in-memory +// machine key, so that initMachineKeyLocked (called as part of starting) +// generates a new machine key. +// +// b.mu must be held. +func (b *LocalBackend) clearMachineKeyLocked() error { + if err := b.store.WriteState(ipn.MachineKeyStateKey, nil); err != nil { + return err + } + b.machinePrivKey = key.MachinePrivate{} + b.logf("machine key cleared") + return nil +} + // migrateStateLocked migrates state from the frontend to the backend. // It is a no-op if prefs is nil // b.mu must be held. @@ -4759,3 +4773,21 @@ func (b *LocalBackend) ListProfiles() []ipn.LoginProfile { defer b.mu.Unlock() return b.pm.Profiles() } + +// ResetAuth resets the authentication state, including persisted keys. Also +// has the side effect of removing all profiles and reseting preferences. The +// backend is left with a new profile, ready for StartLoginInterative to be +// called to register it as new node. +func (b *LocalBackend) ResetAuth() error { + b.mu.Lock() + b.resetControlClientLockedAsync() + if err := b.clearMachineKeyLocked(); err != nil { + b.mu.Unlock() + return err + } + if err := b.pm.DeleteAllProfiles(); err != nil { + b.mu.Unlock() + return err + } + return b.resetForProfileChangeLockedOnEntry() +} diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index ec3ee1002..556d490fb 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -382,6 +382,24 @@ func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error { return pm.writeKnownProfiles() } +// DeleteAllProfiles removes all known profiles and switches to a new empty +// profile. +func (pm *profileManager) DeleteAllProfiles() error { + metricDeleteAllProfile.Add(1) + + for _, kp := range pm.knownProfiles { + if err := pm.store.WriteState(kp.Key, nil); err != nil { + // Write to remove references to profiles we've already deleted, but + // return the original error. + pm.writeKnownProfiles() + return err + } + delete(pm.knownProfiles, kp.ID) + } + pm.NewProfile() + return pm.writeKnownProfiles() +} + func (pm *profileManager) writeKnownProfiles() error { b, err := json.Marshal(pm.knownProfiles) if err != nil { @@ -561,9 +579,10 @@ func (pm *profileManager) migrateFromLegacyPrefs() error { } var ( - metricNewProfile = clientmetric.NewCounter("profiles_new") - metricSwitchProfile = clientmetric.NewCounter("profiles_switch") - metricDeleteProfile = clientmetric.NewCounter("profiles_delete") + metricNewProfile = clientmetric.NewCounter("profiles_new") + metricSwitchProfile = clientmetric.NewCounter("profiles_switch") + metricDeleteProfile = clientmetric.NewCounter("profiles_delete") + metricDeleteAllProfile = clientmetric.NewCounter("profiles_delete_all") metricMigration = clientmetric.NewCounter("profiles_migration") metricMigrationError = clientmetric.NewCounter("profiles_migration_error") diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 130f99abc..c5db03f83 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -83,6 +83,7 @@ var handler = map[string]localAPIHandler{ "ping": (*Handler).servePing, "prefs": (*Handler).servePrefs, "pprof": (*Handler).servePprof, + "reset-auth": (*Handler).serveResetAuth, "serve-config": (*Handler).serveServeConfig, "set-dns": (*Handler).serveSetDNS, "set-expiry-sooner": (*Handler).serveSetExpirySooner, @@ -621,6 +622,23 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) { servePprofFunc(w, r) } +func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "reset-auth modify access denied", http.StatusForbidden) + return + } + if r.Method != httpm.POST { + http.Error(w, "use POST", http.StatusMethodNotAllowed) + return + } + + if err := h.b.ResetAuth(); err != nil { + http.Error(w, "reset-auth failed: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} + func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET":