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) +}