state: clear Unhealthy when node leaves HA candidate set

Restore the legacy auto-clear at write boundaries that drop HA
candidacy: Disconnect, SetApprovedRoutes(empty), and
UpdateNodeFromMapRequest shrinking advertised routes to empty.
Plus a defensive guard in SetNodeUnhealthy.

Updates #3203
This commit is contained in:
Kristoffer Dalby 2026-04-28 16:04:12 +00:00
parent 66ac785c22
commit 9f7c8e9a07
2 changed files with 114 additions and 0 deletions

View file

@ -599,6 +599,9 @@ func (s *State) Disconnect(id types.NodeID, epoch uint64) ([]change.Change, erro
now := time.Now()
n.LastSeen = &now
n.IsOnline = new(false)
// Offline nodes are not HA candidates; drop any stale
// Unhealthy bit so it does not surface in DebugRoutes.
n.Unhealthy = false
})
if !ok {
@ -899,6 +902,12 @@ func (s *State) SetApprovedRoutes(nodeID types.NodeID, routes []netip.Prefix) (t
n, ok := s.nodeStore.UpdateNode(nodeID, func(node *types.Node) {
node.ApprovedRoutes = routes
// A node with no approved routes is no longer an HA
// candidate; drop any stale Unhealthy bit (mirrors the
// legacy routes.SetRoutes(empty) auto-clear).
if len(node.AllApprovedRoutes()) == 0 {
node.Unhealthy = false
}
})
if !ok {
@ -2562,6 +2571,14 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
Msg("applying route approval results")
}
}
// AllApprovedRoutes is announced ∩ approved; a Hostinfo
// update that shrinks the announced set can drop the node
// out of HA candidacy without touching ApprovedRoutes.
// Clear any stale Unhealthy bit in that case.
if len(currentNode.AllApprovedRoutes()) == 0 {
currentNode.Unhealthy = false
}
})
if !ok {