From d1ab26388c0b2b6da6dffc27095f71826bdc1250 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 22 Oct 2026 00:07:33 +0000 Subject: [PATCH] change: do not echo a node's own report back to it An endpoint or DERP report came back to the node as a self update; SaaS sends it to the node's peers only. --- hscontrol/mapper/batcher_concurrency_test.go | 2 +- hscontrol/servertest/mapstream_compat_test.go | 5 ----- hscontrol/state/state.go | 2 +- hscontrol/types/change/change.go | 13 ++++++------- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/hscontrol/mapper/batcher_concurrency_test.go b/hscontrol/mapper/batcher_concurrency_test.go index 5b80268cc..b6b2f5991 100644 --- a/hscontrol/mapper/batcher_concurrency_test.go +++ b/hscontrol/mapper/batcher_concurrency_test.go @@ -274,7 +274,7 @@ func TestAddToBatchDifferential(t *testing.T) { lb.b.nodes.Store(99, nil) } - endpoint := change.EndpointOrDERPUpdate(1, &tailcfg.PeerChange{ + endpoint := change.EndpointOrDERPUpdate(&tailcfg.PeerChange{ NodeID: 1, DERPRegion: 7, Online: new(true), }) ping := change.PingNode(2, &tailcfg.PingRequest{URL: "https://example.com/one"}) diff --git a/hscontrol/servertest/mapstream_compat_test.go b/hscontrol/servertest/mapstream_compat_test.go index 39d3b2fd0..ed7d5bf36 100644 --- a/hscontrol/servertest/mapstream_compat_test.go +++ b/hscontrol/servertest/mapstream_compat_test.go @@ -59,10 +59,6 @@ var mapStreamSkipReasons = map[string]string{ "map-delivery/S3.5-remove/db1/frames": policyResponseToEveryNode, "map-delivery/S3.5-restore/db1/frames": policyResponseToEveryNode, - "map-delivery/S2.1/laptop/frames": ownReportEchoed, - "map-delivery/S2.3/laptop/frames": ownReportEchoed, - "map-delivery/S2.5/laptop/self-endpoints": ownReportEchoed, - "map-delivery/S2.5/laptop/endpoints-gap-bystander": restartEndpointsKept, "map-delivery/S2.5/laptop/endpoints-gap-svc": restartEndpointsKept, @@ -121,7 +117,6 @@ var mapStreamSkipReasons = map[string]string{ const ( policyResponseToEveryNode = "POLICY_RESPONSE_TO_EVERY_NODE: headscale sends every connected node a policy response when the policy, a node's tags or approved routes, or the set of nodes change; SaaS sends only the nodes whose peers or filter change" - ownReportEchoed = "OWN_REPORT_ECHOED: headscale sends a node its own endpoint or DERP report back; SaaS sends it nothing, so its own entry keeps the endpoints of its last full map" restartEndpointsKept = "RESTART_ENDPOINTS_KEPT: headscale keeps a restarted node's last endpoints at its peers until it reports new ones; SaaS sends peers the node without endpoints until then" reconnectAnnounced = "RECONNECT_ANNOUNCED: headscale tells peers a node is online after a stream gap of a few seconds; SaaS sends them nothing" routerReconnectRecompute = "ROUTER_RECONNECT_RECOMPUTE: headscale sends every node a policy response when a subnet router's stream comes back after a few seconds; SaaS sends nothing" diff --git a/hscontrol/state/state.go b/hscontrol/state/state.go index 82554865f..2127e2700 100644 --- a/hscontrol/state/state.go +++ b/hscontrol/state/state.go @@ -4207,7 +4207,7 @@ func buildMapRequestChangeResponse( patch.DERPRegion = currentDERP } - return change.EndpointOrDERPUpdate(id, patch) + return change.EndpointOrDERPUpdate(patch) } return change.Change{} diff --git a/hscontrol/types/change/change.go b/hscontrol/types/change/change.go index d9053b3fd..4e1561ffc 100644 --- a/hscontrol/types/change/change.go +++ b/hscontrol/types/change/change.go @@ -595,13 +595,12 @@ func KeyExpiryFor(id types.NodeID, expiry time.Time) Change { return c } -// EndpointOrDERPUpdate returns a [Change] for when a node's endpoints or DERP region changes. -// The [Change.OriginNode] field enables self-update detection by the mapper. -func EndpointOrDERPUpdate(id types.NodeID, patch *tailcfg.PeerChange) Change { - c := PeerPatched("endpoint/DERP update", patch) - c.OriginNode = id - - return c +// EndpointOrDERPUpdate returns a [Change] for when a node reports new +// endpoints or a new DERP region. It has no [Change.OriginNode]: the node +// already knows what it reported, so, as on Tailscale SaaS, only its peers +// are sent the patch. +func EndpointOrDERPUpdate(patch *tailcfg.PeerChange) Change { + return PeerPatched("endpoint/DERP update", patch) } // NodeKeyRotated returns a [Change] for a node re-logging in: its NodeKey (and