From d7185fd002a2c57d20a98d3e94442da7b259b3ad Mon Sep 17 00:00:00 2001 From: Chris Seufert Date: Thu, 16 Oct 2025 13:47:32 +1100 Subject: [PATCH] caddyhttp: Add `trusted_proxies_unix` for trusting unix socket `X-Forwarded-*` headers (#7265) --- caddyconfig/httpcaddyfile/serveroptions.go | 8 +++ ...e_proxy_trusted_proxies_unix.caddyfiletest | 59 +++++++++++++++++++ modules/caddyhttp/server.go | 18 ++++++ modules/caddyhttp/server_test.go | 33 +++++++++++ 4 files changed, 118 insertions(+) create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 5a04d0daf..9431f1aed 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -51,6 +51,7 @@ type serverOptions struct { StrictSNIHost *bool TrustedProxiesRaw json.RawMessage TrustedProxiesStrict int + TrustedProxiesUnix bool ClientIPHeaders []string ShouldLogCredentials bool Metrics *caddyhttp.Metrics @@ -251,6 +252,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { } serverOpts.TrustedProxiesStrict = 1 + case "trusted_proxies_unix": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.TrustedProxiesUnix = true + case "client_ip_headers": headers := d.RemainingArgs() for _, header := range headers { @@ -342,6 +349,7 @@ func applyServerOptions( server.TrustedProxiesRaw = opts.TrustedProxiesRaw server.ClientIPHeaders = opts.ClientIPHeaders server.TrustedProxiesStrict = opts.TrustedProxiesStrict + server.TrustedProxiesUnix = opts.TrustedProxiesUnix server.Metrics = opts.Metrics if opts.ShouldLogCredentials { if server.Logs == nil { diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest new file mode 100644 index 000000000..8f7175124 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest @@ -0,0 +1,59 @@ +{ + servers { + trusted_proxies_unix + } +} + +example.com { + reverse_proxy https://local:8080 +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http", + "tls": {} + }, + "upstreams": [ + { + "dial": "local:8080" + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "trusted_proxies_unix": true + } + } + } + } +} diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 7d6ac8b90..c195857ba 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -202,6 +202,13 @@ type Server struct { // This option is disabled by default. TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"` + // If greater than zero, enables trusting socket connections + // (e.g. Unix domain sockets) as coming from a trusted + // proxy. + // + // This option is disabled by default. + TrustedProxiesUnix bool `json:"trusted_proxies_unix,omitempty"` + // Enables access logging and configures how access logs are handled // in this server. To minimally enable access logs, simply set this // to a non-null, empty struct. @@ -941,6 +948,17 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) { return false, "" } + if s.TrustedProxiesUnix && r.RemoteAddr == "@" { + if s.TrustedProxiesStrict > 0 { + ipRanges := []netip.Prefix{} + if s.trustedProxies != nil { + ipRanges = s.trustedProxies.GetIPRanges(r) + } + return true, strictUntrustedClientIp(r, s.ClientIPHeaders, ipRanges, "@") + } else { + return true, trustedRealClientIP(r, s.ClientIPHeaders, "@") + } + } // Parse the remote IP, ignore the error as non-fatal, // but the remote IP is required to continue, so we // just return early. This should probably never happen diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go index d47f55c63..eecb392e4 100644 --- a/modules/caddyhttp/server_test.go +++ b/modules/caddyhttp/server_test.go @@ -297,6 +297,39 @@ func TestServer_DetermineTrustedProxy_TrustedLoopback(t *testing.T) { assert.Equal(t, clientIP, "31.40.0.10") } +func TestServer_DetermineTrustedProxy_UnixSocket(t *testing.T) { + server := &Server{ + ClientIPHeaders: []string{"X-Forwarded-For"}, + TrustedProxiesUnix: true, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "@" + req.Header.Set("X-Forwarded-For", "2.2.2.2, 3.3.3.3") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, "2.2.2.2", clientIP) +} + +func TestServer_DetermineTrustedProxy_UnixSocketStrict(t *testing.T) { + server := &Server{ + ClientIPHeaders: []string{"X-Forwarded-For"}, + TrustedProxiesUnix: true, + TrustedProxiesStrict: 1, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "@" + req.Header.Set("X-Forwarded-For", "2.2.2.2, 3.3.3.3") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, "3.3.3.3", clientIP) +} + func TestServer_DetermineTrustedProxy_UntrustedPrefix(t *testing.T) { loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")