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

230 lines
6.4 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.writeNetmapToDiskLockedWithPeers(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.writeNetmapToDiskLockedWithPeers(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.writeNetmapToDiskLockedWithPeers(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.writeNetmapToDiskLockedWithPeers(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)
}
}