mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
debug: refuse requests addressed to unknown host names
This commit is contained in:
1 parent
eeb02858d9
commit
5803ddedd3
3 files changed
+104
-1
No files matched your search
@@ -71,6 +71,11 @@ Query metrics via <http://localhost:9090/metrics> and get an overview of availab
|
||||
<http://localhost:9090/debug/>. Metrics may be queried from outside localhost but the debug interface is subject to
|
||||
additional protection despite listening on all interfaces.
|
||||
|
||||
The debug interface accepts requests addressed to an IP literal, `localhost`, the host in `server_url`, or the hostname
|
||||
in `metrics_listen_addr`. Hostnames are case-insensitive and may end in a dot. Other hostnames receive HTTP 403; use an
|
||||
IP address or one of the accepted names. `dns.base_domain` does not add accepted hosts. This check covers all debug
|
||||
paths, including statsviz. `/metrics` accepts any Host.
|
||||
|
||||
=== "Direct access"
|
||||
|
||||
Access the debug interface directly on the server where Headscale is installed.
|
||||
|
||||
@@ -24,3 +24,58 @@ func TestSecurityHeaders(t *testing.T) {
|
||||
assert.Equal(t, "nosniff", h.Get("X-Content-Type-Options"))
|
||||
assert.Equal(t, "no-referrer", h.Get("Referrer-Policy"))
|
||||
}
|
||||
|
||||
func TestDebugHostGuard(t *testing.T) {
|
||||
app := createTestApp(t)
|
||||
app.cfg.ServerURL = "https://Control.Example.COM:443/headscale"
|
||||
app.cfg.MetricsAddr = "Debug.Internal:9090"
|
||||
app.cfg.BaseDomain = "tailnet.invalid"
|
||||
handler := app.debugHTTPServer().Handler
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
host string
|
||||
path string
|
||||
status int
|
||||
}{
|
||||
{name: "localhost", host: "localhost:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "localhost case and dot", host: "LOCALhost.", path: "/debug/", status: http.StatusOK},
|
||||
{name: "IPv4", host: "127.0.0.1:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "IPv6", host: "[::1]", path: "/debug/", status: http.StatusOK},
|
||||
{name: "IPv6 port", host: "[::1]:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "tailnet IP", host: "100.64.0.1:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "private IP", host: "192.168.0.1:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "server URL host", host: "CoNtRoL.ExAmPlE.CoM.:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "metrics listen host", host: "DEBUG.INTERNAL.:9090", path: "/debug/", status: http.StatusOK},
|
||||
{name: "unknown host", host: "unknown.example", path: "/debug/", status: http.StatusForbidden},
|
||||
{name: "localhost suffix", host: "localhost.remote.example", path: "/debug/", status: http.StatusForbidden},
|
||||
{name: "server suffix", host: "control.example.com.remote", path: "/debug/", status: http.StatusForbidden},
|
||||
{name: "empty host", host: "", path: "/debug/", status: http.StatusForbidden},
|
||||
{name: "base domain", host: "tailnet.invalid", path: "/debug/", status: http.StatusForbidden},
|
||||
{name: "base domain child", host: "node.tailnet.invalid", path: "/debug/", status: http.StatusForbidden},
|
||||
{name: "custom debug handler", host: "unknown.example", path: "/debug/overview", status: http.StatusForbidden},
|
||||
{name: "pprof", host: "unknown.example", path: "/debug/pprof/", status: http.StatusForbidden},
|
||||
{name: "statsviz", host: "unknown.example", path: "/debug/statsviz/", status: http.StatusForbidden},
|
||||
{name: "statsviz websocket", host: "unknown.example", path: "/debug/statsviz/ws", status: http.StatusForbidden},
|
||||
{name: "whole mux", host: "unknown.example", path: "/", status: http.StatusForbidden},
|
||||
{name: "metrics any host", host: "unknown.example", path: "/metrics", status: http.StatusOK},
|
||||
{name: "metrics empty host", host: "", path: "/metrics", status: http.StatusOK},
|
||||
{name: "metrics query", host: "unknown.example", path: "/metrics?x=1", status: http.StatusOK},
|
||||
{name: "metrics slash", host: "unknown.example", path: "/metrics/", status: http.StatusForbidden},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, tt.path, nil)
|
||||
req.Host = tt.host
|
||||
req.RemoteAddr = "127.0.0.1:1234"
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, tt.status, rec.Code)
|
||||
assert.Equal(t, "DENY", rec.Header().Get("X-Frame-Options"))
|
||||
|
||||
if tt.status == http.StatusForbidden {
|
||||
assert.Contains(t, rec.Body.String(), "localhost")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+44
-1
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -77,6 +78,48 @@ func writeDebug(w http.ResponseWriter, r *http.Request, jsonVal func() any, text
|
||||
_, _ = w.Write([]byte(textVal()))
|
||||
}
|
||||
|
||||
func (h *Headscale) debugHostGuard(next http.Handler) http.Handler {
|
||||
normalize := func(host string) string {
|
||||
name, _, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
host = name
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(strings.ToLower(strings.Trim(host, "[]")), ".")
|
||||
}
|
||||
|
||||
allowed := []string{"localhost", normalize(h.cfg.MetricsAddr)}
|
||||
|
||||
serverURL, err := url.Parse(h.cfg.ServerURL)
|
||||
if err == nil {
|
||||
allowed = append(allowed, normalize(serverURL.Hostname()))
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/metrics" {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
host := normalize(r.Host)
|
||||
|
||||
_, err := netip.ParseAddr(host)
|
||||
if host != "" && (err == nil || slices.Contains(allowed, host)) {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(
|
||||
w,
|
||||
"debug host not allowed; use localhost, an IP address, "+
|
||||
"or the server_url or metrics_listen_addr host",
|
||||
http.StatusForbidden,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Headscale) debugHTTPServer() *http.Server {
|
||||
debugMux := http.NewServeMux()
|
||||
debug := tsweb.Debugger(debugMux)
|
||||
@@ -224,7 +267,7 @@ func (h *Headscale) debugHTTPServer() *http.Server {
|
||||
|
||||
debugHTTPServer := &http.Server{
|
||||
Addr: h.cfg.MetricsAddr,
|
||||
Handler: securityHeaders(debugMux),
|
||||
Handler: securityHeaders(h.debugHostGuard(debugMux)),
|
||||
ReadTimeout: types.HTTPTimeout,
|
||||
WriteTimeout: 0,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user