client,cmd/tailscale,ipn/{ipnlocal,localapi}: add debug CLI command to clear netmap caches (#19213)
This is a follow-up to #19117, adding a debug CLI command allowing the operator to explicitly discard cached netmap data, as a safety and recovery measure. Updates #12639 Change-Id: I5c3c47c0204754b9c8e526a4ff8f69d6974db6d0 Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
This commit is contained in:
@@ -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")
|
||||||
|
}
|
||||||
@@ -51,9 +51,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debugCaptureCmd func() *ffcli.Command // or nil
|
debugCaptureCmd func() *ffcli.Command // or nil
|
||||||
debugPortmapCmd func() *ffcli.Command // or nil
|
debugPortmapCmd func() *ffcli.Command // or nil
|
||||||
debugPeerRelayCmd func() *ffcli.Command // or nil
|
debugPeerRelayCmd func() *ffcli.Command // or nil
|
||||||
|
debugClearNetmapCacheCmd func() *ffcli.Command // or nil
|
||||||
)
|
)
|
||||||
|
|
||||||
func debugCmd() *ffcli.Command {
|
func debugCmd() *ffcli.Command {
|
||||||
@@ -394,6 +395,7 @@ func debugCmd() *ffcli.Command {
|
|||||||
Exec: runPrintStateDir,
|
Exec: runPrintStateDir,
|
||||||
},
|
},
|
||||||
ccall(debugPeerRelayCmd),
|
ccall(debugPeerRelayCmd),
|
||||||
|
ccall(debugClearNetmapCacheCmd),
|
||||||
}...),
|
}...),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"tailscale.com/feature/buildfeatures"
|
"tailscale.com/feature/buildfeatures"
|
||||||
"tailscale.com/ipn/ipnlocal/netmapcache"
|
"tailscale.com/ipn/ipnlocal/netmapcache"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
@@ -64,21 +68,50 @@ func (b *LocalBackend) discardDiskCacheLocked() {
|
|||||||
if b.diskCache.cache == nil {
|
if b.diskCache.cache == nil {
|
||||||
return // nothing to do, we do not have a cache
|
return // nothing to do, we do not have a cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reaching here, we have a cache directory that needs to be purged.
|
// Reaching here, we have a cache directory that needs to be purged.
|
||||||
// Log errors but do not fail for them.
|
// Log errors but do not fail for them.
|
||||||
store := netmapcache.FileStore(b.diskCache.dir)
|
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, "") {
|
for key, err := range store.List(ctx, "") {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.logf("listing cache contents: %v", err)
|
errs = append(errs, fmt.Errorf("list cache contest: %w", err))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := store.Remove(ctx, key); err != nil {
|
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 = diskCache{} // drop in-memory state
|
||||||
b.diskCache.dir = ""
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "clear-netmap-cache":
|
||||||
|
h.b.ClearNetmapCache(r.Context())
|
||||||
case "":
|
case "":
|
||||||
err = fmt.Errorf("missing parameter 'action'")
|
err = fmt.Errorf("missing parameter 'action'")
|
||||||
default:
|
default:
|
||||||
|
|||||||
Reference in New Issue
Block a user