From e4e742c776eed9422c6a34a002ed5249d9922762 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 20 May 2026 08:48:35 +0000 Subject: [PATCH] noise: pin outer RemoteAddr onto tunnel requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTTP/2 server inside the Noise tunnel fills r.RemoteAddr from the hijacked TCP socket, so /machine/register and /machine/map logged the reverse proxy's loopback peer (e.g. 127.0.0.1:44388) even with trusted_proxies set. The outer router's realIPMiddleware had already resolved the client IP onto req.RemoteAddr; that value never crossed the hijack. Replace the inner realIPMiddleware mount — dead inside the encrypted tunnel — with overrideRemoteAddr(req.RemoteAddr) so requests served over the tunnel report the outer-resolved client IP. --- hscontrol/noise.go | 21 ++++++++++++++++++--- hscontrol/noise_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 603e5256..5abc6e97 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -159,9 +159,10 @@ func (h *Headscale) NoiseUpgradeHandler( })) r.Use(middleware.RequestID) - if h.realIPMiddleware != nil { - r.Use(h.realIPMiddleware) - } + // The outer router resolved trusted_proxies on req before the + // upgrade; pin that value across the hijack so /machine/* logs the + // client IP instead of the reverse proxy's loopback peer. + r.Use(overrideRemoteAddr(req.RemoteAddr)) r.Use(middleware.RequestLogger(&zerologRequestLogger{})) r.Use(middleware.Recoverer) @@ -294,6 +295,20 @@ func rejectUnsupported( return false } +// overrideRemoteAddr returns middleware that pins r.RemoteAddr to addr. +// Used inside the Noise tunnel: the HTTP/2 server derives r.RemoteAddr +// from the hijacked TCP socket (the reverse proxy's loopback peer), so +// the outer request's resolved client IP must be carried across the +// hijack boundary by hand. +func overrideRemoteAddr(addr string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.RemoteAddr = addr + next.ServeHTTP(w, r) + }) + } +} + func (ns *noiseServer) NotImplementedHandler(writer http.ResponseWriter, req *http.Request) { log.Trace().Caller().Str("path", req.URL.String()).Msg("not implemented handler hit") http.Error(writer, "Not implemented yet", http.StatusNotImplemented) diff --git a/hscontrol/noise_test.go b/hscontrol/noise_test.go index 6c427180..148dc954 100644 --- a/hscontrol/noise_test.go +++ b/hscontrol/noise_test.go @@ -368,3 +368,31 @@ func TestSSHActionFollowUp_RejectsBindingMismatch(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, rec.Code, "binding mismatch must be rejected with 401") } + +// TestOverrideRemoteAddr asserts the middleware used inside the Noise +// tunnel pins r.RemoteAddr to the value captured from the outer +// (pre-hijack) request, so /machine/* requests log the trusted-proxy +// resolved client IP instead of the hijacked TCP socket's loopback peer. +func TestOverrideRemoteAddr(t *testing.T) { + t.Parallel() + + const clientAddr = "192.168.91.240" + + r := chi.NewRouter() + r.Use(overrideRemoteAddr(clientAddr)) + + var observed string + + r.Get("/x", func(w http.ResponseWriter, r *http.Request) { + observed = r.RemoteAddr + + w.WriteHeader(http.StatusOK) + }) + + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/x", nil) + req.RemoteAddr = "127.0.0.1:44388" + + r.ServeHTTP(httptest.NewRecorder(), req) + + assert.Equal(t, clientAddr, observed) +}