tailscale/ipn/ipnlocal/diskcache_test.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

230 lines
6.3 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"net/netip"
"testing"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus"
"tailscale.com/wgengine/magicsock"
)
// newCacheTestNetmap returns a minimal valid netmap suitable for testing disk
// cache operations.
func newCacheTestNetmap() *netmap.NetworkMap {
return &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "test-node.ts.net",
User: tailcfg.UserID(1),
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.0.1/32"),
},
}).View(),
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
tailcfg.UserID(1): (&tailcfg.UserProfile{
LoginName: "user@example.com",
DisplayName: "Test User",
}).View(),
},
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {},
2: {},
3: {},
4: {},
5: {},
6: {},
7: {},
8: {},
9: {},
10: {},
11: {},
},
},
}
}
func TestWriteAndLoadHomeDERP(t *testing.T) {
b := newTestBackend(t)
nm := newCacheTestNetmap()
b.currentNode().SetNetMap(nm)
const wantDERP = 7
b.currentNode().homeDERP.Store(wantDERP)
b.mu.Lock()
defer b.mu.Unlock()
if err := b.writeNetmapToDiskLocked(nm); err != nil {
t.Fatalf("writeNetmapToDiskLocked: %v", err)
}
loaded, ok := b.loadDiskCacheLocked()
if !ok {
t.Fatal("loadDiskCacheLocked returned ok=false")
}
if !loaded.SelfNode.Valid() {
t.Fatal("loaded netmap SelfNode is invalid")
}
if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
t.Errorf("loaded SelfNode.HomeDERP() = %d, want %d", got, wantDERP)
}
}
func TestOnHomeDERPUpdate(t *testing.T) {
t.Run("normal_derp_change", func(t *testing.T) {
b := newTestBackend(t)
done := make(chan struct{})
tstest.Replace(t, &testOnlyHomeDERPUpdate, func() { close(done) })
nm := newCacheTestNetmap()
b.currentNode().SetNetMap(nm)
// Publish a HomeDERPChanged event via the backend's event bus.
bus := b.Sys().Bus.Get()
ec := bus.Client("test.TestOnHomeDERPUpdate")
pub := eventbus.Publish[magicsock.HomeDERPChanged](ec)
const wantDERP = 11
pub.Publish(magicsock.HomeDERPChanged{Old: 0, New: wantDERP})
<-done
if got := b.currentNode().homeDERP.Load(); got != wantDERP {
t.Errorf("b.homeDERP = %d, want %d", got, wantDERP)
}
// Verify the value was persisted to the disk cache.
b.mu.Lock()
defer b.mu.Unlock()
loaded, ok := b.loadDiskCacheLocked()
if !ok {
t.Fatal("loadDiskCacheLocked returned ok=false after homeDERP update")
}
if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
t.Errorf("cached SelfNode.HomeDERP() = %d, want %d", got, wantDERP)
}
})
t.Run("old_does_not_match", func(t *testing.T) {
b := newTestBackend(t)
done := make(chan struct{})
tstest.Replace(t, &testOnlyHomeDERPUpdate, func() { close(done) })
const setDERP = 11
const wantDERP = 4
nm := newCacheTestNetmap()
selfNode := nm.SelfNode.AsStruct()
selfNode.HomeDERP = wantDERP
nm.SelfNode = selfNode.View()
b.currentNode().SetNetMap(nm)
b.currentNode().homeDERP.Store(wantDERP)
// Write an initial cache entry so we can verify it is not overwritten.
b.mu.Lock()
if err := b.writeNetmapToDiskLocked(nm); err != nil {
b.mu.Unlock()
t.Fatalf("setup writeNetmapToDiskLocked: %v", err)
}
b.mu.Unlock()
// Publish a HomeDERPChanged event via the backend's event bus.
bus := b.Sys().Bus.Get()
ec := bus.Client("test.TestOnHomeDERPUpdate")
pub := eventbus.Publish[magicsock.HomeDERPChanged](ec)
pub.Publish(magicsock.HomeDERPChanged{Old: wantDERP + 1, New: setDERP})
<-done
if got := b.currentNode().homeDERP.Load(); got != wantDERP {
t.Errorf("b.homeDERP = %d, wanted no change %d", got, wantDERP)
}
// Verify the cache still exists and still holds the original value.
b.mu.Lock()
defer b.mu.Unlock()
loaded, ok := b.loadDiskCacheLocked()
if !ok {
t.Fatal("loadDiskCacheLocked returned ok=false; expected cache to still exist")
}
if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
t.Errorf("cached SelfNode.HomeDERP() = %d after rejected event, want original %d", got, wantDERP)
}
})
t.Run("new_does_not_exist_in_map", func(t *testing.T) {
b := newTestBackend(t)
done := make(chan struct{})
tstest.Replace(t, &testOnlyHomeDERPUpdate, func() { close(done) })
const setDERP = 111
const wantDERP = 4
nm := newCacheTestNetmap()
selfNode := nm.SelfNode.AsStruct()
selfNode.HomeDERP = wantDERP
nm.SelfNode = selfNode.View()
b.currentNode().SetNetMap(nm)
b.currentNode().homeDERP.Store(wantDERP)
// Write an initial cache entry so we can verify it is not overwritten.
b.mu.Lock()
if err := b.writeNetmapToDiskLocked(nm); err != nil {
b.mu.Unlock()
t.Fatalf("setup writeNetmapToDiskLocked: %v", err)
}
b.mu.Unlock()
// Publish a HomeDERPChanged event via the backend's event bus.
// Old matches the stored homeDERP so only the "new region not in map"
// guard is exercised.
bus := b.Sys().Bus.Get()
ec := bus.Client("test.TestOnHomeDERPUpdate")
pub := eventbus.Publish[magicsock.HomeDERPChanged](ec)
pub.Publish(magicsock.HomeDERPChanged{Old: wantDERP, New: setDERP})
<-done
if got := b.currentNode().homeDERP.Load(); got != wantDERP {
t.Errorf("b.homeDERP = %d, wanted no change %d", got, wantDERP)
}
// Verify the cache still exists and still holds the original value.
b.mu.Lock()
defer b.mu.Unlock()
loaded, ok := b.loadDiskCacheLocked()
if !ok {
t.Fatal("loadDiskCacheLocked returned ok=false; expected cache to still exist")
}
if got := loaded.SelfNode.HomeDERP(); got != wantDERP {
t.Errorf("cached SelfNode.HomeDERP() = %d after rejected event, want original %d", got, wantDERP)
}
})
}
func TestWriteNetmapDoesNotMutateOriginal(t *testing.T) {
b := newTestBackend(t)
nm := newCacheTestNetmap()
b.currentNode().SetNetMap(nm)
originalDERP := nm.SelfNode.HomeDERP() // expected to be 0 initially
const storeDERP = 5
b.currentNode().homeDERP.Store(storeDERP)
b.mu.Lock()
defer b.mu.Unlock()
if err := b.writeNetmapToDiskLocked(nm); err != nil {
t.Fatalf("writeNetmapToDiskLocked: %v", err)
}
// The original netmap must not have been mutated.
if got := nm.SelfNode.HomeDERP(); got != originalDERP {
t.Errorf("original nm.SelfNode.HomeDERP() = %d after write, want %d (original was mutated)", got, originalDERP)
}
}