Files
tailscale/ipn/ipnlocal/diskcache.go
M. J. Fromberger c09407002f ipn/ipnlocal/netmapcache: add UpdateSelfOnly method (#19818)
Some netmap updates are guaranteed to affect only the "static" parts of the
netmap, and so should not require us to walk through all the peers and user
profiles when updating the cache. To support this, the new UpdateSelfOnly
method updates only the Self node and other tailnet settings that are not
dependent on the peers and profiles.

Use this when updating the cache on DERP home changes.

Updates #12542

Change-Id: Ifed522b29d579fb76e010b4ff738cc4e0a72d27f
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-05-20 16:29:04 -07:00

149 lines
4.7 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"context"
"errors"
"fmt"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn/ipnlocal/netmapcache"
"tailscale.com/types/netmap"
)
// diskCache is the state netmap caching to disk.
type diskCache struct {
// all fields guarded by LocalBackend.mu
dir string // active profile cache directory
cache *netmapcache.Cache
}
// writeNetmapToDiskLockedWithoutPeers updates nm in the cache, excluding peers and profiles.
func (b *LocalBackend) writeNetmapToDiskLockedWithoutPeers(nm *netmap.NetworkMap) error {
if !buildfeatures.HasCacheNetMap || nm == nil || nm.Cached {
return nil
} else if err := b.ensureDiskCacheLocked(); err != nil {
return err
}
b.logf("updating netmap in disk cache")
return b.diskCache.cache.UpdateSelfOnly(b.currentNode().Context(), b.patchNetmapHomeDERPLocked(nm))
}
// writeNetmapToDiskLockedWithPeers writes nm into the cache, including peers and profiles.
func (b *LocalBackend) writeNetmapToDiskLockedWithPeers(nm *netmap.NetworkMap) error {
if !buildfeatures.HasCacheNetMap || nm == nil || nm.Cached {
return nil
} else if err := b.ensureDiskCacheLocked(); err != nil {
return err
}
b.logf("writing netmap to disk cache")
return b.diskCache.cache.Store(b.currentNode().Context(), b.patchNetmapHomeDERPLocked(nm))
}
func (b *LocalBackend) patchNetmapHomeDERPLocked(nm *netmap.NetworkMap) *netmap.NetworkMap {
// Set the homeDERP on the self node before saving. The self node homeDERP is
// generally not used since the homeDERP for self is stored in magicsock, but
// to be able to load it during loading the cache, we use the existing field
// to save it.
// Make a shallow copy and mutate a copy of the selfNode.
nmCopy := *nm
selfNode := nm.SelfNode.AsStruct()
selfNode.HomeDERP = int(b.currentNode().homeDERP.Load())
nmCopy.SelfNode = selfNode.View()
return &nmCopy
}
func (b *LocalBackend) ensureDiskCacheLocked() error {
dir, err := b.profileMkdirAllLocked(b.pm.CurrentProfile().ID(), "netmap-cache")
if err != nil {
return err
}
if c := b.diskCache; c.cache == nil || c.dir != dir {
b.diskCache.cache = netmapcache.NewCache(netmapcache.FileStore(dir))
b.diskCache.dir = dir
}
return nil
}
func (b *LocalBackend) loadDiskCacheLocked() (om *netmap.NetworkMap, ok bool) {
if !buildfeatures.HasCacheNetMap {
return nil, false
}
dir, err := b.profileMkdirAllLocked(b.pm.CurrentProfile().ID(), "netmap-cache")
if err != nil {
b.logf("profile data directory: %v", err)
return nil, false
}
if c := b.diskCache; c.cache == nil || c.dir != dir {
b.diskCache.cache = netmapcache.NewCache(netmapcache.FileStore(dir))
b.diskCache.dir = dir
}
nm, err := b.diskCache.cache.Load(b.currentNode().Context())
if err != nil {
b.logf("load netmap from cache: %v", err)
return nil, false
}
return nm, true
}
// discardDiskCacheLocked removes a cached network map for the current node, if
// one exists, and disables the cache.
func (b *LocalBackend) discardDiskCacheLocked() {
if !buildfeatures.HasCacheNetMap {
return
}
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)
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 {
errs = append(errs, fmt.Errorf("list cache contest: %w", err))
break
}
if err := store.Remove(ctx, key); err != nil {
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 = diskCache{} // drop in-memory state
return errors.Join(errs...)
}