control/controlclient: filter out disco updates from full map (#19220)

When getting a full map from control, disco keys for the nodes will also
be delivered. When communicating with a peer that is running without
being connected to control, and having that connection running based on
a TSMP learned disco key, we need to avoid overwriting the disco key for
that peer with the stale one control knows about.

Add a filter that filteres out keys from control, and replace them with
the TSMP learned disco keys.

Updates #12639

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
Claus Lensbøl
2026-04-02 13:08:01 -04:00
committed by GitHub
parent e82ffe03ad
commit ffaebd71fb
2 changed files with 175 additions and 0 deletions
+60
View File
@@ -295,6 +295,7 @@ func (ms *mapSession) handleNonKeepAliveMapResponse(ctx context.Context, resp *t
ms.patchifyPeersChanged(resp)
ms.removeUnwantedDiscoUpdates(resp)
ms.removeUnwantedDiscoUpdatesFromFullNetmapUpdate(resp)
ms.updateStateFromResponse(resp)
@@ -404,6 +405,9 @@ type updateStats struct {
// where the node is offline and has last been seen before the recorded last seen.
func (ms *mapSession) removeUnwantedDiscoUpdates(resp *tailcfg.MapResponse) {
existingMap := ms.netmap()
if existingMap == nil {
return
}
acceptedDiscoUpdates := resp.PeersChangedPatch[:0]
for _, change := range resp.PeersChangedPatch {
@@ -442,6 +446,62 @@ func (ms *mapSession) removeUnwantedDiscoUpdates(resp *tailcfg.MapResponse) {
resp.PeersChangedPatch = acceptedDiscoUpdates
}
// removeUnwantedDiscoUpdatesFromFullNetmapUpdate makes a pass over the full
// set of peers in an update, usually only received when getting a full netmap
// from control at startup. If the pass finds a peer with a disco key where the
// local netmap has a newer key learned via TSMP, overwrite the update with the
// key from TSMP.
func (ms *mapSession) removeUnwantedDiscoUpdatesFromFullNetmapUpdate(resp *tailcfg.MapResponse) {
if len(resp.Peers) == 0 {
return
}
existingMap := ms.netmap()
if existingMap == nil {
return
}
for _, peer := range resp.Peers {
if peer.DiscoKey.IsZero() {
continue
}
// Accept if:
// - peer is new
peerIdx := existingMap.PeerIndexByNodeID(peer.ID)
if peerIdx < 0 {
continue
}
// Accept if:
// - disco key has not changed
existingNode := existingMap.Peers[peerIdx]
if existingNode.DiscoKey() == peer.DiscoKey {
continue
}
// Accept if:
// - key has changed but peer is online
if peer.Online != nil && *peer.Online {
continue
}
// Accept if:
// - there's no last seen on the existing node
existingLastSeen, ok := existingNode.LastSeen().GetOk()
if !ok {
continue
}
// Accept if:
// - last seen on on control is higher
if peer.LastSeen != nil && peer.LastSeen.After(existingLastSeen) {
continue
}
// Overwrite the key in the full netmap update.
peer.DiscoKey = existingNode.DiscoKey()
}
}
// updateStateFromResponse updates ms from res. It takes ownership of res.
func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
ms.updatePeersStateFromResponse(resp)