ipn: enforce lossless IPN bus delta streams

New-style IPN bus subscribers consume stateful delta streams. Reject
NotifyRateLimit when it is combined with those subscription bits so
tailscaled cannot merge or delay messages that clients need to apply in
order.

Also stop silently dropping notifications when a watcher falls behind.
Remove the watcher, replace its stale queue with one terminal ErrMessage
notification, and close the watch.

Updates #20062

Change-Id: Id9d402ea76f4011cd23f122adf62f30dd4b6f90b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-06-09 11:12:20 -07:00
committed by Brad Fitzpatrick
parent 60b935e30f
commit edcc2c94d9
8 changed files with 206 additions and 25 deletions
+4
View File
@@ -902,6 +902,10 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
}
mask = ipn.NotifyWatchOpt(v)
}
if err := ipn.ValidateNotifyWatchOpt(mask); err != nil {
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). On platforms where
+12 -1
View File
@@ -634,6 +634,7 @@ func TestServeWatchIPNBus(t *testing.T) {
tests := []struct {
desc string
permitRead, permitWrite bool
mask ipn.NotifyWatchOpt
wantStatus int
}{
{
@@ -654,6 +655,12 @@ func TestServeWatchIPNBus(t *testing.T) {
permitWrite: true,
wantStatus: http.StatusOK,
},
{
desc: "invalid-rate-limit-mask",
permitRead: true,
mask: ipn.NotifyRateLimit | ipn.NotifyPeerChanges,
wantStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
@@ -668,7 +675,11 @@ func TestServeWatchIPNBus(t *testing.T) {
c := s.Client()
ctx, cancel := context.WithCancel(context.Background())
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/localapi/v0/watch-ipn-bus?mask=%d", s.URL, ipn.NotifyInitialState), nil)
mask := tt.mask
if mask == 0 {
mask = ipn.NotifyInitialState
}
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/localapi/v0/watch-ipn-bus?mask=%d", s.URL, mask), nil)
if err != nil {
t.Fatal(err)
}