mapper: reconcile self on policy recomputes

Self renders from the same state as peers, so leaving it out of policy
responses hid an exit node's approved routes until it reconnected.

Fixes #3502
This commit is contained in:
Kristoffer Dalby authored and Kristoffer Dalby committed 2026-10-07 23:36:16 +02:00
1 parent ba413fca3d
commit ea6b0bf0b7
7 files changed
+259 -34

No files matched your search

+24 -1
View File
@@ -50,6 +50,25 @@ type connectionEntry struct {
// can never become the stream's first frame ahead of the initial
// map. The zero value means the connection is ready.
pendingInitial atomic.Bool
// lastSelf is the self node last delivered to this connection's
// client, which keeps it until sent another. Every send to an
// established connection must go through [multiChannelNodeConn.send]
// to keep it current.
lastSelf atomic.Pointer[tailcfg.Node]
}
// withSelfDelta returns data without its Node when this client already
// holds an equal one: a Node forces a full client netmap rebuild.
func (entry *connectionEntry) withSelfDelta(data *tailcfg.MapResponse) *tailcfg.MapResponse {
if data.Node == nil || !data.Node.Equal(entry.lastSelf.Load()) {
return data
}
stripped := *data
stripped.Node = nil
return &stripped
}
// multiChannelNodeConn manages multiple concurrent connections for a single node.
@@ -341,7 +360,7 @@ func (mc *multiChannelNodeConn) send(data *tailcfg.MapResponse) error {
)
for _, conn := range snapshot {
err := conn.send(data)
err := conn.send(conn.withSelfDelta(data))
if err != nil {
lastErr = err
@@ -352,6 +371,10 @@ func (mc *multiChannelNodeConn) send(data *tailcfg.MapResponse) error {
Msg("send: connection failed")
} else {
successCount++
if data.Node != nil {
conn.lastSelf.Store(data.Node)
}
}
}