mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-26 02:00:44 +09:00
debug: add json and improve
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
committed by
Kristoffer Dalby
parent
9b962956b5
commit
50ed24847b
381
hscontrol/state/debug.go
Normal file
381
hscontrol/state/debug.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/routes"
|
||||
"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[int]*DebugDERPRegion `json:"regions,omitempty"`
|
||||
}
|
||||
|
||||
// DebugDERPRegion represents a single DERP region.
|
||||
type DebugDERPRegion struct {
|
||||
RegionID int `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 {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
allNodes := s.nodeStore.ListNodes()
|
||||
users, _ := s.ListAllUsers()
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("=== Headscale State Overview ===\n\n")
|
||||
|
||||
// Node statistics
|
||||
sb.WriteString(fmt.Sprintf("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.User().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++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf(" - Online: %d\n", onlineCount))
|
||||
sb.WriteString(fmt.Sprintf(" - Expired: %d\n", expiredCount))
|
||||
sb.WriteString(fmt.Sprintf(" - Ephemeral: %d\n", ephemeralCount))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// User statistics
|
||||
sb.WriteString(fmt.Sprintf("Users: %d total\n", len(users)))
|
||||
for userName, nodeCount := range userNodeCounts {
|
||||
sb.WriteString(fmt.Sprintf(" - %s: %d nodes\n", userName, nodeCount))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Policy information
|
||||
sb.WriteString("Policy:\n")
|
||||
sb.WriteString(fmt.Sprintf(" - Mode: %s\n", s.cfg.Policy.Mode))
|
||||
if s.cfg.Policy.Mode == types.PolicyModeFile {
|
||||
sb.WriteString(fmt.Sprintf(" - Path: %s\n", s.cfg.Policy.Path))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// DERP information
|
||||
derpMap := s.derpMap.Load()
|
||||
if derpMap != nil {
|
||||
sb.WriteString(fmt.Sprintf("DERP: %d regions configured\n", len(derpMap.Regions)))
|
||||
} else {
|
||||
sb.WriteString("DERP: not configured\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Route information
|
||||
routeCount := len(strings.Split(strings.TrimSpace(s.primaryRoutes.String()), "\n"))
|
||||
if s.primaryRoutes.String() == "" {
|
||||
routeCount = 0
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("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")
|
||||
|
||||
sb.WriteString(fmt.Sprintf("Total Regions: %d\n\n", len(derpMap.Regions)))
|
||||
|
||||
for regionID, region := range derpMap.Regions {
|
||||
sb.WriteString(fmt.Sprintf("Region %d: %s\n", regionID, region.RegionName))
|
||||
sb.WriteString(fmt.Sprintf(" - Nodes: %d\n", len(region.Nodes)))
|
||||
|
||||
for _, node := range region.Nodes {
|
||||
sb.WriteString(fmt.Sprintf(" - %s (%s:%d)\n",
|
||||
node.Name, node.HostName, node.DERPPort))
|
||||
if node.STUNPort != 0 {
|
||||
sb.WriteString(fmt.Sprintf(" 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]interface{} {
|
||||
// The cache doesn't expose internal statistics, so we provide basic info
|
||||
result := map[string]interface{}{
|
||||
"type": "zcache",
|
||||
"expiration": registerCacheExpiration.String(),
|
||||
"cleanup": registerCacheCleanup.String(),
|
||||
"status": "active",
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 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 := policyBytes(s.db, s.cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(pol), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported policy mode: %s", 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.
|
||||
func (s *State) DebugRoutes() routes.DebugRoutes {
|
||||
return s.primaryRoutes.DebugJSON()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
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.User().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
|
||||
routeCount := len(strings.Split(strings.TrimSpace(s.primaryRoutes.String()), "\n"))
|
||||
if s.primaryRoutes.String() == "" {
|
||||
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[int]*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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user