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 <mihai@tailscale.com>
pull/7097/head
Mihai Parparita 2 years ago committed by Mihai Parparita
parent 947c14793a
commit 4973956419

@ -2148,6 +2148,20 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
return nil 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. // migrateStateLocked migrates state from the frontend to the backend.
// It is a no-op if prefs is nil // It is a no-op if prefs is nil
// b.mu must be held. // b.mu must be held.
@ -4759,3 +4773,21 @@ func (b *LocalBackend) ListProfiles() []ipn.LoginProfile {
defer b.mu.Unlock() defer b.mu.Unlock()
return b.pm.Profiles() 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()
}

@ -382,6 +382,24 @@ func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
return pm.writeKnownProfiles() 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 { func (pm *profileManager) writeKnownProfiles() error {
b, err := json.Marshal(pm.knownProfiles) b, err := json.Marshal(pm.knownProfiles)
if err != nil { if err != nil {
@ -564,6 +582,7 @@ var (
metricNewProfile = clientmetric.NewCounter("profiles_new") metricNewProfile = clientmetric.NewCounter("profiles_new")
metricSwitchProfile = clientmetric.NewCounter("profiles_switch") metricSwitchProfile = clientmetric.NewCounter("profiles_switch")
metricDeleteProfile = clientmetric.NewCounter("profiles_delete") metricDeleteProfile = clientmetric.NewCounter("profiles_delete")
metricDeleteAllProfile = clientmetric.NewCounter("profiles_delete_all")
metricMigration = clientmetric.NewCounter("profiles_migration") metricMigration = clientmetric.NewCounter("profiles_migration")
metricMigrationError = clientmetric.NewCounter("profiles_migration_error") metricMigrationError = clientmetric.NewCounter("profiles_migration_error")

@ -83,6 +83,7 @@ var handler = map[string]localAPIHandler{
"ping": (*Handler).servePing, "ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs, "prefs": (*Handler).servePrefs,
"pprof": (*Handler).servePprof, "pprof": (*Handler).servePprof,
"reset-auth": (*Handler).serveResetAuth,
"serve-config": (*Handler).serveServeConfig, "serve-config": (*Handler).serveServeConfig,
"set-dns": (*Handler).serveSetDNS, "set-dns": (*Handler).serveSetDNS,
"set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-expiry-sooner": (*Handler).serveSetExpirySooner,
@ -621,6 +622,23 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
servePprofFunc(w, r) 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) { func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case "GET": case "GET":

Loading…
Cancel
Save