Files
tailscale/ipn/backend_test.go
T
Brad Fitzpatrick ab4262680b ipn, ipn/ipnlocal: remove Notify.NetMap, size initial status to subscription
Nothing reads ipn.Notify.NetMap anymore. The previous commit removed
its runtime (non-initial) emission, and every first-party client is
also off the initial one: the Win32 and WinUI GUIs and the Apple
bridge subscribe with InitialStatus or InitialState plus peer deltas,
Android no longer uses it, and the remaining in-tree subscribers that set
NotifyInitialNetMap (sniproxy and the kube helpers) only did so to get
the initial Notify.SelfChange and discarded the netmap that tailscaled
built, encoded, and shipped for them.

Delete the Notify.NetMap field and the NotifyInitialNetMap bit. The
bit value stays reserved under the name ObsoleteNotifyInitialNetMap and
ValidateNotifyWatchOpt rejects subscriptions that set it, like the
NotifyRateLimit bit removed in the previous commit. NotifyNoNetMap
remains accepted as a no-op because shipping GUIs still set it.

The blessed way to seed a watcher's view is NotifyInitialStatus, but
it unconditionally built O(peers) status entries, which is exactly the
waste this series is deleting for watchers that only care about the
self node. Size the initial status to the subscription instead:
Status.Peer is only populated when the watcher also set
NotifyPeerChanges or NotifyPeerPatches, since only peer-delta
subscribers need a peer baseline to apply deltas to. That matches its
existing first-party users (containerboot and the WinUI GUI both pair
InitialStatus with peer bits).

Migrate sniproxy and the kube helpers to NotifyInitialStatus: they
seed from InitialStatus.Self and react to the (ungated) runtime
Notify.SelfChange messages after that, so their initial message now
carries one PeerStatus instead of a full netmap.

Also drop doc comment references to LocalClient.NetMap, a method that
does not exist; on-demand fetches go through other LocalAPI methods
such as LocalClient.Status.

Updates #12542

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I242992a744c0ffd0be6f27e8c735aa69d5b23b5e
2026-09-14 09:37:30 -07:00

