diff --git a/ipn/backend.go b/ipn/backend.go index 51617e08e..32b38200a 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -90,7 +90,8 @@ const ( // NotifyPeerChanges, if set, causes netmap delta updates to be sent as [tailcfg.PeerChange] rather than a full NetMap. // Full netmap responses from the control plane are still sent as a full NetMap. PeerChanges are only sent to sessions - // that have opted in to this mode. + // that have opted in to this mode. Setting this flag also implies NotifyInitialNetMap, as the first message must + // contain a full netmap to establish the baseline for future peer changes. NotifyPeerChanges NotifyWatchOpt = 1 << 12 ) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 7432e33a7..d6aee5da1 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3296,7 +3296,7 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A b.mu.Lock() - const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode | ipn.NotifyInitialClientVersion + const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode | ipn.NotifyInitialClientVersion | ipn.NotifyPeerChanges if mask&initialBits != 0 { cn := b.currentNode() ini = &ipn.Notify{Version: version.Long()} @@ -3310,7 +3310,10 @@ 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 mask&(ipn.NotifyInitialNetMap|ipn.NotifyPeerChanges) != 0 { + // Include the initial NetMap if explicitly requested, or if the + // client subscribes to peer change deltas. Deltas are useless + // without a base netmap to apply them to. ini.NetMap = cn.NetMap() } if mask&ipn.NotifyInitialDriveShares != 0 && b.DriveSharingEnabled() { diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 70cbc8991..4409befbb 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -7642,6 +7642,69 @@ func unexpectedClientVersion(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool return false } +func TestWatchNotificationsPeerChangesImpliesInitialNetMap(t *testing.T) { + tests := []struct { + name string + mask ipn.NotifyWatchOpt + wantInitialNetMap bool + }{ + { + name: "with_notify_peer_changes", + mask: ipn.NotifyPeerChanges, + wantInitialNetMap: true, + }, + { + name: "with_notify_initial_state_only", + mask: ipn.NotifyInitialState, + wantInitialNetMap: false, + }, + { + name: "with_no_flags", + mask: 0, + wantInitialNetMap: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lb := newTestLocalBackend(t) + + testNetMap := &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + ID: 1, + Name: "self.tailscale.ts.net.", + }).View(), + } + lb.mu.Lock() + lb.setNetMapLocked(testNetMap) + lb.mu.Unlock() + + nw := newNotificationWatcher(t, lb, ipnauth.Self) + + if tt.wantInitialNetMap { + nw.watch(tt.mask, []wantedNotification{ + { + name: "InitialNetMap", + cond: func(_ testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool { + return n.NetMap != nil + }, + }, + }) + } else { + nw.watch(tt.mask, nil, func(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool { + if n.NetMap != nil { + t.Errorf("unexpected NetMap in initial notification without NotifyInitialNetMap or NotifyPeerChanges") + return true + } + return false + }) + } + + nw.check() + }) + } +} + func checkError(tb testing.TB, got, want error, fatal bool) { tb.Helper() f := tb.Errorf