From 4973956419be7dd1b70c63f25cf30f939f8fbddc Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Fri, 27 Jan 2023 15:41:03 -0800 Subject: [PATCH] ipn/ipnlocal: add /reset-auth LocalAPI endpoint The iOS has a command to reset the persisted state of the app, but it was doing its own direct keychain manipulation. This proved to be brittle (since we changed how preferences are stored with #6022), so we instead add a LocalAPI endpoint to do do this, which can be updated in tandem. This clears the same state as the iOS implementation (tailscale/corp#3186), that is the machine key and preferences (which includes the node key). Notably this does not clear the logtail ID, so that logs from the device still end up in the same place. Updates tailscale/corp#8923 Signed-off-by: Mihai Parparita --- ipn/ipnlocal/local.go | 32 ++++++++++++++++++++++++++++++++ ipn/ipnlocal/profiles.go | 25 ++++++++++++++++++++++--- ipn/localapi/localapi.go | 18 ++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) 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":