From 5705538f2e54909ccb739dc823c73b2907b7e061 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 17 Oct 2026 09:25:52 +0000 Subject: [PATCH] noise: stop serving metrics inside the tunnel Any machine key reached it, bypassing metrics_listen_addr. No client asks for it. --- hscontrol/noise.go | 2 -- hscontrol/noise_test.go | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 326723ea2..d6b8a35aa 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -161,8 +161,6 @@ func (h *Headscale) NoiseUpgradeHandler( r.Use(middleware.RequestLogger(&zerologRequestLogger{})) r.Use(middleware.Recoverer) - r.Handle("/metrics", metrics.Handler()) - r.Route("/machine", func(r chi.Router) { r.Post("/register", ns.RegistrationHandler) r.Post("/map", ns.PollNetMapHandler) diff --git a/hscontrol/noise_test.go b/hscontrol/noise_test.go index 9f3559d7d..83914174e 100644 --- a/hscontrol/noise_test.go +++ b/hscontrol/noise_test.go @@ -32,9 +32,13 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "tailscale.com/control/ts2021" + "tailscale.com/net/netmon" "tailscale.com/net/netutil" + "tailscale.com/net/tsdial" "tailscale.com/tailcfg" "tailscale.com/types/key" + "tailscale.com/util/eventbus" "tailscale.com/util/zstdframe" ) @@ -1999,6 +2003,45 @@ func TestNoiseHTTPServerTimeouts(t *testing.T) { }) } +// TestNoiseRouter_DoesNotServeMetrics pins that the metrics registry is not +// reachable through a Noise tunnel. The handshake accepts any machine key, so +// every route on the tunnel's router is open to anyone who reaches the public +// listener; metrics belong on metrics_listen_addr only. +func TestNoiseRouter_DoesNotServeMetrics(t *testing.T) { + t.Parallel() + + app := createTestApp(t) + server := httptest.NewServer(app.HTTPHandler()) + t.Cleanup(server.Close) + + dialer := tsdial.NewDialer(netmon.NewStatic()) + dialer.SetBus(eventbus.New()) + + // A machine key that never registered. + client, err := ts2021.NewClient(ts2021.ClientOpts{ + ServerURL: server.URL, + PrivKey: key.NewMachine(), + ServerPubKey: app.noisePrivateKey.Public(), + Dialer: dialer, + Logf: t.Logf, + }) + require.NoError(t, err) + t.Cleanup(func() { _ = client.Close() }) + + // The client sends every request down the tunnel whatever the host. + req, err := http.NewRequestWithContext( + t.Context(), http.MethodGet, "https://headscale/metrics", nil, + ) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} + // TestSSHActionFollowUp_ParksOneRequestPerSession pins that a pending SSH // check keeps one waiting follow-up, the newest. Older waiters re-delegate at // once instead of staying parked until the session ends.