diff --git a/hscontrol/debug.go b/hscontrol/debug.go index 60676a1d..2d828227 100644 --- a/hscontrol/debug.go +++ b/hscontrol/debug.go @@ -105,10 +105,9 @@ func (h *Headscale) debugHTTPServer() *http.Server { w.Write(dmJSON) })) debug.Handle("registration-cache", "Pending registrations", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // TODO(kradalby): This should be replaced with a proper state method that returns registration info - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) - w.Write([]byte("{}")) // For now, return empty object + w.Write([]byte(h.state.GetRegistrationCacheDebug())) })) debug.Handle("routes", "Routes", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") diff --git a/hscontrol/state/state.go b/hscontrol/state/state.go index 0a743184..24abcf65 100644 --- a/hscontrol/state/state.go +++ b/hscontrol/state/state.go @@ -9,6 +9,7 @@ import ( "io" "net/netip" "os" + "strings" "sync/atomic" "time" @@ -738,6 +739,43 @@ func (s *State) SetRegistrationCacheEntry(id types.RegistrationID, entry types.R s.registrationCache.Set(id, entry) } +// GetRegistrationCacheDebug returns all registration cache entries as a formatted string +func (s *State) GetRegistrationCacheDebug() string { + s.mu.RLock() + defer s.mu.RUnlock() + + items := s.registrationCache.Items() + if len(items) == 0 { + return "No pending registrations" + } + + var result strings.Builder + result.WriteString(fmt.Sprintf("Registration Cache (%d entries):\n", len(items))) + result.WriteString("=====================================\n") + + for id, item := range items { + regNode := item.Object + result.WriteString(fmt.Sprintf(" ID: %s\n", id)) + result.WriteString(fmt.Sprintf(" Node ID: %d\n", regNode.Node.ID)) + result.WriteString(fmt.Sprintf(" Hostname: %s\n", regNode.Node.Hostname)) + result.WriteString(fmt.Sprintf(" Given Name: %s\n", regNode.Node.GivenName)) + result.WriteString(fmt.Sprintf(" User ID: %d\n", regNode.Node.UserID)) + result.WriteString(fmt.Sprintf(" Register Method: %s\n", regNode.Node.RegisterMethod)) + result.WriteString(fmt.Sprintf(" Expires: %v\n", item.Expiration)) + result.WriteString(fmt.Sprintf(" Channel Status: %s\n", func() string { + select { + case <-regNode.Registered: + return "closed" + default: + return "open" + } + }())) + result.WriteString("-------------------------------------\n") + } + + return result.String() +} + // HandleNodeFromAuthPath handles node registration through authentication flow (like OIDC). func (s *State) HandleNodeFromAuthPath( registrationID types.RegistrationID, diff --git a/hscontrol/types/common.go b/hscontrol/types/common.go index a80f2ab4..dd786002 100644 --- a/hscontrol/types/common.go +++ b/hscontrol/types/common.go @@ -188,6 +188,19 @@ type RegisterNode struct { Registered chan *Node } +// RegisterNodeDebug is a JSON-serializable version of RegisterNode +// that excludes the channel field to prevent JSON marshaling errors +type RegisterNodeDebug struct { + Node Node `json:"node"` +} + +// ToDebug converts a RegisterNode to its JSON-serializable form +func (rn *RegisterNode) ToDebug() RegisterNodeDebug { + return RegisterNodeDebug{ + Node: rn.Node, + } +} + // DefaultBatcherWorkers returns the default number of batcher workers. // Default to 3/4 of CPU cores, minimum 1, no maximum. func DefaultBatcherWorkers() int {