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
+115
View File
@@ -743,6 +743,7 @@ func TestUpdateDiscoForNodeCallback(t *testing.T) {
nu.lastTSMPKey, nu.lastTSMPDisco)
}
})
// Even though key stays in list of update, the updater only triggers on TSMP.
t.Run("key_not_wired_through_to_updater", func(t *testing.T) {
nu := &rememberLastNetmapUpdater{
done: make(chan any, 1),
@@ -776,6 +777,7 @@ func TestUpdateDiscoForNodeCallback(t *testing.T) {
DiscoKey: &newKey,
}},
}
// Not TSMP Path, just regular injection path.
ms.HandleNonKeepAliveMapResponse(t.Context(), resp)
<-nu.done
@@ -786,6 +788,119 @@ func TestUpdateDiscoForNodeCallback(t *testing.T) {
})
}
func TestUpdateDiscoForNodeCallbackWithFullNetmap(t *testing.T) {
now := time.Now()
oldTime := time.Unix(1, 0)
tests := []struct {
name string
initialOnline bool
initialLastSeen time.Time
updateOnline bool
updateLastSeen time.Time
expectNewDisco bool
}{
{
name: "disco key wired through when newer lastSeen",
initialOnline: false,
initialLastSeen: oldTime,
updateOnline: false,
updateLastSeen: now,
expectNewDisco: true,
},
{
name: "disco key NOT wired through when older lastSeen",
initialOnline: false,
initialLastSeen: now,
updateOnline: false,
updateLastSeen: oldTime,
expectNewDisco: false,
},
{
name: "disco key wired through when newer lastSeen, going offline",
initialOnline: true,
initialLastSeen: oldTime,
updateOnline: false,
updateLastSeen: now,
expectNewDisco: true,
},
{
name: "online flip with newer lastSeen",
initialOnline: false,
initialLastSeen: oldTime,
updateOnline: true,
updateLastSeen: now,
expectNewDisco: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nu := &rememberLastNetmapUpdater{
done: make(chan any, 1),
}
ms := newTestMapSession(t, nu)
oldKey := key.NewDisco()
// Initial node
node := tailcfg.Node{
ID: 1,
Key: key.NewNode().Public(),
DiscoKey: oldKey.Public(),
Online: new(tt.initialOnline),
LastSeen: new(tt.initialLastSeen),
Name: "host.network.ts.net",
}
if nm := ms.netmapForResponse(&tailcfg.MapResponse{
Peers: []*tailcfg.Node{&node},
}); len(nm.Peers) != 1 {
t.Fatalf("node not inserted")
}
newKey := key.NewDisco()
// Updated node
newNode := tailcfg.Node{
ID: 1,
Key: node.Key,
DiscoKey: newKey.Public(),
Online: new(tt.updateOnline),
LastSeen: new(tt.updateLastSeen),
Name: "host.network.ts.net",
}
ms.HandleNonKeepAliveMapResponse(t.Context(), &tailcfg.MapResponse{
Node: &newNode,
Peers: []*tailcfg.Node{
&newNode,
},
})
<-nu.done
newMap := nu.last
if n := len(newMap.Peers); n != 1 {
t.Fatalf("netmap not right length, got %d, expected %d", n, 1)
}
peer := newMap.Peers[0]
expectedDisco := oldKey.Public()
if tt.expectNewDisco {
expectedDisco = newKey.Public()
}
if peer.Key() != node.Key || peer.DiscoKey() != expectedDisco {
t.Fatalf("expected [%s]=%s, got [%s]=%s",
node.Key, expectedDisco,
peer.Key(), peer.DiscoKey(),
)
}
})
}
}
func first[T any](s []T) T {
if len(s) == 0 {
var zero T