From a8f40a2ca57c4d8dde4f3122c63a9822abc3a131 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 27 May 2026 14:50:31 +0000 Subject: [PATCH] ipn/ipnlocal: add missing bus notify of peers on full netmap The prior aa5da2e5f22a78 ("process node adds/removes in constant time") commit missed a bus notification case, where new-style subscribers set NotifyNoNetmap and then the controlclient map routing sends a full update (rather than a delta). Those profiles + peers need to be put on the bus too. I noticed this only when porting the Android app over to use the new bus stuff. Updates #19607 Updates #12542 Change-Id: I82c35011d2c532222ca27f7d4e790522c31bd156 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 19 +++++++++++++++ ipn/ipnlocal/local_test.go | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index a49e1296a..9b4f6ae67 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1998,6 +1998,13 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c selfChange = st.NetMap.SelfNode.AsStruct() } notify := ipn.Notify{SelfChange: selfChange} + if b.hasPeerChangeWatcherLocked() { + notify.UserProfiles = st.NetMap.UserProfiles + notify.PeersChanged = make([]*tailcfg.Node, 0, len(st.NetMap.Peers)) + for _, p := range st.NetMap.Peers { + notify.PeersChanged = append(notify.PeersChanged, p.AsStruct()) + } + } if goosGetsLegacyNetmapNotify { notify.NetMap = st.NetMap } @@ -3887,6 +3894,18 @@ func (b *LocalBackend) notifyForSessionLocked(sess *watchSession, n *ipn.Notify) return &nCopy } +// hasPeerChangeWatcherLocked reports whether any active watcher wants peer-set +// notifications. b.mu must be held. +func (b *LocalBackend) hasPeerChangeWatcherLocked() bool { + syncs.AssertLocked(&b.mu) + for _, sess := range b.notifyWatchers { + if sess.mask&(ipn.NotifyPeerChanges|ipn.NotifyPeerPatches) != 0 { + return true + } + } + return false +} + // setAuthURLLocked sets the authURL and triggers [LocalBackend.popBrowserAuthNow] if the URL has changed. // This method is called when a new authURL is received from the control plane, meaning that either a user // has started a new interactive login (e.g., by running `tailscale login` or clicking Login in the GUI), diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 7a1cd1230..3c31a1dd6 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -2184,6 +2184,55 @@ func TestNotifyForSessionPeerVisibility(t *testing.T) { }) } +func TestSetControlClientStatusSendsFullNetmapAsPeerChanges(t *testing.T) { + b := newTestLocalBackend(t) + nw := newNotificationWatcher(t, b, ipnauth.Self) + nw.watch(ipn.NotifyPeerChanges|ipn.NotifyNoNetMap, []wantedNotification{{ + name: "full netmap as peer changes", + cond: func(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool { + 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 + } + if got, want := len(n.UserProfiles), 3; got != want { + t.Errorf("UserProfiles len = %d; want %d", got, want) + return false + } + gotPeers := set.Of(n.PeersChanged[0].ID, n.PeersChanged[1].ID) + wantPeers := set.Of(tailcfg.NodeID(10), tailcfg.NodeID(20)) + if !gotPeers.Equal(wantPeers) { + t.Errorf("PeersChanged IDs = %v; want %v", gotPeers, wantPeers) + } + return true + }, + }}) + + nm := &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + ID: 1, + User: 1, + Key: makeNodeKeyFromID(1), + }).View(), + Peers: []tailcfg.NodeView{ + (&tailcfg.Node{ID: 10, User: 2, Key: makeNodeKeyFromID(10)}).View(), + (&tailcfg.Node{ID: 20, User: 3, Key: makeNodeKeyFromID(20)}).View(), + }, + UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ + 1: (&tailcfg.UserProfile{ID: 1, LoginName: "self@example.com"}).View(), + 2: (&tailcfg.UserProfile{ID: 2, LoginName: "peer1@example.com"}).View(), + 3: (&tailcfg.UserProfile{ID: 3, LoginName: "peer2@example.com"}).View(), + }, + } + b.SetControlClientStatus(b.cc, controlclient.Status{NetMap: nm, LoggedIn: true}) + nw.check() +} + // TestNotifyForSessionUserProfilesGating verifies that // [Notify.UserProfiles] is only delivered to sessions opted in to // NotifyPeerChanges/NotifyPeerPatches, and is deduped per-UserID