diff --git a/cmd/tailscale/cli/debug-cachenetmap.go b/cmd/tailscale/cli/debug-cachenetmap.go new file mode 100644 index 000000000..735469ee4 --- /dev/null +++ b/cmd/tailscale/cli/debug-cachenetmap.go @@ -0,0 +1,31 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !ios && !ts_omit_cachenetmap + +package cli + +import ( + "context" + "errors" + + "github.com/peterbourgon/ff/v3/ffcli" +) + +func init() { + debugClearNetmapCacheCmd = func() *ffcli.Command { + return &ffcli.Command{ + Name: "clear-netmap-cache", + ShortUsage: "tailscale debug clear-netmap-cache", + ShortHelp: "Remove and discard cached network maps (if any)", + Exec: runDebugClearNetmapCache, + } + } +} + +func runDebugClearNetmapCache(ctx context.Context, args []string) error { + if len(args) != 0 { + return errors.New("unexpected arguments") + } + return localClient.DebugAction(ctx, "clear-netmap-cache") +} diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 10c383a51..944f99f91 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -51,9 +51,10 @@ import ( ) var ( - debugCaptureCmd func() *ffcli.Command // or nil - debugPortmapCmd func() *ffcli.Command // or nil - debugPeerRelayCmd func() *ffcli.Command // or nil + debugCaptureCmd func() *ffcli.Command // or nil + debugPortmapCmd func() *ffcli.Command // or nil + debugPeerRelayCmd func() *ffcli.Command // or nil + debugClearNetmapCacheCmd func() *ffcli.Command // or nil ) func debugCmd() *ffcli.Command { @@ -394,6 +395,7 @@ func debugCmd() *ffcli.Command { Exec: runPrintStateDir, }, ccall(debugPeerRelayCmd), + ccall(debugClearNetmapCacheCmd), }...), } } diff --git a/ipn/ipnlocal/diskcache.go b/ipn/ipnlocal/diskcache.go index 4de47cca2..03ced7967 100644 --- a/ipn/ipnlocal/diskcache.go +++ b/ipn/ipnlocal/diskcache.go @@ -4,6 +4,10 @@ package ipnlocal import ( + "context" + "errors" + "fmt" + "tailscale.com/feature/buildfeatures" "tailscale.com/ipn/ipnlocal/netmapcache" "tailscale.com/types/netmap" @@ -64,21 +68,50 @@ func (b *LocalBackend) discardDiskCacheLocked() { if b.diskCache.cache == nil { return // nothing to do, we do not have a cache } - // Reaching here, we have a cache directory that needs to be purged. // Log errors but do not fail for them. store := netmapcache.FileStore(b.diskCache.dir) - ctx := b.currentNode().Context() + if err := b.clearStoreLocked(b.currentNode().Context(), store); err != nil { + b.logf("clearing netmap cache: %v", err) + } + b.diskCache = diskCache{} // drop in-memory state +} + +// clearStoreLocked discards all the keys in the specified store. +func (b *LocalBackend) clearStoreLocked(ctx context.Context, store netmapcache.Store) error { + var errs []error for key, err := range store.List(ctx, "") { if err != nil { - b.logf("listing cache contents: %v", err) + errs = append(errs, fmt.Errorf("list cache contest: %w", err)) break } if err := store.Remove(ctx, key); err != nil { - b.logf("discarding cache key %q: %v", key, err) + errs = append(errs, fmt.Errorf("discard cache key %q: %w", key, err)) + } + } + return errors.Join(errs...) +} + +// ClearNetmapCache discards stored netmap caches (if any) for profiles for the +// current user of b. It also drops any cache from the active backend session, +// if there is one. +func (b *LocalBackend) ClearNetmapCache(ctx context.Context) error { + if !buildfeatures.HasCacheNetMap { + return nil // disabled + } + + b.mu.Lock() + defer b.mu.Unlock() + + var errs []error + for _, p := range b.pm.Profiles() { + store := netmapcache.FileStore(b.profileDataPathLocked(p.ID(), "netmap-cache")) + err := b.clearStoreLocked(ctx, store) + if err != nil { + errs = append(errs, fmt.Errorf("clear netmap cache for profile %q: %w", p.ID(), err)) } } - b.diskCache.cache = nil // drop reference - b.diskCache.dir = "" + b.diskCache = diskCache{} // drop in-memory state + return errors.Join(errs...) } diff --git a/ipn/localapi/debug.go b/ipn/localapi/debug.go index bc77464fd..d8e46040d 100644 --- a/ipn/localapi/debug.go +++ b/ipn/localapi/debug.go @@ -241,6 +241,8 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { if err == nil { return } + case "clear-netmap-cache": + h.b.ClearNetmapCache(r.Context()) case "": err = fmt.Errorf("missing parameter 'action'") default: