ipn/ipnlocal: add missing bus notify of peers on full netmap

The prior aa5da2e5f2 ("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 <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-05-27 08:03:47 -07:00
committed by Brad Fitzpatrick
parent 0e2b3f31af
commit a8f40a2ca5
2 changed files with 68 additions and 0 deletions
+19
View File
@@ -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),
+49
View File
@@ -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