ipn, ipn/ipnlocal: add Notify.SelfChange

Add a new bus signal that lets reactive consumers (containerboot, kube
agents, sniproxy, tsconsensus, etc.) react to self-node updates without
having to subscribe to the full netmap. Today those consumers either
watch Notify.NetMap (which on large tailnets is expensive to encode and
ship per watcher) or poll. SelfChange is a cheap, narrow alternative:
addresses, name, key expiry, capabilities, etc.

Consumers that need additional state can react to SelfChange and then
fetch the relevant bits on demand via existing LocalClient methods.

Producer-side, every netmap-bearing setControlClientStatus call now
also publishes SelfChange. Future changes will migrate individual
in-tree consumers off Notify.NetMap to this signal, and eventually
gate the legacy NetMap emission to platforms whose host GUIs still
require it.

Updates #12542

Change-Id: I4441650b0e085d663eb6bf26a03748b7d961ca49
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2026-04-30 21:12:42 +00:00 committed by Brad Fitzpatrick
parent 9f343fdc0c
commit a6c5d23742
4 changed files with 26 additions and 2 deletions

View File

@ -118,6 +118,17 @@ type Notify struct {
Prefs *PrefsView // if non-nil && Valid, the new or current preferences
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
// SelfChange, if non-nil, indicates that this node's own [tailcfg.Node]
// has changed: addresses, name, key expiry, capabilities, etc. It carries
// the new self node so reactive consumers (containerboot, kube agents,
// sniproxy, etc.) can read the current self state without watching the
// full netmap.
//
// Consumers that need additional state (peers, DNS config, packet
// filter) should react to SelfChange by fetching the relevant bits on
// demand via [LocalClient].
SelfChange *tailcfg.Node `json:",omitzero"`
// PeerChanges, if non-nil, is a list of [tailcfg.PeerChange] that have occurred since the last
// full netmap update. This is sent in lieu of a full NetMap when [NotifyPeerChanges] is set in
// the session's mask and a netmap update is derived from an incremental MapResponse.
@ -196,6 +207,9 @@ func (n Notify) String() string {
if n.NetMap != nil {
sb.WriteString("NetMap{...} ")
}
if n.SelfChange != nil {
fmt.Fprintf(&sb, "SelfChange(%v) ", n.SelfChange.StableID)
}
if n.PeerChanges != nil {
fmt.Fprintf(&sb, "PeerChanges(%d) ", len(n.PeerChanges))
}

View File

@ -205,6 +205,7 @@ func isNotableNotify(n *ipn.Notify) bool {
n.Prefs != nil ||
n.ErrMessage != nil ||
n.LoginFinished != nil ||
n.SelfChange != nil ||
!n.DriveShares.IsNil() ||
n.Health != nil ||
len(n.IncomingFiles) > 0 ||

View File

@ -32,6 +32,7 @@ func TestIsNotableNotify(t *testing.T) {
{"netmap", &ipn.Notify{NetMap: new(netmap.NetworkMap)}, false},
{"peerchanges", &ipn.Notify{PeerChanges: []*tailcfg.PeerChange{{}}}, false},
{"engine", &ipn.Notify{Engine: new(ipn.EngineStatus)}, false},
{"selfchange", &ipn.Notify{SelfChange: &tailcfg.Node{}}, true},
}
// Then for all other fields, assume they're notable.
@ -41,7 +42,7 @@ func TestIsNotableNotify(t *testing.T) {
for sf := range rt.Fields() {
n := &ipn.Notify{}
switch sf.Name {
case "_", "NetMap", "PeerChanges", "Engine", "Version":
case "_", "NetMap", "PeerChanges", "SelfChange", "Engine", "Version":
// Already covered above or not applicable.
continue
case "DriveShares":

View File

@ -1897,7 +1897,15 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c
// Update the DERP map in the health package, which uses it for health notifications
b.health.SetDERPMap(st.NetMap.DERPMap)
b.sendLocked(ipn.Notify{NetMap: st.NetMap})
// Notify watchers that the self node may have changed. Reactive
// consumers (containerboot, kube agents, sniproxy, etc.) listen on
// this signal and re-fetch peers/DNS via the LocalAPI if they need
// more than self info.
var selfChange *tailcfg.Node
if st.NetMap.SelfNode.Valid() {
selfChange = st.NetMap.SelfNode.AsStruct()
}
b.sendLocked(ipn.Notify{NetMap: st.NetMap, SelfChange: selfChange})
// The error here is unimportant as is the result. This will recalculate the suggested exit node
// cache the value and push any changes to the IPN bus.