Skip to content

Commit

Permalink
ipn/ipnlocal: add /reset-auth LocalAPI endpoint
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
mihaip committed Jan 28, 2023
1 parent 947c147 commit 5a4d467
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 4 deletions.
2 changes: 1 addition & 1 deletion cmd/tailscaled/depaware.txt
Expand Up @@ -339,7 +339,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/maps from tailscale.com/wgengine+
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
Expand Down
32 changes: 32 additions & 0 deletions ipn/ipnlocal/local.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
}
31 changes: 28 additions & 3 deletions ipn/ipnlocal/profiles.go
Expand Up @@ -12,6 +12,7 @@ import (
"runtime"
"time"

"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/envknob"
"tailscale.com/ipn"
Expand Down Expand Up @@ -382,6 +383,29 @@ 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)

deletedProfileIDs := make([]ipn.ProfileID, 0)
for _, kp := range pm.knownProfiles {
if err := pm.store.WriteState(kp.Key, nil); err != nil {
// Write profiles we've already deleted, but return the original
// error.
for _, id := range deletedProfileIDs {
delete(pm.knownProfiles, id)
}
pm.writeKnownProfiles()
return err
}
deletedProfileIDs = append(deletedProfileIDs, kp.ID)
}
pm.NewProfile()
maps.Clear(pm.knownProfiles)
return pm.writeKnownProfiles()
}

func (pm *profileManager) writeKnownProfiles() error {
b, err := json.Marshal(pm.knownProfiles)
if err != nil {
Expand Down Expand Up @@ -561,9 +585,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")
Expand Down
18 changes: 18 additions & 0 deletions ipn/localapi/localapi.go
Expand Up @@ -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,
Expand Down Expand Up @@ -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":
Expand Down

0 comments on commit 5a4d467

Please sign in to comment.