app: switch from gorilla to chi mux

Replace gorilla/mux with go-chi/chi as the HTTP router and add a
custom zerolog-based request logger to replace chi's default
stdlib-based middleware.Logger, consistent with the rest of the
application.

Updates #1850
This commit is contained in:
Kristoffer Dalby 2026-02-24 18:47:40 +00:00
parent 25ccb5a161
commit 30338441c1
5 changed files with 179 additions and 55 deletions

View file

@ -20,7 +20,9 @@ import (
"github.com/cenkalti/backoff/v5"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/metrics"
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@ -457,50 +459,57 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
return os.Remove(h.cfg.UnixSocket)
}
func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
router := mux.NewRouter()
router.Use(prometheusMiddleware)
func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
r := chi.NewRouter()
r.Use(metrics.Collector(metrics.CollectorOpts{
Host: false,
Proto: true,
Skip: func(r *http.Request) bool {
return r.Method != http.MethodOptions
},
}))
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.RequestLogger(&zerologRequestLogger{}))
r.Use(middleware.Recoverer)
router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).
Methods(http.MethodPost, http.MethodGet)
r.Post(ts2021UpgradePath, h.NoiseUpgradeHandler)
router.HandleFunc("/robots.txt", h.RobotsHandler).Methods(http.MethodGet)
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/version", h.VersionHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{registration_id}", h.authProvider.RegisterHandler).
Methods(http.MethodGet)
r.Get("/robots.txt", h.RobotsHandler)
r.Get("/health", h.HealthHandler)
r.Get("/version", h.VersionHandler)
r.Get("/key", h.KeyHandler)
r.Get("/register/{registration_id}", h.authProvider.RegisterHandler)
if provider, ok := h.authProvider.(*AuthProviderOIDC); ok {
router.HandleFunc("/oidc/callback", provider.OIDCCallbackHandler).Methods(http.MethodGet)
r.Get("/oidc/callback", provider.OIDCCallbackHandler)
}
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).
Methods(http.MethodGet)
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
r.Get("/apple", h.AppleConfigMessage)
r.Get("/apple/{platform}", h.ApplePlatformConfig)
r.Get("/windows", h.WindowsConfigMessage)
// TODO(kristoffer): move swagger into a package
router.HandleFunc("/swagger", headscale.SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1).
Methods(http.MethodGet)
r.Get("/swagger", headscale.SwaggerUI)
r.Get("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1)
router.HandleFunc("/verify", h.VerifyHandler).Methods(http.MethodPost)
r.Post("/verify", h.VerifyHandler)
if h.cfg.DERP.ServerEnabled {
router.HandleFunc("/derp", h.DERPServer.DERPHandler)
router.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
router.HandleFunc("/derp/latency-check", derpServer.DERPProbeHandler)
router.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.state.DERPMap()))
r.HandleFunc("/derp", h.DERPServer.DERPHandler)
r.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
r.HandleFunc("/derp/latency-check", derpServer.DERPProbeHandler)
r.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.state.DERPMap()))
}
apiRouter := router.PathPrefix("/api").Subrouter()
apiRouter.Use(h.httpAuthenticationMiddleware)
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
router.HandleFunc("/favicon.ico", FaviconHandler)
router.PathPrefix("/").HandlerFunc(BlankHandler)
r.Route("/api", func(r chi.Router) {
r.Use(h.httpAuthenticationMiddleware)
r.HandleFunc("/v1/*", grpcMux.ServeHTTP)
})
r.Get("/favicon.ico", FaviconHandler)
r.Get("/", BlankHandler)
return router
return r
}
// Serve launches the HTTP and gRPC server service Headscale and the API.
@ -1083,3 +1092,52 @@ func (l *acmeLogger) RoundTrip(req *http.Request) (*http.Response, error) {
return resp, nil
}
// zerologRequestLogger implements chi's middleware.LogFormatter
// to route HTTP request logs through zerolog.
type zerologRequestLogger struct{}
func (z *zerologRequestLogger) NewLogEntry(
r *http.Request,
) middleware.LogEntry {
return &zerologLogEntry{
method: r.Method,
path: r.URL.Path,
proto: r.Proto,
remote: r.RemoteAddr,
}
}
type zerologLogEntry struct {
method string
path string
proto string
remote string
}
func (e *zerologLogEntry) Write(
status, bytes int,
header http.Header,
elapsed time.Duration,
extra any,
) {
log.Info().
Str("method", e.method).
Str("path", e.path).
Str("proto", e.proto).
Str("remote", e.remote).
Int("status", status).
Int("bytes", bytes).
Dur("elapsed", elapsed).
Msg("http request")
}
func (e *zerologLogEntry) Panic(
v any,
stack []byte,
) {
log.Error().
Interface("panic", v).
Bytes("stack", stack).
Msg("http handler panic")
}