Skip to content

Commit 6403c8d

Browse files
authored
use tsweb debugger (#2420)
This PR switches the homegrown debug endpoint to using tsweb.Debugger, a neat toolkit with batteries included for pprof and friends, and making it easy to add additional debug info: I've started out by adding a bunch of "introspect" endpoints image So users can see the acl, filter, config, derpmap and connected nodes as headscale sees them.
1 parent b3fa16f commit 6403c8d

File tree

6 files changed

+135
-20
lines changed

6 files changed

+135
-20
lines changed

CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
## Next
44

5-
65
### Changes
76

87
- `oidc.map_legacy_users` and `oidc.strip_email_domain` has been removed
98
[#2411](https://github.com/juanfont/headscale/pull/2411)
10-
9+
- Add more information to `/debug` endpoint
10+
[#2420](https://github.com/juanfont/headscale/pull/2420)
11+
- It is now possible to inspect running goroutines and take profiles
12+
- View of config, policy, filter, ssh policy per node, connected nodes and
13+
DERPmap
1114

1215
## 0.25.0 (2025-02-xx)
1316

@@ -23,7 +26,7 @@
2326
- A logged out node logging in with the same user will replace the existing
2427
node.
2528
- Remove support for Tailscale clients older than 1.62 (Capability version 87)
26-
[#2405](https://github.com/juanfont/headscale/pull/2405)
29+
[#2405](https://github.com/juanfont/headscale/pull/2405)
2730

2831
### Changes
2932

@@ -49,6 +52,7 @@
4952
## 0.24.3 (2025-02-07)
5053

5154
### Changes
55+
5256
- Fix migration error caused by nodes having invalid auth keys
5357
[#2412](https://github.com/juanfont/headscale/pull/2412)
5458
- Pre auth keys belonging to a user are no longer deleted with the user

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
# When updating go.mod or go.sum, a new sha will need to be calculated,
3232
# update this if you have a mismatch after doing a change to those files.
33-
vendorHash = "sha256-ZQj2A0GdLhHc7JLW7qgpGBveXXNWg9ueSG47OZQQXEw=";
33+
vendorHash = "sha256-CoxqEAxGdefyiIhz84LXXxPrZ1JWsX8Ernv1USr9JTs=";
3434

3535
subPackages = ["cmd/headscale"];
3636

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ require (
8989
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
9090
github.com/akutz/memconn v0.1.0 // indirect
9191
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
92+
github.com/arl/statsviz v0.6.0 // indirect
9293
github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
9394
github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect
9495
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect
@@ -141,6 +142,7 @@ require (
141142
github.com/gookit/color v1.5.4 // indirect
142143
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
143144
github.com/gorilla/securecookie v1.1.2 // indirect
145+
github.com/gorilla/websocket v1.5.0 // indirect
144146
github.com/hashicorp/go-version v1.7.0 // indirect
145147
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
146148
github.com/illarion/gonotify/v2 v2.0.3 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
4141
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
4242
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
4343
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
44+
github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE=
45+
github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s=
4446
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
4547
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
4648
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
@@ -242,6 +244,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
242244
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
243245
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
244246
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
247+
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
248+
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
245249
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
246250
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
247251
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=

hscontrol/app.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import (
3636
"github.com/juanfont/headscale/hscontrol/util"
3737
zerolog "github.com/philip-bui/grpc-zerolog"
3838
"github.com/pkg/profile"
39-
"github.com/prometheus/client_golang/prometheus/promhttp"
4039
zl "github.com/rs/zerolog"
4140
"github.com/rs/zerolog/log"
4241
"golang.org/x/crypto/acme"
@@ -786,26 +785,12 @@ func (h *Headscale) Serve() error {
786785
log.Info().
787786
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
788787

789-
debugMux := http.NewServeMux()
790-
debugMux.Handle("/debug/pprof/", http.DefaultServeMux)
791-
debugMux.HandleFunc("/debug/notifier", func(w http.ResponseWriter, r *http.Request) {
792-
w.WriteHeader(http.StatusOK)
793-
w.Write([]byte(h.nodeNotifier.String()))
794-
})
795-
debugMux.Handle("/metrics", promhttp.Handler())
796-
797-
debugHTTPServer := &http.Server{
798-
Addr: h.cfg.MetricsAddr,
799-
Handler: debugMux,
800-
ReadTimeout: types.HTTPTimeout,
801-
WriteTimeout: 0,
802-
}
803-
804788
debugHTTPListener, err := net.Listen("tcp", h.cfg.MetricsAddr)
805789
if err != nil {
806790
return fmt.Errorf("failed to bind to TCP address: %w", err)
807791
}
808792

793+
debugHTTPServer := h.debugHTTPServer()
809794
errorGroup.Go(func() error { return debugHTTPServer.Serve(debugHTTPListener) })
810795

811796
log.Info().

hscontrol/debug.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package hscontrol
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/arl/statsviz"
9+
"github.com/juanfont/headscale/hscontrol/types"
10+
"github.com/prometheus/client_golang/prometheus/promhttp"
11+
"tailscale.com/tailcfg"
12+
"tailscale.com/tsweb"
13+
)
14+
15+
func (h *Headscale) debugHTTPServer() *http.Server {
16+
debugMux := http.NewServeMux()
17+
debug := tsweb.Debugger(debugMux)
18+
debug.Handle("notifier", "Connected nodes in notifier", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
w.WriteHeader(http.StatusOK)
20+
w.Write([]byte(h.nodeNotifier.String()))
21+
}))
22+
debug.Handle("config", "Current configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23+
config, err := json.MarshalIndent(h.cfg, "", " ")
24+
if err != nil {
25+
httpError(w, err)
26+
return
27+
}
28+
w.Header().Set("Content-Type", "text/plain")
29+
w.WriteHeader(http.StatusOK)
30+
w.Write(config)
31+
}))
32+
debug.Handle("policy", "Current policy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
pol, err := h.policyBytes()
34+
if err != nil {
35+
httpError(w, err)
36+
return
37+
}
38+
w.Header().Set("Content-Type", "text/plain")
39+
w.WriteHeader(http.StatusOK)
40+
w.Write(pol)
41+
}))
42+
debug.Handle("filter", "Current filter", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43+
filter := h.polMan.Filter()
44+
45+
filterJSON, err := json.MarshalIndent(filter, "", " ")
46+
if err != nil {
47+
httpError(w, err)
48+
return
49+
}
50+
w.Header().Set("Content-Type", "text/plain")
51+
w.WriteHeader(http.StatusOK)
52+
w.Write(filterJSON)
53+
}))
54+
debug.Handle("ssh", "SSH Policy per node", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55+
nodes, err := h.db.ListNodes()
56+
if err != nil {
57+
httpError(w, err)
58+
return
59+
}
60+
61+
sshPol := make(map[string]*tailcfg.SSHPolicy)
62+
for _, node := range nodes {
63+
pol, err := h.polMan.SSHPolicy(node)
64+
if err != nil {
65+
httpError(w, err)
66+
return
67+
}
68+
69+
sshPol[fmt.Sprintf("id:%d hostname:%s givenname:%s", node.ID, node.Hostname, node.GivenName)] = pol
70+
}
71+
72+
sshJSON, err := json.MarshalIndent(sshPol, "", " ")
73+
if err != nil {
74+
httpError(w, err)
75+
return
76+
}
77+
w.Header().Set("Content-Type", "text/plain")
78+
w.WriteHeader(http.StatusOK)
79+
w.Write(sshJSON)
80+
}))
81+
debug.Handle("derpmap", "Current DERPMap", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82+
dm := h.DERPMap
83+
84+
dmJSON, err := json.MarshalIndent(dm, "", " ")
85+
if err != nil {
86+
httpError(w, err)
87+
return
88+
}
89+
w.Header().Set("Content-Type", "text/plain")
90+
w.WriteHeader(http.StatusOK)
91+
w.Write(dmJSON)
92+
}))
93+
debug.Handle("registration-cache", "Pending registrations", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
94+
registrationsJSON, err := json.MarshalIndent(h.registrationCache.Items(), "", " ")
95+
if err != nil {
96+
httpError(w, err)
97+
return
98+
}
99+
w.Header().Set("Content-Type", "text/plain")
100+
w.WriteHeader(http.StatusOK)
101+
w.Write(registrationsJSON)
102+
}))
103+
104+
err := statsviz.Register(debugMux)
105+
if err == nil {
106+
debug.URL("/debug/statsviz", "Statsviz (visualise go metrics)")
107+
}
108+
109+
debug.URL("/metrics", "Prometheus metrics")
110+
debugMux.Handle("/metrics", promhttp.Handler())
111+
112+
debugHTTPServer := &http.Server{
113+
Addr: h.cfg.MetricsAddr,
114+
Handler: debugMux,
115+
ReadTimeout: types.HTTPTimeout,
116+
WriteTimeout: 0,
117+
}
118+
119+
return debugHTTPServer
120+
}

0 commit comments

Comments
 (0)