259 lines
6.4 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"encoding/json"
"go/types"
"maps"
"math/bits"
"slices"
"strconv"
"strings"
"testing"
"golang.org/x/tools/go/packages"
"tailscale.com/health"
"tailscale.com/types/empty"
"tailscale.com/util/mak"
)
func TestNotifyString(t *testing.T) {
for _, tt := range []struct {
name string
value Notify
expected string
}{
{
name: "notify-empty",
value: Notify{},
expected: "Notify{}",
},
{
name: "notify-with-login-finished",
value: Notify{LoginFinished: &empty.Message{}},
expected: "Notify{LoginFinished}",
},
{
name: "notify-with-multiple-fields",
value: Notify{LoginFinished: &empty.Message{}, Health: &health.State{}},
expected: "Notify{LoginFinished Health{...}}",
},
} {
t.Run(tt.name, func(t *testing.T) {
actual := tt.value.String()
if actual != tt.expected {
t.Fatalf("expected=%q, actual=%q", tt.expected, actual)
}
})
}
}
func TestPeerWireGuardStateJSON(t *testing.T) {
tests := []struct {
state PeerWireGuardState
json string
}{
{PeerWireGuardStateNone, `"none"`},
{PeerWireGuardStateHandshake, `"handshake"`},
{PeerWireGuardStateEstablished, `"established"`},
{PeerWireGuardStateExpired, `"expired"`},
}
for _, tt := range tests {
t.Run(tt.state.String(), func(t *testing.T) {
got, err := json.Marshal(tt.state)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if string(got) != tt.json {
t.Errorf("Marshal(%v) = %s; want %s", tt.state, got, tt.json)
}
var back PeerWireGuardState
if err := json.Unmarshal(got, &back); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if back != tt.state {
t.Errorf("round-trip = %v; want %v", back, tt.state)
}
})
}
var bad PeerWireGuardState
if err := json.Unmarshal([]byte(`"bogus"`), &bad); err == nil {
t.Errorf("Unmarshal of bogus value did not return an error")
}
}
func TestValidateNotifyWatchOpt(t *testing.T) {
tests := []struct {
name string
mask NotifyWatchOpt
wantErr bool
}{
{
name: "peer-changes",
mask: NotifyPeerChanges | NotifyPeerPatches | NotifyNoNetMap | NotifyInitialStatus,
},
{
name: "in-process-no-disconnect",
mask: NotifyInProcessNoDisconnect | NotifyPeerChanges,
},
{
name: "obsolete-rate-limit-only",
mask: ObsoleteNotifyRateLimit,
wantErr: true,
},
{
name: "obsolete-rate-limit-with-peer-changes",
mask: ObsoleteNotifyRateLimit | NotifyPeerChanges,
wantErr: true,
},
{
name: "obsolete-initial-netmap",
mask: ObsoleteNotifyInitialNetMap,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateNotifyWatchOpt(tt.mask)
if gotErr := err != nil; gotErr != tt.wantErr {
t.Fatalf("ValidateNotifyWatchOpt(%v) error = %v; wantErr %v", tt.mask, err, tt.wantErr)
}
})
}
}
func TestNotifyWatchOptString(t *testing.T) {
consts := findNotifyWatchOptConstants(t)
t.Logf("consts = %#v", consts)
t.Run("zero", func(t *testing.T) {
var zero NotifyWatchOpt
want := "ipn.NotifyWatchOpt(0x0)"
if got := zero.String(); got != want {
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", zero, got, want)
}
})
t.Run("unknown", func(t *testing.T) {
msb := NotifyWatchOpt(1 << 63)
want := "ipn.NotifyWatchOpt(0x8000000000000000)"
if got := msb.String(); got != want {
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", msb, got, want)
}
})
t.Run("simple", func(t *testing.T) {
for _, c := range slices.Sorted(maps.Keys(consts)) {
if bits.OnesCount64(uint64(c)) > 1 {
continue // multiple bits comes later
}
want := "ipn." + consts[c]
if got := c.String(); got != want {
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", c, got, want)
}
}
})
t.Run("composite", func(t *testing.T) {
for _, tc := range []struct {
name string
value NotifyWatchOpt
want string
}{
{
name: "single",
value: NotifyWatchEngineUpdates,
want: "ipn.NotifyWatchEngineUpdates",
},
{
name: "double",
value: NotifyWatchEngineUpdates | NotifyInitialState,
want: "(ipn.NotifyWatchEngineUpdates | ipn.NotifyInitialState)",
},
{
name: "triple",
value: NotifyWatchEngineUpdates | NotifyInitialState | NotifyInitialPrefs,
want: "(ipn.NotifyWatchEngineUpdates | ipn.NotifyInitialState | ipn.NotifyInitialPrefs)",
},
{
name: "unknown",
value: NotifyWatchEngineUpdates | NotifyWatchOpt(1<<63),
want: "(ipn.NotifyWatchEngineUpdates | ipn.NotifyWatchOpt(0x8000000000000000))",
},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.value.String(); got != tc.want {
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", tc.value, got, tc.want)
}
})
}
})
// Check that every named NotifyWatchOpt value is mapped inside [NotifyWatchOpt.String].
t.Run("all", func(t *testing.T) {
var all NotifyWatchOpt
var names []string // names are sorted and only contain simple consts
for _, c := range slices.Sorted(maps.Keys(consts)) {
all |= c
if bits.OnesCount64(uint64(c)) == 1 {
names = append(names, "ipn."+consts[c])
}
}
want := "(" + strings.Join(names, " | ") + ")"
if got := all.String(); got != want {
t.Errorf("all.String() = %q, want %q", got, want)
}
})
}
func findNotifyWatchOptConstants(t *testing.T) map[NotifyWatchOpt]string {
t.Helper()
// Load the current package.
cfg := &packages.Config{
Mode: packages.NeedTypes,
}
pkgs, err := packages.Load(cfg, ".")
if err != nil {
t.Fatalf("failed to load packages: %v", err)
}
// Find all the [NotifyWatchOpt] constants that represent this enum.
var found map[NotifyWatchOpt]string
for _, pkg := range pkgs {
if len(pkg.Errors) > 0 {
t.Fatalf("package %s has errors: %v", pkg.Name, pkg.Errors)
}
wantType := pkg.Types.Path() + ".NotifyWatchOpt"
scope := pkg.Types.Scope()
for _, name := range scope.Names() {
obj := scope.Lookup(name)
if obj == nil || obj.Type().String() != wantType {
continue
}
c, ok := obj.(*types.Const)
if !ok {
continue
}
s := c.Val().ExactString()
val, err := strconv.ParseUint(s, 10, 64)
if err != nil {
t.Fatalf("cannot parse %q: %v", s, err)
}
mak.Set(&found, NotifyWatchOpt(val), name)
}
}
if len(found) == 0 {
t.Fatal("could not find NotifyWatchOpt constants")
}
return found
}