tailscale/ipn/ipnlocal/diskcache.go
Claus Lensbøl 78627c132f
wgengine/magicsock,ipn/ipnlocal: store and load homeDERP from cache (#19491)
With netmap caching, the home DERP of the self node was neither saved to
the cache or loaded from it, making nodes not stick to a DERP when
starting without a connection to control.

Instead, make sure that when a cache is available, load that cache,
before looking for DERP servers. This is implemented by allowing a skip
of ReSTUN in setting the DERP map (we must have a DERP map before
setting the home DERP), so the DERP from cache will set itself and be
sticky until a connection to control is established.

Making DERP only change when connected to control is handled by existing
code from f072d017bd8241675aa946a27fc1827f570435cb.

Updates #19490

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-04-29 10:24:09 -04:00

130 lines
3.8 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
}
func (b *LocalBackend) writeNetmapToDiskLocked(nm *netmap.NetworkMap) error {
if !buildfeatures.HasCacheNetMap || nm == nil || nm.Cached {
return nil
}
b.logf("writing netmap to disk cache")
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
}
// 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 b.diskCache.cache.Store(b.currentNode().Context(), &nmCopy)
}
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...)
}