diff --git a/ipn/backend.go b/ipn/backend.go index 3e956f473..9ee3bc651 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -81,6 +81,13 @@ const ( NotifyInitialHealthState NotifyWatchOpt = 1 << 7 // if set, the first Notify message (sent immediately) will contain the current health.State of the client NotifyRateLimit NotifyWatchOpt = 1 << 8 // if set, rate limit spammy netmap updates to every few seconds + + // NotifyOmitLegacyNetmap, if set, causes the backend to not send Notify + // mesesages with the [Notify.NetMap] field populated. See that field's docs + // for background. Clients should use NodeUpdate, UserUpdate, etc. + // At some point in the future (maybe a year or two from 2025-01-04?), this will + // become the default and only way. + NotifyOmitLegacyNetmap NotifyWatchOpt = 1 << 9 ) // Notify is a communication from a backend (e.g. tailscaled) to a frontend @@ -102,12 +109,23 @@ type Notify struct { // For State InUseOtherUser, ErrMessage is not critical and just contains the details. ErrMessage *string - LoginFinished *empty.Message // non-nil when/if the login process succeeded - State *State // if non-nil, the new or current IPN state - Prefs *PrefsView // if non-nil && Valid, the new or current preferences - NetMap *netmap.NetworkMap // if non-nil, the new or current netmap - Engine *EngineStatus // if non-nil, the new or current wireguard stats - BrowseToURL *string // if non-nil, UI should open a browser right now + LoginFinished *empty.Message // non-nil when/if the login process succeeded + State *State // if non-nil, the new or current IPN state + Prefs *PrefsView // if non-nil && Valid, the new or current preferences + Engine *EngineStatus // if non-nil, the new or current wireguard stats + BrowseToURL *string // if non-nil, UI should open a browser right now + + // NetMap, if non-nil, the new or current network map. + // + // If [NotifyOmitLegacyNetmap] is used on an WatchIPNBus subscription, this + // field is always nil. + // + // Deprecated: while sent by default (as of 2025-01-04), it is a colossal + // type and on its way out. With large networks with many peers and high + // churn, sending this non-stop to GUI clients uses a lot of CPU and + // bandwidth. It was a quadratic mistake from day 1; see + // tailscale/tailscale#1909 and tailscale/tailscale#13390. + NetMap *netmap.NetworkMap // if non-nil, the new or current netmap // FilesWaiting if non-nil means that files are buffered in // the Tailscale daemon and ready for local transfer to the @@ -153,9 +171,62 @@ type Notify struct { // any changes to the user in the UI. Health *health.State `json:",omitempty"` + // ResetNodesAndUsers, if true, means that all previously mentioned + // nodes and users should be forgotten. + // + // This is set to true when a new network map long poll to the control + // plane begins, but before any NodeUpdate or UserUpdate are sent. + ResetNodesAndUsers bool `json:",omitempty"` + + // NodeUpdate is a map of node updates. + // + // The values are always non-nil. + NodeUpdate map[tailcfg.StableNodeID]*NodeUpdate `json:",omitempty"` + + // UserUpdate is a map of user updates. + // + // The values are always non-nil. + UserUpdate map[tailcfg.UserID]*UserUpdate `json:",omitempty"` + // type is mirrored in xcode/IPN/Core/LocalAPI/Model/LocalAPIModel.swift } +// NodeUpdate describes a change to a node in the network map. +type NodeUpdate struct { + // NodeID is the integer form of the Node ID being updated or deleted. + NodeID tailcfg.NodeID + + // Deleted is true if the node has been deleted from the network map. + Deleted bool `json:",omitempty"` + + // IsSelf is true if this node is the current node. + // False or omitted means it's a peer. + IsSelf bool `json:",omitempty"` + + // New is the new node information, if the node has been updated. + // It maybe omitted for delta updates, depending on the type of update + // and the client's WatchIPNBus subscription options. + New *tailcfg.Node `json:",omitempty"` + + // Patch, if non-nil, describes the minimal version of the change made to the node. + // When Patch and New are both non-nil, the New field represents the state + // after the Patch has been applied. + Patch *tailcfg.PeerChange `json:",omitempty"` +} + +// UserUpdate describes a change to a user in the network map. +type UserUpdate struct { + UserID tailcfg.UserID + + // Deleted is true if the user has been deleted from the network map. + // That is, the user is no longer referenced by any node in the network map. + Deleted bool `json:",omitempty"` + + // Profile is the new user profile, if the user has been newly added or + // updated. If Delete is true, this is omitted. + Profile *tailcfg.UserProfile `json:",omitempty"` +} + func (n Notify) String() string { var sb strings.Builder sb.WriteString("Notify{") diff --git a/ipn/ipnlocal/bus.go b/ipn/ipnlocal/bus.go index 111a877d8..0b435b7be 100644 --- a/ipn/ipnlocal/bus.go +++ b/ipn/ipnlocal/bus.go @@ -154,6 +154,9 @@ func isNotableNotify(n *ipn.Notify) bool { n.LoginFinished != nil || !n.DriveShares.IsNil() || n.Health != nil || + n.ResetNodesAndUsers || + n.NodeUpdate != nil || + n.UserUpdate != nil || len(n.IncomingFiles) > 0 || len(n.OutgoingFiles) > 0 || n.FilesWaiting != nil diff --git a/ipn/ipnlocal/bus_test.go b/ipn/ipnlocal/bus_test.go index 5c75ac54d..74610200d 100644 --- a/ipn/ipnlocal/bus_test.go +++ b/ipn/ipnlocal/bus_test.go @@ -12,6 +12,7 @@ import ( "tailscale.com/drive" "tailscale.com/ipn" + "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/tstime" "tailscale.com/types/logger" @@ -45,6 +46,10 @@ func TestIsNotableNotify(t *testing.T) { continue case "DriveShares": n.DriveShares = views.SliceOfViews[*drive.Share, drive.ShareView](make([]*drive.Share, 1)) + case "NodeUpdate": + n.NodeUpdate = map[tailcfg.StableNodeID]*ipn.NodeUpdate{} + case "UserUpdate": + n.UserUpdate = map[tailcfg.UserID]*ipn.UserUpdate{} default: rf := reflect.ValueOf(n).Elem().Field(i) switch rf.Kind() { @@ -54,6 +59,8 @@ func TestIsNotableNotify(t *testing.T) { rf.SetString("foo") case reflect.Slice: rf.Set(reflect.MakeSlice(rf.Type(), 1, 1)) + case reflect.Bool: + rf.SetBool(true) default: t.Errorf("unhandled field kind %v for %q", rf.Kind(), sf.Name) }