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
This commit is contained in:
Brad Fitzpatrick authored and Brad Fitzpatrick committed 2026-09-14 09:37:30 -07:00
1 parent 1b0f7b3c6c
commit ab4262680b
13 files changed
+191 -94

No files matched your search

+1 -1
View File
@@ -139,7 +139,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/key from tailscale.com/client/local+
tailscale.com/types/lazy from tailscale.com/version+
tailscale.com/types/logger from tailscale.com/cmd/derper+
tailscale.com/types/netmap from tailscale.com/ipn+
tailscale.com/types/netmap from tailscale.com/net/routecheck
tailscale.com/types/opt from tailscale.com/envknob+
tailscale.com/types/persist from tailscale.com/ipn+
tailscale.com/types/preftype from tailscale.com/ipn
+10 -8
View File
@@ -138,10 +138,9 @@ func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, pro
}
// Finally, start mainloop to configure app connector based on information
// in the self node's CapMap. We set NotifyInitialNetMap so the first
// Notify carries the current self node (now via Notify.SelfChange);
// subsequent self changes wake us up too.
bus, err := lc.WatchIPNBus(ctx, ipn.NotifyWatchEngineUpdates|ipn.NotifyInitialNetMap)
// in the self node's CapMap. The initial status carries the current
// self node; subsequent self changes arrive via Notify.SelfChange.
bus, err := lc.WatchIPNBus(ctx, ipn.NotifyWatchEngineUpdates|ipn.NotifyInitialStatus)
if err != nil {
log.Fatalf("watching IPN bus: %v", err)
}
@@ -155,13 +154,16 @@ func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, pro
log.Fatalf("reading IPN bus: %v", err)
}
self := msg.SelfChange
if self == nil {
var capMap tailcfg.NodeCapMap
if self := msg.SelfChange; self != nil {
capMap = self.CapMap
} else if st := msg.InitialStatus; st != nil && st.Self != nil {
capMap = st.Self.CapMap
} else {
continue
}
var c appctype.AppConnectorConfig
// View() lets us reuse the existing CapView decoder.
nmConf, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.AppConnectorConfig](self.View().CapMap(), configCapKey)
nmConf, err := tailcfg.UnmarshalNodeCapJSON[appctype.AppConnectorConfig](capMap, configCapKey)
if err != nil {
log.Printf("failed to read app connector configuration from coordination server: %v", err)
} else if len(nmConf) > 0 {
+1 -1
View File
@@ -275,7 +275,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/key from tailscale.com/client/local+
tailscale.com/types/lazy from tailscale.com/util/testenv+
tailscale.com/types/logger from tailscale.com/client/web+
tailscale.com/types/netmap from tailscale.com/ipn+
tailscale.com/types/netmap from tailscale.com/net/routecheck
tailscale.com/types/nettype from tailscale.com/net/netcheck+
tailscale.com/types/opt from tailscale.com/client/tailscale+
tailscale.com/types/persist from tailscale.com/ipn+
+42 -34
View File
@@ -17,7 +17,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/structs"
"tailscale.com/types/views"
"tailscale.com/util/syspolicy/policyclient"
@@ -86,9 +85,19 @@ const (
// each one via Engine.RequestStatus.
NotifyWatchEngineUpdates NotifyWatchOpt = 1 << 0
NotifyInitialState NotifyWatchOpt = 1 << 1 // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL + SessionID
NotifyInitialPrefs NotifyWatchOpt = 1 << 2 // if set, the first Notify message (sent immediately) will contain the current Prefs
NotifyInitialNetMap NotifyWatchOpt = 1 << 3 // if set, the first Notify message (sent immediately) will contain the current NetMap
NotifyInitialState NotifyWatchOpt = 1 << 1 // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL + SessionID
NotifyInitialPrefs NotifyWatchOpt = 1 << 2 // if set, the first Notify message (sent immediately) will contain the current Prefs
// ObsoleteNotifyInitialNetMap is a formerly valid bit (previously named
// NotifyInitialNetMap) that asked for the first Notify message to
// carry the full netmap in the since-removed Notify.NetMap field.
// Subscription requests that set it are now rejected by
// [ValidateNotifyWatchOpt]. Watchers seed their view from
// [NotifyInitialStatus] instead, and those that need more than the
// status fetch what they need via other LocalAPI methods. The bit
// value remains reserved so it is never reused with a different
// meaning.
ObsoleteNotifyInitialNetMap NotifyWatchOpt = 1 << 3
NotifyNoPrivateKeys NotifyWatchOpt = 1 << 4 // (no-op) it used to redact private keys; now they always are and this does nothing
NotifyInitialDriveShares NotifyWatchOpt = 1 << 5 // if set, the first Notify message (sent immediately) will contain the current Taildrive Shares
@@ -128,27 +137,29 @@ const (
// observes every per-peer mutation; it just receives them as full
// Nodes rather than narrow patches. The cost is bus bandwidth.
//
// It is permitted to combine this with [NotifyInitialNetMap] for
// backwards compatibility. New code should pair this with
// [NotifyInitialStatus] instead.
// Watchers typically pair this with [NotifyInitialStatus] to seed
// their initial state.
NotifyPeerChanges NotifyWatchOpt = 1 << 12
// NotifyNoNetMap historically suppressed the legacy [Notify.NetMap]
// NotifyNoNetMap historically suppressed the legacy Notify.NetMap
// field on runtime (non-initial) Notify messages on platforms where
// tailscaled still emitted it by default. tailscaled no longer emits
// NetMap on runtime messages on any platform, so this bit is now a
// no-op. It remains accepted for compatibility with clients that set
// it. The initial-state NetMap (sent when [NotifyInitialNetMap] is
// set) was never affected by this bit.
// tailscaled still emitted it by default. That field no longer
// exists, so this bit is now a no-op. It remains accepted for
// compatibility with clients that set it.
NotifyNoNetMap NotifyWatchOpt = 1 << 13
// NotifyInitialStatus, if set, causes the first Notify message (sent
// immediately) to contain the current [ipnstate.Status] in
// [Notify.InitialStatus]. Together with [Notify.SelfChange] and
// [Notify.PeersChanged] on subsequent messages, it lets a watcher
// stitch together a continuous view of the local node's state without
// fetching the netmap directly. Prefer this over [LocalClient.NetMap]
// for new code that wants a stable, client-facing snapshot type.
// stitch together a continuous view of the local node's state as a
// stable, client-facing snapshot type, without needing the netmap.
//
// The status is sized to the subscription: its per-peer entries
// (Status.Peer) are only populated if the watcher also set
// [NotifyPeerChanges] or [NotifyPeerPatches], since building them is
// O(peers) and only peer-delta subscribers need a peer baseline.
// Self-only watchers get Status.Self and the scalar fields.
NotifyInitialStatus NotifyWatchOpt = 1 << 14
// NotifyPeerPatches, if set, opts the watcher into narrow per-field
@@ -226,7 +237,7 @@ func (o NotifyWatchOpt) String() string {
try(NotifyWatchEngineUpdates, "NotifyWatchEngineUpdates")
try(NotifyInitialState, "NotifyInitialState")
try(NotifyInitialPrefs, "NotifyInitialPrefs")
try(NotifyInitialNetMap, "NotifyInitialNetMap")
try(ObsoleteNotifyInitialNetMap, "ObsoleteNotifyInitialNetMap")
try(NotifyNoPrivateKeys, "NotifyNoPrivateKeys")
try(NotifyInitialDriveShares, "NotifyInitialDriveShares")
try(NotifyInitialOutgoingFiles, "NotifyInitialOutgoingFiles")
@@ -283,6 +294,9 @@ func ValidateNotifyWatchOpt(mask NotifyWatchOpt) error {
if mask&ObsoleteNotifyRateLimit != 0 {
return errors.New("the NotifyRateLimit IPN bus subscription bit is no longer supported")
}
if mask&ObsoleteNotifyInitialNetMap != 0 {
return errors.New("the NotifyInitialNetMap IPN bus subscription bit is no longer supported; seed from NotifyInitialStatus instead")
}
return nil
}
@@ -318,9 +332,15 @@ type Notify struct {
// sniproxy, etc.) can read the current self state without watching the
// full netmap.
//
// Consumers that need additional state (peers, DNS config, packet
// filter) should react to SelfChange by fetching the full netmap on
// demand via [LocalClient.NetMap].
// Runtime self changes are not gated by any subscription bit: every
// watcher receives them. There is no initial SelfChange; watchers
// that need the current self node at the start of a session should
// set [NotifyInitialStatus] and seed from InitialStatus.Self.
//
// Consumers that need additional state (peers, DNS config, etc.)
// should react to SelfChange by fetching what they need via other
// LocalAPI methods, such as LocalClient.Status or
// LocalClient.DNSConfig.
SelfChange *tailcfg.Node `json:",omitzero"`
// InitialStatus, if non-nil, is the current [ipnstate.Status]. It is
@@ -330,18 +350,6 @@ type Notify struct {
// a continuous view of node state without fetching the netmap.
InitialStatus *ipnstate.Status `json:",omitzero"`
// NetMap, if non-nil, is the full network map. It is only delivered
// on the initial Notify of a session, and only when the watcher
// requested [NotifyInitialNetMap]; it is always nil on subsequent
// messages. New consumers should prefer [LocalClient.NetMap] for
// one-shot fetches and [Notify.SelfChange] / [Notify.PeerChanges]
// for incremental reactive updates.
//
// Deprecated: this field is slated for removal in favor of
// [Notify.InitialStatus] + [Notify.SelfChange] /
// [Notify.PeerChanges], etc, as this field doesn't scale.
NetMap *netmap.NetworkMap
// PeerChangedPatch, if non-empty, lists narrow per-field peer patches
// since the last Notify (currently Online, LastSeen, DERPHome,
// Endpoints). It mirrors [tailcfg.MapResponse.PeersChangedPatch].
@@ -392,8 +400,8 @@ type Notify struct {
// The producer guarantees that any UserID referenced by a peer in
// a [Notify.PeersChanged] / [Notify.PeerChangedPatch] entry will
// have its profile delivered either earlier on this same session
// (e.g. via the initial NetMap or via an earlier Notify carrying
// UserProfiles) or in this same Notify. A consumer that sees a
// (via an earlier Notify carrying UserProfiles) or in this same
// Notify. A consumer that sees a
// UserID it doesn't recognize on a session that opted in to
// peer-change notifications can treat it as a bug; the
// [LocalClient.UserProfile] LocalAPI fallback exists for sessions
+5
View File
@@ -110,6 +110,11 @@ func TestValidateNotifyWatchOpt(t *testing.T) {
mask: ObsoleteNotifyRateLimit | NotifyPeerChanges,
wantErr: true,
},
{
name: "obsolete-initial-netmap",
mask: ObsoleteNotifyInitialNetMap,
wantErr: true,
},
}
for _, tt := range tests {
+9 -14
View File
@@ -2071,7 +2071,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c
// Notify watchers that the self node may have changed. Reactive
// consumers (containerboot, kube agents, sniproxy, etc.) listen on
// this signal and re-fetch peers/DNS via [LocalClient.NetMap] if
// this signal and re-fetch peers/DNS via other LocalAPI methods if
// they need more than self info.
var selfChange *tailcfg.Node
if st.NetMap.SelfNode.Valid() {
@@ -3741,7 +3741,13 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
// channel before InitialStatus is delivered.
var statusSB *ipnstate.StatusBuilder
if mask&ipn.NotifyInitialStatus != 0 {
statusSB = &ipnstate.StatusBuilder{WantPeers: true}
// The initial status is sized to the subscription: building the
// per-peer status entries is O(peers), so only do it for watchers
// that subscribed to peer deltas and thus need a peer baseline to
// apply them to. Self-only watchers get Status.Self and the
// scalar fields.
wantPeers := mask&(ipn.NotifyPeerChanges|ipn.NotifyPeerPatches) != 0
statusSB = &ipnstate.StatusBuilder{WantPeers: wantPeers}
b.e.UpdateStatus(statusSB)
}
if mask&ipn.NotifyPeerWireGuardState != 0 {
@@ -3756,11 +3762,10 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
var policyUID string
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs |
ipn.NotifyInitialNetMap | ipn.NotifyInitialStatus |
ipn.NotifyInitialStatus |
ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode |
ipn.NotifyInitialClientVersion | ipn.NotifySysPolicyChanges | ipn.NotifyPeerWireGuardState
if mask&initialBits != 0 {
cn := b.currentNode()
ini = &ipn.Notify{Version: version.Long()}
if mask&ipn.NotifyInitialState != 0 {
ini.SessionID = sessionID
@@ -3772,16 +3777,6 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
if mask&ipn.NotifyInitialPrefs != 0 {
ini.Prefs = new(b.sanitizedPrefsLocked())
}
if mask&ipn.NotifyInitialNetMap != 0 {
if nm := cn.NetMap(); nm != nil && nm.SelfNode.Valid() {
ini.SelfChange = nm.SelfNode.AsStruct()
}
// The legacy initial NetMap is delivered cross-platform: it
// is what watchers asked for by setting NotifyInitialNetMap
// and is always a one-shot, so the cost of building it is
// paid once per bus subscription.
ini.NetMap = cn.netMapWithPeers()
}
if statusSB != nil {
b.updateStatusLocked(statusSB)
ini.InitialStatus = statusSB.Status()
+54 -3
View File
@@ -2470,9 +2470,6 @@ func TestSetControlClientStatusSendsFullNetmapAsPeerChanges(t *testing.T) {
if n.SelfChange == nil {
return false
}
if n.NetMap != nil {
t.Errorf("NetMap was delivered to NotifyNoNetMap watcher")
}
if got, want := len(n.PeersChanged), 2; got != want {
t.Errorf("PeersChanged len = %d; want %d", got, want)
return false
@@ -2510,6 +2507,55 @@ func TestSetControlClientStatusSendsFullNetmapAsPeerChanges(t *testing.T) {
nw.check()
}
// TestWatchNotificationsInitialStatusPeers verifies that the initial
// status is sized to the subscription: Status.Peer entries are only
// populated for watchers that subscribed to peer deltas, while
// Status.Self is populated either way.
func TestWatchNotificationsInitialStatusPeers(t *testing.T) {
tests := []struct {
name string
mask ipn.NotifyWatchOpt
wantPeers bool
}{
{"self-only", ipn.NotifyInitialStatus, false},
{"peer-changes", ipn.NotifyInitialStatus | ipn.NotifyPeerChanges, true},
{"peer-patches", ipn.NotifyInitialStatus | ipn.NotifyPeerPatches, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestLocalBackend(t)
b.currentNode().SetNetMap(&netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
ID: 1,
User: 1,
Key: makeNodeKeyFromID(1),
}).View(),
Peers: []tailcfg.NodeView{
(&tailcfg.Node{ID: 10, User: 1, Key: makeNodeKeyFromID(10)}).View(),
},
})
nw := newNotificationWatcher(t, b, ipnauth.Self)
nw.watch(tt.mask, []wantedNotification{{
name: "initial status",
cond: func(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
if n.InitialStatus == nil {
return false
}
if n.InitialStatus.Self == nil {
t.Errorf("InitialStatus.Self = nil; want non-nil")
}
if got := len(n.InitialStatus.Peer); (got > 0) != tt.wantPeers {
t.Errorf("len(InitialStatus.Peer) = %d; wantPeers = %v", got, tt.wantPeers)
}
return true
},
}})
nw.check()
})
}
}
type expiryCallbackClock struct {
tstime.StdClock
now time.Time
@@ -5248,6 +5294,11 @@ func TestDriveManageShares(t *testing.T) {
0,
func() { wg.Done() },
func(n *ipn.Notify) bool {
if n.DriveShares.IsNil() {
// Skip unrelated notifications, such as the
// initial SelfChange sent to every watcher.
return true
}
select {
case result <- n.DriveShares:
default:
+2 -2
View File
@@ -154,8 +154,8 @@ type nodeBackend struct {
// by mergeUserProfiles as deltas arrive. It parallels the peers map:
// netMap.UserProfiles is the frozen snapshot from the last full install,
// while this field reflects incremental updates. Readers that need a
// snapshot (e.g. the initial-Notify NetMap or [LocalClient.NetMap]
// paths) must clone this map.
// snapshot (e.g. [LocalBackend.NetMapWithPeers] callers such as the
// c2n and debug netmap handlers) must clone this map.
userProfiles map[tailcfg.UserID]tailcfg.UserProfileView
// packetFilterRules and packetFilter are the live packet filter state,
-4
View File
@@ -910,10 +910,6 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// NotifyInitialNetMap is permitted alongside NotifyPeerChanges /
// NotifyPeerPatches for backwards compatibility with clients that
// set both (e.g. the Apple client). The initial netmap is a one-shot;
// peer-change subscribers simply receive deltas after that point.
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
+5 -4
View File
@@ -207,10 +207,11 @@ func (cm *CertManager) runCertLoop(ctx context.Context, domain string) {
}
// domains before issuing the cert for the first time. It uses the IPN bus
// only as a wake-up trigger (Notify.SelfChange) and queries the current
// cert domains explicitly via [LocalClient.CertDomains].
// only as a wake-up trigger (the initial status and subsequent
// Notify.SelfChange messages) and queries the current cert domains
// explicitly via [LocalClient.CertDomains].
func (cm *CertManager) waitForCertDomain(ctx context.Context, domain string) error {
w, err := cm.lc.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
w, err := cm.lc.WatchIPNBus(ctx, ipn.NotifyInitialStatus)
if err != nil {
return fmt.Errorf("error watching IPN bus: %w", err)
}
@@ -221,7 +222,7 @@ func (cm *CertManager) waitForCertDomain(ctx context.Context, domain string) err
if err != nil {
return err
}
if n.SelfChange == nil {
if n.SelfChange == nil && n.InitialStatus == nil {
continue
}
domains, err := cm.lc.CertDomains(ctx)
+3 -1
View File
@@ -60,7 +60,7 @@ func (h *Healthz) Update(healthy bool) {
}
func (h *Healthz) MonitorHealth(ctx context.Context, lc *local.Client) error {
w, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
w, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialStatus)
if err != nil {
return fmt.Errorf("failed to watch IPN bus: %w", err)
}
@@ -73,6 +73,8 @@ func (h *Healthz) MonitorHealth(ctx context.Context, lc *local.Client) error {
if self := n.SelfChange; self != nil {
h.Update(len(self.Addresses) != 0)
} else if st := n.InitialStatus; st != nil && st.Self != nil {
h.Update(len(st.Self.TailscaleIPs) != 0)
}
}
}
+58 -21
View File
@@ -14,12 +14,14 @@ import (
"context"
"encoding/json"
"fmt"
"net/netip"
"slices"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/kube/kubetypes"
klc "tailscale.com/kube/localclient"
"tailscale.com/tailcfg"
"tailscale.com/util/deephash"
)
const (
@@ -44,43 +46,39 @@ func SetInitialKeys(store ipn.StateStore, podUID string) error {
// KeepKeysUpdated sets state store keys consistent with containerboot to
// signal proxy readiness to the operator. It runs until its context is
// cancelled or it hits an error. It watches the IPN bus for SelfChange
// notifications (which fire whenever the self node changes) and reads
// the new self node directly from the notify.
// cancelled or it hits an error. It seeds the self node from the initial
// status and then watches the IPN bus for SelfChange notifications, which
// fire whenever the self node changes.
func KeepKeysUpdated(ctx context.Context, store ipn.StateStore, lc klc.LocalClient) error {
w, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
w, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialStatus)
if err != nil {
return fmt.Errorf("error watching IPN bus: %w", err)
}
defer w.Close()
var currentDeviceID, currentDeviceIPs, currentDeviceFQDN deephash.Sum
for {
n, err := w.Next() // Blocks on a streaming LocalAPI HTTP call.
if err != nil {
if err == ctx.Err() {
return nil
}
return err
}
self := n.SelfChange
if self == nil {
continue
}
var prevDeviceID tailcfg.StableNodeID
var prevFQDN string
var prevAddrs []netip.Prefix
if deviceID := self.StableID; deephash.Update(&currentDeviceID, &deviceID) {
// storeSelf writes the device ID, FQDN, and IP state keys derived
// from the given self node, skipping any whose value is unchanged
// since the last write.
storeSelf := func(self tailcfg.NodeView) error {
if deviceID := self.StableID(); deviceID != prevDeviceID {
if err := store.WriteState(keyDeviceID, []byte(deviceID)); err != nil {
return fmt.Errorf("failed to store device ID in state: %w", err)
}
prevDeviceID = deviceID
}
if fqdn := self.Name; deephash.Update(&currentDeviceFQDN, &fqdn) {
if fqdn := self.Name(); fqdn != prevFQDN {
if err := store.WriteState(keyDeviceFQDN, []byte(fqdn)); err != nil {
return fmt.Errorf("failed to store device FQDN in state: %w", err)
}
prevFQDN = fqdn
}
if addrs := self.Addresses; deephash.Update(&currentDeviceIPs, &addrs) {
if addrs := self.Addresses().AsSlice(); !slices.Equal(addrs, prevAddrs) {
var deviceIPs []string
for _, addr := range addrs {
deviceIPs = append(deviceIPs, addr.Addr().String())
@@ -92,6 +90,45 @@ func KeepKeysUpdated(ctx context.Context, store ipn.StateStore, lc klc.LocalClie
if err := store.WriteState(keyDeviceIPs, deviceIPsValue); err != nil {
return fmt.Errorf("failed to store device IPs in state: %w", err)
}
prevAddrs = addrs
}
return nil
}
for {
n, err := w.Next() // Blocks on a streaming LocalAPI HTTP call.
if err != nil {
if err == ctx.Err() {
return nil
}
return err
}
var self tailcfg.NodeView
switch {
case n.SelfChange != nil:
self = n.SelfChange.View()
case n.InitialStatus != nil && n.InitialStatus.Self != nil:
self = selfNodeFromPeerStatus(n.InitialStatus.Self)
default:
continue
}
if err := storeSelf(self); err != nil {
return err
}
}
}
// selfNodeFromPeerStatus converts the subset of ps that KeepKeysUpdated
// reads into a [tailcfg.NodeView], so the initial status seed and
// subsequent [ipn.Notify.SelfChange] updates can share one code path.
func selfNodeFromPeerStatus(ps *ipnstate.PeerStatus) tailcfg.NodeView {
n := &tailcfg.Node{
StableID: ps.ID,
Name: ps.DNSName,
}
for _, ip := range ps.TailscaleIPs {
n.Addresses = append(n.Addresses, netip.PrefixFrom(ip, ip.BitLen()))
}
return n.View()
}
+1 -1
View File
@@ -1182,7 +1182,7 @@ func setUpServiceState(t *testing.T, name, ip string, host, client *Server,
}
waitForLatestNetmap := func(t *testing.T, s *Server) {
t.Helper()
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialNetMap))
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialStatus))
defer w.Close()
for {
must.Get(w.Next())