mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-13 11:29:09 +09:00
5dd622a31b
primaryRoutes lived as a separate write-then-read store guarded by an external mutex. Each Connect, Disconnect, SetApprovedRoutes, and HA prober write touched both NodeStore and primaryRoutes in sequence, opening a TOCTOU window in which a stale Disconnect could overwrite a fresh Connect's route assignment (issue #3203). The election algorithm is a pure function of the snapshot: every node contributes its IsOnline + AllApprovedRoutes + a runtime Unhealthy bit. Move the computation into snapshotFromNodes so it runs in the NodeStore writer goroutine and recomputes from immutable input on every batch — no separate primaryRoutes mutation is required. Caller-side changes are local. GetNodePrimaryRoutes, RoutesForPeer, the HA prober and the debug endpoint all read PrimaryRoutesForNode / HANodes / DebugRoutes off NodeStore. Health writes flow through State.SetNodeUnhealthy, which captures the snapshot's primaries before/after and signals back whether failover happened. The SetApprovedRoutes and UpdateNodeFromMapRequest paths use the same prevPrimaries / maps.Equal pattern to detect when announced/approved changes shifted a primary. The per-node lock and connectGen counter that closed the original TOCTOU window are gone too: Connect bumps a SessionEpoch field on the node inside its UpdateNode closure, Disconnect re-checks it in its closure and no-ops on mismatch. The NodeStore writer goroutine serialises both batches so the check + mutation are atomic by construction. Updates #3203