noise: pin outer RemoteAddr onto tunnel requests

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.
This commit is contained in:
Kristoffer Dalby
2026-05-20 08:48:35 +00:00
parent 4cca63155d
commit e4e742c776
2 changed files with 46 additions and 3 deletions

View File

@@ -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)

View File

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