mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-21 07:44:52 +09:00
state: consolidate route-election and node helpers
This commit is contained in:
+14
-57
@@ -62,51 +62,23 @@ type DebugStringInfo struct {
|
|||||||
|
|
||||||
// DebugOverview returns a comprehensive overview of the current state for debugging.
|
// DebugOverview returns a comprehensive overview of the current state for debugging.
|
||||||
func (s *State) DebugOverview() string {
|
func (s *State) DebugOverview() string {
|
||||||
allNodes := s.nodeStore.ListNodes()
|
info := s.DebugOverviewJSON()
|
||||||
users, _ := s.ListAllUsers()
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
sb.WriteString("=== Headscale State Overview ===\n\n")
|
sb.WriteString("=== Headscale State Overview ===\n\n")
|
||||||
|
|
||||||
// Node statistics
|
// Node statistics
|
||||||
fmt.Fprintf(&sb, "Nodes: %d total\n", allNodes.Len())
|
fmt.Fprintf(&sb, "Nodes: %d total\n", info.Nodes.Total)
|
||||||
|
fmt.Fprintf(&sb, " - Online: %d\n", info.Nodes.Online)
|
||||||
userNodeCounts := make(map[string]int)
|
fmt.Fprintf(&sb, " - Expired: %d\n", info.Nodes.Expired)
|
||||||
onlineCount := 0
|
fmt.Fprintf(&sb, " - Ephemeral: %d\n", info.Nodes.Ephemeral)
|
||||||
expiredCount := 0
|
|
||||||
ephemeralCount := 0
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for _, node := range allNodes.All() {
|
|
||||||
if node.Valid() {
|
|
||||||
userName := node.Owner().Name()
|
|
||||||
userNodeCounts[userName]++
|
|
||||||
|
|
||||||
if node.IsOnline().Valid() && node.IsOnline().Get() {
|
|
||||||
onlineCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Expiry().Valid() && node.Expiry().Get().Before(now) {
|
|
||||||
expiredCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.AuthKey().Valid() && node.AuthKey().Ephemeral() {
|
|
||||||
ephemeralCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(&sb, " - Online: %d\n", onlineCount)
|
|
||||||
fmt.Fprintf(&sb, " - Expired: %d\n", expiredCount)
|
|
||||||
fmt.Fprintf(&sb, " - Ephemeral: %d\n", ephemeralCount)
|
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// User statistics
|
// User statistics
|
||||||
fmt.Fprintf(&sb, "Users: %d total\n", len(users))
|
fmt.Fprintf(&sb, "Users: %d total\n", info.TotalUsers)
|
||||||
|
|
||||||
for userName, nodeCount := range userNodeCounts {
|
for userName, nodeCount := range info.Users {
|
||||||
fmt.Fprintf(&sb, " - %s: %d nodes\n", userName, nodeCount)
|
fmt.Fprintf(&sb, " - %s: %d nodes\n", userName, nodeCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,18 +86,17 @@ func (s *State) DebugOverview() string {
|
|||||||
|
|
||||||
// Policy information
|
// Policy information
|
||||||
sb.WriteString("Policy:\n")
|
sb.WriteString("Policy:\n")
|
||||||
fmt.Fprintf(&sb, " - Mode: %s\n", s.cfg.Policy.Mode)
|
fmt.Fprintf(&sb, " - Mode: %s\n", info.Policy.Mode)
|
||||||
|
|
||||||
if s.cfg.Policy.Mode == types.PolicyModeFile {
|
if info.Policy.Mode == string(types.PolicyModeFile) {
|
||||||
fmt.Fprintf(&sb, " - Path: %s\n", s.cfg.Policy.Path)
|
fmt.Fprintf(&sb, " - Path: %s\n", info.Policy.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// DERP information
|
// DERP information
|
||||||
derpMap := s.derpMap.Load()
|
if info.DERP.Configured {
|
||||||
if derpMap != nil {
|
fmt.Fprintf(&sb, "DERP: %d regions configured\n", info.DERP.Regions)
|
||||||
fmt.Fprintf(&sb, "DERP: %d regions configured\n", len(derpMap.Regions))
|
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("DERP: not configured\n")
|
sb.WriteString("DERP: not configured\n")
|
||||||
}
|
}
|
||||||
@@ -133,14 +104,7 @@ func (s *State) DebugOverview() string {
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// Route information
|
// Route information
|
||||||
primaryStr := s.PrimaryRoutesString()
|
fmt.Fprintf(&sb, "Primary Routes: %d active\n", info.PrimaryRoutes)
|
||||||
|
|
||||||
routeCount := len(strings.Split(strings.TrimSpace(primaryStr), "\n"))
|
|
||||||
if primaryStr == "" {
|
|
||||||
routeCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(&sb, "Primary Routes: %d active\n", routeCount)
|
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// Registration cache
|
// Registration cache
|
||||||
@@ -371,14 +335,7 @@ func (s *State) DebugOverviewJSON() DebugOverviewInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Route information
|
// Route information
|
||||||
primaryStr := s.PrimaryRoutesString()
|
info.PrimaryRoutes = len(s.nodeStore.PrimaryRoutes())
|
||||||
|
|
||||||
routeCount := len(strings.Split(strings.TrimSpace(primaryStr), "\n"))
|
|
||||||
if primaryStr == "" {
|
|
||||||
routeCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
info.PrimaryRoutes = routeCount
|
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,16 +74,24 @@ func (p *HAHealthProber) ProbeOnce(
|
|||||||
) {
|
) {
|
||||||
haNodes := p.state.nodeStore.HANodes()
|
haNodes := p.state.nodeStore.HANodes()
|
||||||
|
|
||||||
// Drop stable-session entries for nodes that are no longer HA
|
// Build the deduplicated node-ID set and slice in one pass.
|
||||||
// candidates so a future reappearance starts fresh.
|
var nodeIDs []types.NodeID
|
||||||
|
|
||||||
seen := make(set.Set[types.NodeID])
|
seen := make(set.Set[types.NodeID])
|
||||||
|
|
||||||
for _, nodes := range haNodes {
|
for _, nodes := range haNodes {
|
||||||
for _, id := range nodes {
|
for _, id := range nodes {
|
||||||
|
if seen.Contains(id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
seen.Add(id)
|
seen.Add(id)
|
||||||
|
nodeIDs = append(nodeIDs, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop stable-session entries for nodes that are no longer HA
|
||||||
|
// candidates so a future reappearance starts fresh.
|
||||||
p.lastStableSession.Range(func(id types.NodeID, _ uint64) bool {
|
p.lastStableSession.Range(func(id types.NodeID, _ uint64) bool {
|
||||||
if !seen.Contains(id) {
|
if !seen.Contains(id) {
|
||||||
p.lastStableSession.Delete(id)
|
p.lastStableSession.Delete(id)
|
||||||
@@ -96,20 +104,6 @@ func (p *HAHealthProber) ProbeOnce(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate node IDs across prefixes.
|
|
||||||
var nodeIDs []types.NodeID
|
|
||||||
|
|
||||||
dedup := make(set.Set[types.NodeID])
|
|
||||||
|
|
||||||
for _, nodes := range haNodes {
|
|
||||||
for _, id := range nodes {
|
|
||||||
if !dedup.Contains(id) {
|
|
||||||
dedup.Add(id)
|
|
||||||
nodeIDs = append(nodeIDs, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("haNodes", len(nodeIDs)).
|
Int("haNodes", len(nodeIDs)).
|
||||||
Msg("HA health prober starting probe cycle")
|
Msg("HA health prober starting probe cycle")
|
||||||
|
|||||||
@@ -527,33 +527,24 @@ func (s *NodeStore) applyBatch(batch []work) {
|
|||||||
// Update node count gauge
|
// Update node count gauge
|
||||||
nodeStoreNodesCount.Set(float64(len(nodes)))
|
nodeStoreNodesCount.Set(float64(len(nodes)))
|
||||||
|
|
||||||
// Send the resulting nodes to all work items that requested them
|
// Send the resulting nodes to all work items that requested them.
|
||||||
|
// A zero-value NodeView{} reports Valid()==false, matching node.View()
|
||||||
|
// for a node that was deleted or never existed.
|
||||||
for nodeID, workItems := range nodeResultRequests {
|
for nodeID, workItems := range nodeResultRequests {
|
||||||
|
var nodeView types.NodeView
|
||||||
if node, exists := nodes[nodeID]; exists {
|
if node, exists := nodes[nodeID]; exists {
|
||||||
nodeView := node.View()
|
nodeView = node.View()
|
||||||
for _, w := range workItems {
|
}
|
||||||
w.nodeResult <- nodeView
|
|
||||||
|
|
||||||
close(w.nodeResult)
|
for _, w := range workItems {
|
||||||
|
w.nodeResult <- nodeView
|
||||||
|
|
||||||
if w.errResult != nil {
|
close(w.nodeResult)
|
||||||
w.errResult <- setErrResults[w]
|
|
||||||
|
|
||||||
close(w.errResult)
|
if w.errResult != nil {
|
||||||
}
|
w.errResult <- setErrResults[w]
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Node was deleted or doesn't exist
|
|
||||||
for _, w := range workItems {
|
|
||||||
w.nodeResult <- types.NodeView{} // Send invalid view
|
|
||||||
|
|
||||||
close(w.nodeResult)
|
close(w.errResult)
|
||||||
|
|
||||||
if w.errResult != nil {
|
|
||||||
w.errResult <- setErrResults[w]
|
|
||||||
|
|
||||||
close(w.errResult)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -669,26 +660,13 @@ func snapshotFromNodes(
|
|||||||
return newSnap
|
return newSnap
|
||||||
}
|
}
|
||||||
|
|
||||||
// electPrimaryRoutes picks the primary advertiser for each non-exit
|
// onlineAdvertisers maps each non-exit prefix to the IDs of online nodes
|
||||||
// prefix. Inputs are restricted to online nodes that advertise the
|
// that approve it. Nodes are visited in the order of ids, so the per-prefix
|
||||||
// prefix. The previous primary is preserved when it is still online
|
// slices preserve that order.
|
||||||
// and healthy (anti-flap); otherwise the lowest-NodeID healthy
|
func onlineAdvertisers(
|
||||||
// advertiser wins. When every advertiser is unhealthy the previous
|
|
||||||
// primary is preserved only if still a candidate — falling back to
|
|
||||||
// any other candidate would point peers at a node the prober has
|
|
||||||
// already declared unreachable, so leaving the prefix unmapped is
|
|
||||||
// preferred until a probe cycle finds one that responds.
|
|
||||||
func electPrimaryRoutes(
|
|
||||||
nodes map[types.NodeID]types.Node,
|
nodes map[types.NodeID]types.Node,
|
||||||
prev map[netip.Prefix]types.NodeID,
|
ids []types.NodeID,
|
||||||
) (map[netip.Prefix]types.NodeID, map[types.NodeID]bool) {
|
) map[netip.Prefix][]types.NodeID {
|
||||||
ids := make([]types.NodeID, 0, len(nodes))
|
|
||||||
for id := range nodes {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(ids)
|
|
||||||
|
|
||||||
advertisers := make(map[netip.Prefix][]types.NodeID)
|
advertisers := make(map[netip.Prefix][]types.NodeID)
|
||||||
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
@@ -706,6 +684,24 @@ func electPrimaryRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return advertisers
|
||||||
|
}
|
||||||
|
|
||||||
|
// electPrimaryRoutes picks the primary advertiser for each non-exit
|
||||||
|
// prefix. Inputs are restricted to online nodes that advertise the
|
||||||
|
// prefix. The previous primary is preserved when it is still online
|
||||||
|
// and healthy (anti-flap); otherwise the lowest-NodeID healthy
|
||||||
|
// advertiser wins. When every advertiser is unhealthy the previous
|
||||||
|
// primary is preserved only if still a candidate — falling back to
|
||||||
|
// any other candidate would point peers at a node the prober has
|
||||||
|
// already declared unreachable, so leaving the prefix unmapped is
|
||||||
|
// preferred until a probe cycle finds one that responds.
|
||||||
|
func electPrimaryRoutes(
|
||||||
|
nodes map[types.NodeID]types.Node,
|
||||||
|
prev map[netip.Prefix]types.NodeID,
|
||||||
|
) (map[netip.Prefix]types.NodeID, map[types.NodeID]bool) {
|
||||||
|
advertisers := onlineAdvertisers(nodes, slices.Sorted(maps.Keys(nodes)))
|
||||||
|
|
||||||
routes := make(map[netip.Prefix]types.NodeID, len(advertisers))
|
routes := make(map[netip.Prefix]types.NodeID, len(advertisers))
|
||||||
for prefix, candidates := range advertisers {
|
for prefix, candidates := range advertisers {
|
||||||
if cur, ok := prev[prefix]; ok &&
|
if cur, ok := prev[prefix]; ok &&
|
||||||
@@ -734,7 +730,7 @@ func electPrimaryRoutes(
|
|||||||
// would point peers at a node the prober has already declared
|
// would point peers at a node the prober has already declared
|
||||||
// unreachable; leaving the prefix unmapped is honest until a
|
// unreachable; leaving the prefix unmapped is honest until a
|
||||||
// probe cycle picks one that responds.
|
// probe cycle picks one that responds.
|
||||||
if !found && len(candidates) >= 1 {
|
if !found {
|
||||||
if cur, ok := prev[prefix]; ok && slices.Contains(candidates, cur) {
|
if cur, ok := prev[prefix]; ok && slices.Contains(candidates, cur) {
|
||||||
selected = cur
|
selected = cur
|
||||||
found = true
|
found = true
|
||||||
@@ -919,21 +915,10 @@ func (s *NodeStore) PrimaryRoutesForNode(id types.NodeID) []netip.Prefix {
|
|||||||
func (s *NodeStore) HANodes() map[netip.Prefix][]types.NodeID {
|
func (s *NodeStore) HANodes() map[netip.Prefix][]types.NodeID {
|
||||||
snap := s.data.Load()
|
snap := s.data.Load()
|
||||||
|
|
||||||
advertisers := make(map[netip.Prefix][]types.NodeID)
|
advertisers := onlineAdvertisers(
|
||||||
|
snap.nodesByID,
|
||||||
for id, n := range snap.nodesByID {
|
slices.Sorted(maps.Keys(snap.nodesByID)),
|
||||||
if n.IsOnline == nil || !*n.IsOnline {
|
)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range n.AllApprovedRoutes() {
|
|
||||||
if tsaddr.IsExitRoute(p) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
advertisers[p] = append(advertisers[p], id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make(map[netip.Prefix][]types.NodeID)
|
out := make(map[netip.Prefix][]types.NodeID)
|
||||||
|
|
||||||
@@ -942,7 +927,6 @@ func (s *NodeStore) HANodes() map[netip.Prefix][]types.NodeID {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Sort(ids)
|
|
||||||
out[p] = ids
|
out[p] = ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -771,6 +771,16 @@ func (s *State) ResolveNode(query string) (types.NodeView, bool) {
|
|||||||
return s.GetNodeByID(id)
|
return s.GetNodeByID(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keepLowest returns whichever node has the lower ID, so repeated
|
||||||
|
// calls resolve deterministically across snapshot iterations.
|
||||||
|
keepLowest := func(cur, cand types.NodeView) types.NodeView {
|
||||||
|
if !cur.Valid() || cand.ID() < cur.ID() {
|
||||||
|
return cand
|
||||||
|
}
|
||||||
|
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
|
||||||
// Try IP address.
|
// Try IP address.
|
||||||
addr, addrErr := netip.ParseAddr(query)
|
addr, addrErr := netip.ParseAddr(query)
|
||||||
if addrErr == nil {
|
if addrErr == nil {
|
||||||
@@ -781,9 +791,7 @@ func (s *State) ResolveNode(query string) (types.NodeView, bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !match.Valid() || n.ID() < match.ID() {
|
match = keepLowest(match, n)
|
||||||
match = n
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return match, match.Valid()
|
return match, match.Valid()
|
||||||
@@ -795,13 +803,9 @@ func (s *State) ResolveNode(query string) (types.NodeView, bool) {
|
|||||||
|
|
||||||
for _, n := range s.ListNodes().All() {
|
for _, n := range s.ListNodes().All() {
|
||||||
if n.GivenName() == query {
|
if n.GivenName() == query {
|
||||||
if !givenMatch.Valid() || n.ID() < givenMatch.ID() {
|
givenMatch = keepLowest(givenMatch, n)
|
||||||
givenMatch = n
|
|
||||||
}
|
|
||||||
} else if n.Hostname() == query {
|
} else if n.Hostname() == query {
|
||||||
if !hostMatch.Valid() || n.ID() < hostMatch.ID() {
|
hostMatch = keepLowest(hostMatch, n)
|
||||||
hostMatch = n
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user