mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-01 03:04:07 +09:00
Bounds the DERP HTTP client's error body read. DERP region IDs are now
tailcfg.DERPRegionID.
(cherry picked from commit 3aece6501, adapted)
436 lines
11 KiB
Go
436 lines
11 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
hsdb "github.com/juanfont/headscale/hscontrol/db"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// DebugOverviewInfo represents the state overview information in a structured format.
|
|
type DebugOverviewInfo struct {
|
|
Nodes struct {
|
|
Total int `json:"total"`
|
|
Online int `json:"online"`
|
|
Expired int `json:"expired"`
|
|
Ephemeral int `json:"ephemeral"`
|
|
} `json:"nodes"`
|
|
Users map[string]int `json:"users"` // username -> node count
|
|
TotalUsers int `json:"total_users"`
|
|
Policy struct {
|
|
Mode string `json:"mode"`
|
|
Path string `json:"path,omitempty"`
|
|
} `json:"policy"`
|
|
DERP struct {
|
|
Configured bool `json:"configured"`
|
|
Regions int `json:"regions"`
|
|
} `json:"derp"`
|
|
PrimaryRoutes int `json:"primary_routes"`
|
|
}
|
|
|
|
// DebugDERPInfo represents DERP map information in a structured format.
|
|
type DebugDERPInfo struct {
|
|
Configured bool `json:"configured"`
|
|
TotalRegions int `json:"total_regions"`
|
|
Regions map[tailcfg.DERPRegionID]*DebugDERPRegion `json:"regions,omitempty"`
|
|
}
|
|
|
|
// DebugDERPRegion represents a single DERP region.
|
|
type DebugDERPRegion struct {
|
|
RegionID tailcfg.DERPRegionID `json:"region_id"`
|
|
RegionName string `json:"region_name"`
|
|
Nodes []*DebugDERPNode `json:"nodes"`
|
|
}
|
|
|
|
// DebugDERPNode represents a single DERP node.
|
|
type DebugDERPNode struct {
|
|
Name string `json:"name"`
|
|
HostName string `json:"hostname"`
|
|
DERPPort int `json:"derp_port"`
|
|
STUNPort int `json:"stun_port,omitempty"`
|
|
}
|
|
|
|
// DebugStringInfo wraps a debug string for JSON serialization.
|
|
type DebugStringInfo struct {
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
// DebugOverview returns a comprehensive overview of the current state for debugging.
|
|
func (s *State) DebugOverview() string {
|
|
allNodes := s.nodeStore.ListNodes()
|
|
users, _ := s.ListAllUsers()
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("=== Headscale State Overview ===\n\n")
|
|
|
|
// Node statistics
|
|
fmt.Fprintf(&sb, "Nodes: %d total\n", allNodes.Len())
|
|
|
|
userNodeCounts := make(map[string]int)
|
|
onlineCount := 0
|
|
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")
|
|
|
|
// User statistics
|
|
fmt.Fprintf(&sb, "Users: %d total\n", len(users))
|
|
|
|
for userName, nodeCount := range userNodeCounts {
|
|
fmt.Fprintf(&sb, " - %s: %d nodes\n", userName, nodeCount)
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
|
|
// Policy information
|
|
sb.WriteString("Policy:\n")
|
|
fmt.Fprintf(&sb, " - Mode: %s\n", s.cfg.Policy.Mode)
|
|
|
|
if s.cfg.Policy.Mode == types.PolicyModeFile {
|
|
fmt.Fprintf(&sb, " - Path: %s\n", s.cfg.Policy.Path)
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
|
|
// DERP information
|
|
derpMap := s.derpMap.Load()
|
|
if derpMap != nil {
|
|
fmt.Fprintf(&sb, "DERP: %d regions configured\n", len(derpMap.Regions))
|
|
} else {
|
|
sb.WriteString("DERP: not configured\n")
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
|
|
// Route information
|
|
primaryStr := s.PrimaryRoutesString()
|
|
|
|
routeCount := len(strings.Split(strings.TrimSpace(primaryStr), "\n"))
|
|
if primaryStr == "" {
|
|
routeCount = 0
|
|
}
|
|
|
|
fmt.Fprintf(&sb, "Primary Routes: %d active\n", routeCount)
|
|
sb.WriteString("\n")
|
|
|
|
// Registration cache
|
|
sb.WriteString("Registration Cache: active\n")
|
|
sb.WriteString("\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// DebugNodeStore returns debug information about the [NodeStore].
|
|
func (s *State) DebugNodeStore() string {
|
|
return s.nodeStore.DebugString()
|
|
}
|
|
|
|
// DebugDERPMap returns debug information about the DERP map configuration.
|
|
func (s *State) DebugDERPMap() string {
|
|
derpMap := s.derpMap.Load()
|
|
if derpMap == nil {
|
|
return "DERP Map: not configured\n"
|
|
}
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("=== DERP Map Configuration ===\n\n")
|
|
|
|
fmt.Fprintf(&sb, "Total Regions: %d\n\n", len(derpMap.Regions))
|
|
|
|
for regionID, region := range derpMap.Regions {
|
|
fmt.Fprintf(&sb, "Region %d: %s\n", regionID, region.RegionName)
|
|
fmt.Fprintf(&sb, " - Nodes: %d\n", len(region.Nodes))
|
|
|
|
for _, node := range region.Nodes {
|
|
fmt.Fprintf(&sb, " - %s (%s:%d)\n",
|
|
node.Name, node.HostName, node.DERPPort)
|
|
|
|
if node.STUNPort != 0 {
|
|
fmt.Fprintf(&sb, " STUN: %d\n", node.STUNPort)
|
|
}
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// DebugSSHPolicies returns debug information about SSH policies for all nodes.
|
|
func (s *State) DebugSSHPolicies() map[string]*tailcfg.SSHPolicy {
|
|
nodes := s.nodeStore.ListNodes()
|
|
|
|
sshPolicies := make(map[string]*tailcfg.SSHPolicy)
|
|
|
|
for _, node := range nodes.All() {
|
|
if !node.Valid() {
|
|
continue
|
|
}
|
|
|
|
pol, err := s.SSHPolicy(node)
|
|
if err != nil {
|
|
// Store the error information
|
|
continue
|
|
}
|
|
|
|
key := fmt.Sprintf("id:%d hostname:%s givenname:%s",
|
|
node.ID(), node.Hostname(), node.GivenName())
|
|
sshPolicies[key] = pol
|
|
}
|
|
|
|
return sshPolicies
|
|
}
|
|
|
|
// DebugRegistrationCache returns debug information about the registration cache.
|
|
func (s *State) DebugRegistrationCache() map[string]any {
|
|
return map[string]any{
|
|
"type": "expirable-lru",
|
|
"expiration": registerCacheExpiration.String(),
|
|
"max_entries": defaultRegisterCacheMaxEntries,
|
|
"current_len": s.authCache.Len(),
|
|
"status": "active",
|
|
}
|
|
}
|
|
|
|
// DebugConfig returns debug information about the current configuration.
|
|
func (s *State) DebugConfig() *types.Config {
|
|
return s.cfg
|
|
}
|
|
|
|
// DebugPolicy returns the current policy data as a string.
|
|
func (s *State) DebugPolicy() (string, error) {
|
|
switch s.cfg.Policy.Mode {
|
|
case types.PolicyModeDB:
|
|
p, err := s.GetPolicy()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return p.Data, nil
|
|
case types.PolicyModeFile:
|
|
pol, err := hsdb.PolicyBytes(s.db.DB, s.cfg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(pol), nil
|
|
default:
|
|
return "", fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, s.cfg.Policy.Mode)
|
|
}
|
|
}
|
|
|
|
// DebugFilter returns the current filter rules and matchers.
|
|
func (s *State) DebugFilter() ([]tailcfg.FilterRule, error) {
|
|
filter, _ := s.Filter()
|
|
return filter, nil
|
|
}
|
|
|
|
// DebugRoutes returns the current primary routes information as a
|
|
// structured object built from the [NodeStore] snapshot.
|
|
func (s *State) DebugRoutes() types.DebugRoutes {
|
|
debug := types.DebugRoutes{
|
|
AvailableRoutes: make(map[types.NodeID][]netip.Prefix),
|
|
PrimaryRoutes: make(map[string]types.NodeID),
|
|
}
|
|
|
|
for _, nv := range s.nodeStore.ListNodes().All() {
|
|
if !nv.Valid() {
|
|
continue
|
|
}
|
|
|
|
online, known := nv.IsOnline().GetOk()
|
|
if !known || !online {
|
|
continue
|
|
}
|
|
|
|
approved := nv.AllApprovedRoutes()
|
|
if len(approved) == 0 {
|
|
continue
|
|
}
|
|
|
|
slices.SortFunc(approved, netip.Prefix.Compare)
|
|
debug.AvailableRoutes[nv.ID()] = approved
|
|
}
|
|
|
|
for prefix, id := range s.nodeStore.PrimaryRoutes() {
|
|
debug.PrimaryRoutes[prefix.String()] = id
|
|
}
|
|
|
|
var unhealthy []types.NodeID
|
|
|
|
for _, nv := range s.nodeStore.ListNodes().All() {
|
|
if !nv.Valid() {
|
|
continue
|
|
}
|
|
|
|
if !s.nodeStore.IsNodeHealthy(nv.ID()) {
|
|
unhealthy = append(unhealthy, nv.ID())
|
|
}
|
|
}
|
|
|
|
if len(unhealthy) > 0 {
|
|
slices.Sort(unhealthy)
|
|
debug.UnhealthyNodes = unhealthy
|
|
}
|
|
|
|
return debug
|
|
}
|
|
|
|
// DebugRoutesString returns the current primary routes information as a string.
|
|
func (s *State) DebugRoutesString() string {
|
|
return s.PrimaryRoutesString()
|
|
}
|
|
|
|
// DebugPolicyManager returns the policy manager debug string.
|
|
func (s *State) DebugPolicyManager() string {
|
|
return s.PolicyDebugString()
|
|
}
|
|
|
|
// PolicyDebugString returns a debug representation of the current policy.
|
|
func (s *State) PolicyDebugString() string {
|
|
return s.polMan.DebugString()
|
|
}
|
|
|
|
// DebugOverviewJSON returns a structured overview of the current state for debugging.
|
|
func (s *State) DebugOverviewJSON() DebugOverviewInfo {
|
|
allNodes := s.nodeStore.ListNodes()
|
|
users, _ := s.ListAllUsers()
|
|
|
|
info := DebugOverviewInfo{
|
|
Users: make(map[string]int),
|
|
TotalUsers: len(users),
|
|
}
|
|
|
|
// Node statistics
|
|
info.Nodes.Total = allNodes.Len()
|
|
now := time.Now()
|
|
|
|
for _, node := range allNodes.All() {
|
|
if node.Valid() {
|
|
userName := node.Owner().Name()
|
|
info.Users[userName]++
|
|
|
|
if node.IsOnline().Valid() && node.IsOnline().Get() {
|
|
info.Nodes.Online++
|
|
}
|
|
|
|
if node.Expiry().Valid() && node.Expiry().Get().Before(now) {
|
|
info.Nodes.Expired++
|
|
}
|
|
|
|
if node.AuthKey().Valid() && node.AuthKey().Ephemeral() {
|
|
info.Nodes.Ephemeral++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Policy information
|
|
info.Policy.Mode = string(s.cfg.Policy.Mode)
|
|
if s.cfg.Policy.Mode == types.PolicyModeFile {
|
|
info.Policy.Path = s.cfg.Policy.Path
|
|
}
|
|
|
|
derpMap := s.derpMap.Load()
|
|
if derpMap != nil {
|
|
info.DERP.Configured = true
|
|
info.DERP.Regions = len(derpMap.Regions)
|
|
} else {
|
|
info.DERP.Configured = false
|
|
info.DERP.Regions = 0
|
|
}
|
|
|
|
// Route information
|
|
primaryStr := s.PrimaryRoutesString()
|
|
|
|
routeCount := len(strings.Split(strings.TrimSpace(primaryStr), "\n"))
|
|
if primaryStr == "" {
|
|
routeCount = 0
|
|
}
|
|
|
|
info.PrimaryRoutes = routeCount
|
|
|
|
return info
|
|
}
|
|
|
|
// DebugDERPJSON returns structured debug information about the DERP map configuration.
|
|
func (s *State) DebugDERPJSON() DebugDERPInfo {
|
|
derpMap := s.derpMap.Load()
|
|
|
|
info := DebugDERPInfo{
|
|
Configured: derpMap != nil,
|
|
Regions: make(map[tailcfg.DERPRegionID]*DebugDERPRegion),
|
|
}
|
|
|
|
if derpMap == nil {
|
|
return info
|
|
}
|
|
|
|
info.TotalRegions = len(derpMap.Regions)
|
|
|
|
for regionID, region := range derpMap.Regions {
|
|
debugRegion := &DebugDERPRegion{
|
|
RegionID: regionID,
|
|
RegionName: region.RegionName,
|
|
Nodes: make([]*DebugDERPNode, 0, len(region.Nodes)),
|
|
}
|
|
|
|
for _, node := range region.Nodes {
|
|
debugNode := &DebugDERPNode{
|
|
Name: node.Name,
|
|
HostName: node.HostName,
|
|
DERPPort: node.DERPPort,
|
|
STUNPort: node.STUNPort,
|
|
}
|
|
debugRegion.Nodes = append(debugRegion.Nodes, debugNode)
|
|
}
|
|
|
|
info.Regions[regionID] = debugRegion
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// DebugNodeStoreJSON returns the actual nodes map from the current [NodeStore] snapshot.
|
|
func (s *State) DebugNodeStoreJSON() map[types.NodeID]types.Node {
|
|
snapshot := s.nodeStore.data.Load()
|
|
return snapshot.nodesByID
|
|
}
|
|
|
|
// DebugPolicyManagerJSON returns structured debug information about the policy manager.
|
|
func (s *State) DebugPolicyManagerJSON() DebugStringInfo {
|
|
return DebugStringInfo{
|
|
Content: s.polMan.DebugString(),
|
|
}
|
|
}
|