mirror of
https://github.com/traefik/traefik
synced 2026-02-03 11:10:33 +00:00
feat: add global option to disable X-Forwarded-For appending
This commit is contained in:
@@ -83,6 +83,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
|||||||
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
|
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
|
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
|
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
|
||||||
|
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
|
||||||
| <a id="opt-entrypoints-name-forwardedheaders-trustedips" href="#opt-entrypoints-name-forwardedheaders-trustedips" title="#opt-entrypoints-name-forwardedheaders-trustedips">entrypoints._name_.forwardedheaders.trustedips</a> | Trust only forwarded headers from selected IPs. | |
|
| <a id="opt-entrypoints-name-forwardedheaders-trustedips" href="#opt-entrypoints-name-forwardedheaders-trustedips" title="#opt-entrypoints-name-forwardedheaders-trustedips">entrypoints._name_.forwardedheaders.trustedips</a> | Trust only forwarded headers from selected IPs. | |
|
||||||
| <a id="opt-entrypoints-name-http" href="#opt-entrypoints-name-http" title="#opt-entrypoints-name-http">entrypoints._name_.http</a> | HTTP configuration. | |
|
| <a id="opt-entrypoints-name-http" href="#opt-entrypoints-name-http" title="#opt-entrypoints-name-http">entrypoints._name_.http</a> | HTTP configuration. | |
|
||||||
| <a id="opt-entrypoints-name-http-encodequerysemicolons" href="#opt-entrypoints-name-http-encodequerysemicolons" title="#opt-entrypoints-name-http-encodequerysemicolons">entrypoints._name_.http.encodequerysemicolons</a> | Defines whether request query semicolons should be URLEncoded. | false |
|
| <a id="opt-entrypoints-name-http-encodequerysemicolons" href="#opt-entrypoints-name-http-encodequerysemicolons" title="#opt-entrypoints-name-http-encodequerysemicolons">entrypoints._name_.http.encodequerysemicolons</a> | Defines whether request query semicolons should be URLEncoded. | false |
|
||||||
@@ -141,6 +142,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
|||||||
| <a id="opt-experimental-plugins-name-settings-useunsafe" href="#opt-experimental-plugins-name-settings-useunsafe" title="#opt-experimental-plugins-name-settings-useunsafe">experimental.plugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe and syscall packages. | false |
|
| <a id="opt-experimental-plugins-name-settings-useunsafe" href="#opt-experimental-plugins-name-settings-useunsafe" title="#opt-experimental-plugins-name-settings-useunsafe">experimental.plugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe and syscall packages. | false |
|
||||||
| <a id="opt-experimental-plugins-name-version" href="#opt-experimental-plugins-name-version" title="#opt-experimental-plugins-name-version">experimental.plugins._name_.version</a> | plugin's version. | |
|
| <a id="opt-experimental-plugins-name-version" href="#opt-experimental-plugins-name-version" title="#opt-experimental-plugins-name-version">experimental.plugins._name_.version</a> | plugin's version. | |
|
||||||
| <a id="opt-global-checknewversion" href="#opt-global-checknewversion" title="#opt-global-checknewversion">global.checknewversion</a> | Periodically check if a new version has been released. | true |
|
| <a id="opt-global-checknewversion" href="#opt-global-checknewversion" title="#opt-global-checknewversion">global.checknewversion</a> | Periodically check if a new version has been released. | true |
|
||||||
|
| <a id="opt-global-notappendxforwardedfor" href="#opt-global-notappendxforwardedfor" title="#opt-global-notappendxforwardedfor">global.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
|
||||||
| <a id="opt-global-sendanonymoususage" href="#opt-global-sendanonymoususage" title="#opt-global-sendanonymoususage">global.sendanonymoususage</a> | Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default. | false |
|
| <a id="opt-global-sendanonymoususage" href="#opt-global-sendanonymoususage" title="#opt-global-sendanonymoususage">global.sendanonymoususage</a> | Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default. | false |
|
||||||
| <a id="opt-hostresolver" href="#opt-hostresolver" title="#opt-hostresolver">hostresolver</a> | Enable CNAME Flattening. | false |
|
| <a id="opt-hostresolver" href="#opt-hostresolver" title="#opt-hostresolver">hostresolver</a> | Enable CNAME Flattening. | false |
|
||||||
| <a id="opt-hostresolver-cnameflattening" href="#opt-hostresolver-cnameflattening" title="#opt-hostresolver-cnameflattening">hostresolver.cnameflattening</a> | A flag to enable/disable CNAME flattening | false |
|
| <a id="opt-hostresolver-cnameflattening" href="#opt-hostresolver-cnameflattening" title="#opt-hostresolver-cnameflattening">hostresolver.cnameflattening</a> | A flag to enable/disable CNAME flattening | false |
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ additionalArguments:
|
|||||||
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
|
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
|
||||||
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
|
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
|
||||||
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
|
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
|
||||||
|
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No |
|
||||||
| <a id="opt-http-redirections-entryPoint-to" href="#opt-http-redirections-entryPoint-to" title="#opt-http-redirections-entryPoint-to">`http.redirections.`<br />`entryPoint.to`</a> | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
|
| <a id="opt-http-redirections-entryPoint-to" href="#opt-http-redirections-entryPoint-to" title="#opt-http-redirections-entryPoint-to">`http.redirections.`<br />`entryPoint.to`</a> | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
|
||||||
| <a id="opt-http-redirections-entryPoint-scheme" href="#opt-http-redirections-entryPoint-scheme" title="#opt-http-redirections-entryPoint-scheme">`http.redirections.`<br />`entryPoint.scheme`</a> | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
|
| <a id="opt-http-redirections-entryPoint-scheme" href="#opt-http-redirections-entryPoint-scheme" title="#opt-http-redirections-entryPoint-scheme">`http.redirections.`<br />`entryPoint.scheme`</a> | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
|
||||||
| <a id="opt-http-redirections-entryPoint-permanent" href="#opt-http-redirections-entryPoint-permanent" title="#opt-http-redirections-entryPoint-permanent">`http.redirections.`<br />`entryPoint.permanent`</a> | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
|
| <a id="opt-http-redirections-entryPoint-permanent" href="#opt-http-redirections-entryPoint-permanent" title="#opt-http-redirections-entryPoint-permanent">`http.redirections.`<br />`entryPoint.permanent`</a> | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
[entryPoints.web.forwardedHeaders]
|
||||||
|
insecure = true
|
||||||
|
notAppendXForwardedFor = true
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .DynamicConfPath }}"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
[entryPoints.web.forwardedHeaders]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .DynamicConfPath }}"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
[entryPoints.web.forwardedHeaders]
|
||||||
|
insecure = true
|
||||||
|
notAppendXForwardedFor = true
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[experimental]
|
||||||
|
[experimental.fastProxy]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .DynamicConfPath }}"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
[entryPoints.web.forwardedHeaders]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[experimental]
|
||||||
|
[experimental.fastProxy]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .DynamicConfPath }}"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[http.routers]
|
||||||
|
[http.routers.router1]
|
||||||
|
entryPoints = ["web"]
|
||||||
|
rule = "PathPrefix(`/`)"
|
||||||
|
service = "service1"
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
[http.services.service1.loadBalancer]
|
||||||
|
[[http.services.service1.loadBalancer.servers]]
|
||||||
|
url = "{{ .Server }}"
|
||||||
@@ -94,6 +94,197 @@ func (s *SimpleSuite) TestSimpleFastProxy() {
|
|||||||
assert.GreaterOrEqual(s.T(), 1, callCount)
|
assert.GreaterOrEqual(s.T(), 1, callCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestXForwardedForDisabled() {
|
||||||
|
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// Echo back the X-Forwarded-For header
|
||||||
|
xff := req.Header.Get("X-Forwarded-For")
|
||||||
|
_, _ = rw.Write([]byte(xff))
|
||||||
|
}))
|
||||||
|
defer srv1.Close()
|
||||||
|
|
||||||
|
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
||||||
|
Server string
|
||||||
|
}{
|
||||||
|
Server: srv1.URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
staticConf := s.adaptFile("fixtures/x_forwarded_for.toml", struct {
|
||||||
|
DynamicConfPath string
|
||||||
|
}{
|
||||||
|
DynamicConfPath: dynamicConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.traefikCmd(withConfigFile(staticConf))
|
||||||
|
|
||||||
|
// Wait for Traefik to start
|
||||||
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Test with appendXForwardedFor = false
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Set an existing X-Forwarded-For header
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// The backend should receive the original X-Forwarded-For header unchanged
|
||||||
|
// (Traefik should NOT append RemoteAddr when appendXForwardedFor = false)
|
||||||
|
assert.Equal(s.T(), "1.2.3.4", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestXForwardedForEnabled() {
|
||||||
|
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// Echo back the X-Forwarded-For header
|
||||||
|
xff := req.Header.Get("X-Forwarded-For")
|
||||||
|
_, _ = rw.Write([]byte(xff))
|
||||||
|
}))
|
||||||
|
defer srv1.Close()
|
||||||
|
|
||||||
|
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
||||||
|
Server string
|
||||||
|
}{
|
||||||
|
Server: srv1.URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use a config with appendXForwardedFor = true
|
||||||
|
staticConf := s.adaptFile("fixtures/x_forwarded_for_enabled.toml", struct {
|
||||||
|
DynamicConfPath string
|
||||||
|
}{
|
||||||
|
DynamicConfPath: dynamicConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.traefikCmd(withConfigFile(staticConf))
|
||||||
|
|
||||||
|
// Wait for Traefik to start
|
||||||
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Test with default appendXForwardedFor = true
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Set an existing X-Forwarded-For header
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// The backend should receive the X-Forwarded-For header with RemoteAddr appended
|
||||||
|
// (should be "1.2.3.4, 127.0.0.1" since the request comes from localhost)
|
||||||
|
assert.Contains(s.T(), string(body), "1.2.3.4,")
|
||||||
|
assert.Contains(s.T(), string(body), "127.0.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestXForwardedForDisabledFastProxy() {
|
||||||
|
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// Verify FastProxy is being used
|
||||||
|
assert.Contains(s.T(), req.Header, "X-Traefik-Fast-Proxy")
|
||||||
|
|
||||||
|
// Echo back the X-Forwarded-For header
|
||||||
|
xff := req.Header.Get("X-Forwarded-For")
|
||||||
|
_, _ = rw.Write([]byte(xff))
|
||||||
|
}))
|
||||||
|
defer srv1.Close()
|
||||||
|
|
||||||
|
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
||||||
|
Server string
|
||||||
|
}{
|
||||||
|
Server: srv1.URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
staticConf := s.adaptFile("fixtures/x_forwarded_for_fastproxy.toml", struct {
|
||||||
|
DynamicConfPath string
|
||||||
|
}{
|
||||||
|
DynamicConfPath: dynamicConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.traefikCmd(withConfigFile(staticConf))
|
||||||
|
|
||||||
|
// Wait for Traefik to start
|
||||||
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Test with appendXForwardedFor = false
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Set an existing X-Forwarded-For header
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// The backend should receive the original X-Forwarded-For header unchanged
|
||||||
|
// (FastProxy should NOT append RemoteAddr when notAppendXForwardedFor = true)
|
||||||
|
assert.Equal(s.T(), "1.2.3.4", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestXForwardedForEnabledFastProxy() {
|
||||||
|
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// Verify FastProxy is being used
|
||||||
|
assert.Contains(s.T(), req.Header, "X-Traefik-Fast-Proxy")
|
||||||
|
|
||||||
|
// Echo back the X-Forwarded-For header
|
||||||
|
xff := req.Header.Get("X-Forwarded-For")
|
||||||
|
_, _ = rw.Write([]byte(xff))
|
||||||
|
}))
|
||||||
|
defer srv1.Close()
|
||||||
|
|
||||||
|
dynamicConf := s.adaptFile("resources/compose/x_forwarded_for.toml", struct {
|
||||||
|
Server string
|
||||||
|
}{
|
||||||
|
Server: srv1.URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use a config with appendXForwardedFor = false (default)
|
||||||
|
staticConf := s.adaptFile("fixtures/x_forwarded_for_fastproxy_enabled.toml", struct {
|
||||||
|
DynamicConfPath string
|
||||||
|
}{
|
||||||
|
DynamicConfPath: dynamicConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.traefikCmd(withConfigFile(staticConf))
|
||||||
|
|
||||||
|
// Wait for Traefik to start
|
||||||
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("service1"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Test with default appendXForwardedFor = true
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// Set an existing X-Forwarded-For header
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
// The backend should receive the X-Forwarded-For header with RemoteAddr appended
|
||||||
|
// (FastProxy should append RemoteAddr when notAppendXForwardedFor = false)
|
||||||
|
// (should be "1.2.3.4, 127.0.0.1" since the request comes from localhost)
|
||||||
|
assert.Contains(s.T(), string(body), "1.2.3.4,")
|
||||||
|
assert.Contains(s.T(), string(body), "127.0.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SimpleSuite) TestWithWebConfig() {
|
func (s *SimpleSuite) TestWithWebConfig() {
|
||||||
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))
|
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))
|
||||||
|
|
||||||
|
|||||||
@@ -128,9 +128,10 @@ type TLSConfig struct {
|
|||||||
|
|
||||||
// ForwardedHeaders Trust client forwarding headers.
|
// ForwardedHeaders Trust client forwarding headers.
|
||||||
type ForwardedHeaders struct {
|
type ForwardedHeaders struct {
|
||||||
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
||||||
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
|
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
|
||||||
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
|
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
|
||||||
|
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyProtocol contains Proxy-Protocol configuration.
|
// ProxyProtocol contains Proxy-Protocol configuration.
|
||||||
|
|||||||
@@ -112,8 +112,9 @@ type CertificateResolver struct {
|
|||||||
|
|
||||||
// Global holds the global configuration.
|
// Global holds the global configuration.
|
||||||
type Global struct {
|
type Global struct {
|
||||||
CheckNewVersion bool `description:"Periodically check if a new version has been released." json:"checkNewVersion,omitempty" toml:"checkNewVersion,omitempty" yaml:"checkNewVersion,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
CheckNewVersion bool `description:"Periodically check if a new version has been released." json:"checkNewVersion,omitempty" toml:"checkNewVersion,omitempty" yaml:"checkNewVersion,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
SendAnonymousUsage bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default." json:"sendAnonymousUsage,omitempty" toml:"sendAnonymousUsage,omitempty" yaml:"sendAnonymousUsage,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
SendAnonymousUsage bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default." json:"sendAnonymousUsage,omitempty" toml:"sendAnonymousUsage,omitempty" yaml:"sendAnonymousUsage,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServersTransport options to configure communication between Traefik and the servers.
|
// ServersTransport options to configure communication between Traefik and the servers.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/traefik/traefik/v3/pkg/ip"
|
"github.com/traefik/traefik/v3/pkg/ip"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
|
||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,16 +48,17 @@ var xHeaders = []string{
|
|||||||
// Unless insecure is set,
|
// Unless insecure is set,
|
||||||
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
|
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
|
||||||
type XForwarded struct {
|
type XForwarded struct {
|
||||||
insecure bool
|
insecure bool
|
||||||
trustedIPs []string
|
trustedIPs []string
|
||||||
connectionHeaders []string
|
connectionHeaders []string
|
||||||
ipChecker *ip.Checker
|
notAppendXForwardedFor bool
|
||||||
next http.Handler
|
ipChecker *ip.Checker
|
||||||
hostname string
|
next http.Handler
|
||||||
|
hostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewXForwarded creates a new XForwarded.
|
// NewXForwarded creates a new XForwarded.
|
||||||
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, next http.Handler) (*XForwarded, error) {
|
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) {
|
||||||
var ipChecker *ip.Checker
|
var ipChecker *ip.Checker
|
||||||
if len(trustedIPs) > 0 {
|
if len(trustedIPs) > 0 {
|
||||||
var err error
|
var err error
|
||||||
@@ -72,12 +74,13 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &XForwarded{
|
return &XForwarded{
|
||||||
insecure: insecure,
|
insecure: insecure,
|
||||||
trustedIPs: trustedIPs,
|
trustedIPs: trustedIPs,
|
||||||
connectionHeaders: connectionHeaders,
|
connectionHeaders: connectionHeaders,
|
||||||
ipChecker: ipChecker,
|
notAppendXForwardedFor: notAppendXForwardedFor,
|
||||||
next: next,
|
ipChecker: ipChecker,
|
||||||
hostname: hostname,
|
next: next,
|
||||||
|
hostname: hostname,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +201,10 @@ func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
x.removeConnectionHeaders(r)
|
x.removeConnectionHeaders(r)
|
||||||
|
|
||||||
|
if x.notAppendXForwardedFor {
|
||||||
|
r = r.WithContext(httputil.SetNotAppendXFF(r.Context()))
|
||||||
|
}
|
||||||
|
|
||||||
x.next.ServeHTTP(w, r)
|
x.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders,
|
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false,
|
||||||
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -655,7 +655,7 @@ func TestConnection(t *testing.T) {
|
|||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, nil)
|
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
|
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
|
||||||
|
|||||||
+13
-11
@@ -212,18 +212,20 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
outReq.Header.SetMethod(req.Method)
|
outReq.Header.SetMethod(req.Method)
|
||||||
|
|
||||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
if !proxyhttputil.ShouldNotAppendXFF(req.Context()) {
|
||||||
// If we aren't the first proxy retain prior
|
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||||
// X-Forwarded-For information as a comma+space
|
// If we aren't the first proxy retain prior
|
||||||
// separated list and fold multiple headers into one.
|
// X-Forwarded-For information as a comma+space
|
||||||
prior, ok := req.Header["X-Forwarded-For"]
|
// separated list and fold multiple headers into one.
|
||||||
if len(prior) > 0 {
|
prior, ok := req.Header["X-Forwarded-For"]
|
||||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
if len(prior) > 0 {
|
||||||
}
|
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||||
|
}
|
||||||
|
|
||||||
omit := ok && prior == nil // Go Issue 38079: nil now means don't populate the header
|
omit := ok && prior == nil // Go Issue 38079: nil now means don't populate the header
|
||||||
if !omit {
|
if !omit {
|
||||||
outReq.Header.Set("X-Forwarded-For", clientIP)
|
outReq.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/static"
|
"github.com/traefik/traefik/v3/pkg/config/static"
|
||||||
|
proxyhttputil "github.com/traefik/traefik/v3/pkg/proxy/httputil"
|
||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -406,6 +407,90 @@ func TestTransferEncodingChunked(t *testing.T) {
|
|||||||
assert.Equal(t, "chunk 0\nchunk 1\nchunk 2\n", string(body))
|
assert.Equal(t, "chunk 0\nchunk 1\nchunk 2\n", string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXForwardedFor(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
notAppendXFF bool
|
||||||
|
incomingXFF string
|
||||||
|
expectedXFF string
|
||||||
|
expectedXFFNotPresent bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "appends RemoteAddr when notAppendXFF is false",
|
||||||
|
notAppendXFF: false,
|
||||||
|
incomingXFF: "",
|
||||||
|
expectedXFF: "192.0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "appends RemoteAddr to existing XFF when notAppendXFF is false",
|
||||||
|
notAppendXFF: false,
|
||||||
|
incomingXFF: "203.0.113.1",
|
||||||
|
expectedXFF: "203.0.113.1, 192.0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "does not append RemoteAddr when notAppendXFF is true and no incoming XFF",
|
||||||
|
notAppendXFF: true,
|
||||||
|
incomingXFF: "",
|
||||||
|
expectedXFFNotPresent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "preserves existing XFF when notAppendXFF is true",
|
||||||
|
notAppendXFF: true,
|
||||||
|
incomingXFF: "203.0.113.1",
|
||||||
|
expectedXFF: "203.0.113.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "preserves multiple XFF values when notAppendXFF is true",
|
||||||
|
notAppendXFF: true,
|
||||||
|
incomingXFF: "203.0.113.1, 198.51.100.1",
|
||||||
|
expectedXFF: "203.0.113.1, 198.51.100.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
var receivedXFF string
|
||||||
|
var xffPresent bool
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
receivedXFF = req.Header.Get("X-Forwarded-For")
|
||||||
|
xffPresent = req.Header.Get("X-Forwarded-For") != "" || len(req.Header["X-Forwarded-For"]) > 0
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{})
|
||||||
|
|
||||||
|
proxyHandler, err := builder.Build("", testhelpers.MustParseURL(server.URL), true, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
if test.notAppendXFF {
|
||||||
|
ctx = proxyhttputil.SetNotAppendXFF(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.RemoteAddr = "192.0.2.1:12345"
|
||||||
|
|
||||||
|
if test.incomingXFF != "" {
|
||||||
|
req.Header.Set("X-Forwarded-For", test.incomingXFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
proxyHandler.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, res.Code)
|
||||||
|
|
||||||
|
if test.expectedXFFNotPresent {
|
||||||
|
assert.False(t, xffPresent, "X-Forwarded-For header should not be present")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, test.expectedXFF, receivedXFF)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type transportManagerMock struct {
|
type transportManagerMock struct {
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-19
@@ -19,17 +19,41 @@ import (
|
|||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type key string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
|
// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
|
||||||
StatusClientClosedRequest = 499
|
StatusClientClosedRequest = 499
|
||||||
|
|
||||||
// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
|
// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
|
||||||
StatusClientClosedRequestText = "Client Closed Request"
|
StatusClientClosedRequestText = "Client Closed Request"
|
||||||
|
|
||||||
|
notAppendXFFKey key = "NotAppendXFF"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetNotAppendXFF indicates xff should not be appended.
|
||||||
|
func SetNotAppendXFF(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, notAppendXFFKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldNotAppendXFF returns whether X-Forwarded-For should not be appended.
|
||||||
|
func ShouldNotAppendXFF(ctx context.Context) bool {
|
||||||
|
val := ctx.Value(notAppendXFFKey)
|
||||||
|
if val == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
notAppendXFF, ok := val.(bool)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return notAppendXFF
|
||||||
|
}
|
||||||
|
|
||||||
func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler {
|
func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler {
|
||||||
return &httputil.ReverseProxy{
|
return &httputil.ReverseProxy{
|
||||||
Director: directorBuilder(target, passHostHeader, preservePath),
|
Rewrite: rewriteRequestBuilder(target, passHostHeader, preservePath),
|
||||||
Transport: roundTripper,
|
Transport: roundTripper,
|
||||||
FlushInterval: flushInterval,
|
FlushInterval: flushInterval,
|
||||||
BufferPool: bufferPool,
|
BufferPool: bufferPool,
|
||||||
@@ -38,45 +62,82 @@ func buildSingleHostProxy(target *url.URL, passHostHeader bool, preservePath boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func directorBuilder(target *url.URL, passHostHeader bool, preservePath bool) func(req *http.Request) {
|
func rewriteRequestBuilder(target *url.URL, passHostHeader bool, preservePath bool) func(*httputil.ProxyRequest) {
|
||||||
return func(outReq *http.Request) {
|
return func(pr *httputil.ProxyRequest) {
|
||||||
outReq.URL.Scheme = target.Scheme
|
copyForwardedHeader(pr.Out.Header, pr.In.Header)
|
||||||
outReq.URL.Host = target.Host
|
if !ShouldNotAppendXFF(pr.In.Context()) {
|
||||||
|
if clientIP, _, err := net.SplitHostPort(pr.In.RemoteAddr); err == nil {
|
||||||
|
// If we aren't the first proxy retain prior
|
||||||
|
// X-Forwarded-For information as a comma+space
|
||||||
|
// separated list and fold multiple headers into one.
|
||||||
|
prior, ok := pr.Out.Header["X-Forwarded-For"]
|
||||||
|
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
||||||
|
if len(prior) > 0 {
|
||||||
|
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||||
|
}
|
||||||
|
if !omit {
|
||||||
|
pr.Out.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
u := outReq.URL
|
pr.Out.URL.Scheme = target.Scheme
|
||||||
if outReq.RequestURI != "" {
|
pr.Out.URL.Host = target.Host
|
||||||
parsedURL, err := url.ParseRequestURI(outReq.RequestURI)
|
|
||||||
|
u := pr.Out.URL
|
||||||
|
if pr.Out.RequestURI != "" {
|
||||||
|
parsedURL, err := url.ParseRequestURI(pr.Out.RequestURI)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
u = parsedURL
|
u = parsedURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outReq.URL.Path = u.Path
|
pr.Out.URL.Path = u.Path
|
||||||
outReq.URL.RawPath = u.RawPath
|
pr.Out.URL.RawPath = u.RawPath
|
||||||
|
|
||||||
if preservePath {
|
if preservePath {
|
||||||
outReq.URL.Path, outReq.URL.RawPath = JoinURLPath(target, u)
|
pr.Out.URL.Path, pr.Out.URL.RawPath = JoinURLPath(target, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
|
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
|
||||||
outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
pr.Out.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
||||||
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
pr.Out.RequestURI = "" // Outgoing request should not have RequestURI
|
||||||
|
|
||||||
outReq.Proto = "HTTP/1.1"
|
pr.Out.Proto = "HTTP/1.1"
|
||||||
outReq.ProtoMajor = 1
|
pr.Out.ProtoMajor = 1
|
||||||
outReq.ProtoMinor = 1
|
pr.Out.ProtoMinor = 1
|
||||||
|
|
||||||
// Do not pass client Host header unless option PassHostHeader is set.
|
// Do not pass client Host header unless option PassHostHeader is set.
|
||||||
if !passHostHeader {
|
if !passHostHeader {
|
||||||
outReq.Host = outReq.URL.Host
|
pr.Out.Host = pr.Out.URL.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
if isWebSocketUpgrade(outReq) {
|
if isWebSocketUpgrade(pr.Out) {
|
||||||
cleanWebSocketHeaders(outReq)
|
cleanWebSocketHeaders(pr.Out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyForwardedHeader copies header that are removed by the reverseProxy when a rewriteRequest is used.
|
||||||
|
func copyForwardedHeader(dst, src http.Header) {
|
||||||
|
prior, ok := src["X-Forwarded-For"]
|
||||||
|
if ok {
|
||||||
|
dst["X-Forwarded-For"] = prior
|
||||||
|
}
|
||||||
|
prior, ok = src["Forwarded"]
|
||||||
|
if ok {
|
||||||
|
dst["Forwarded"] = prior
|
||||||
|
}
|
||||||
|
prior, ok = src["X-Forwarded-Host"]
|
||||||
|
if ok {
|
||||||
|
dst["X-Forwarded-Host"] = prior
|
||||||
|
}
|
||||||
|
prior, ok = src["X-Forwarded-Proto"]
|
||||||
|
if ok {
|
||||||
|
dst["X-Forwarded-Proto"] = prior
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// cleanWebSocketHeaders Even if the websocket RFC says that headers should be case-insensitive,
|
// cleanWebSocketHeaders Even if the websocket RFC says that headers should be case-insensitive,
|
||||||
// some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept,
|
// some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept,
|
||||||
// Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive.
|
// Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ import (
|
|||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_directorBuilder(t *testing.T) {
|
func Test_rewriteRequestBuilder(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
target *url.URL
|
target *url.URL
|
||||||
@@ -25,6 +26,7 @@ func Test_directorBuilder(t *testing.T) {
|
|||||||
expectedPath string
|
expectedPath string
|
||||||
expectedRawPath string
|
expectedRawPath string
|
||||||
expectedQuery string
|
expectedQuery string
|
||||||
|
notAppendXFF bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Basic proxy",
|
name: "Basic proxy",
|
||||||
@@ -37,6 +39,18 @@ func Test_directorBuilder(t *testing.T) {
|
|||||||
expectedPath: "/test",
|
expectedPath: "/test",
|
||||||
expectedQuery: "param=value",
|
expectedQuery: "param=value",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Basic proxy - notAppendXFF",
|
||||||
|
target: testhelpers.MustParseURL("http://example.com"),
|
||||||
|
passHostHeader: false,
|
||||||
|
preservePath: false,
|
||||||
|
incomingURL: "http://localhost/test?param=value",
|
||||||
|
expectedScheme: "http",
|
||||||
|
expectedHost: "example.com",
|
||||||
|
expectedPath: "/test",
|
||||||
|
expectedQuery: "param=value",
|
||||||
|
notAppendXFF: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "HTTPS target",
|
name: "HTTPS target",
|
||||||
target: testhelpers.MustParseURL("https://secure.example.com"),
|
target: testhelpers.MustParseURL("https://secure.example.com"),
|
||||||
@@ -85,21 +99,41 @@ func Test_directorBuilder(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
director := directorBuilder(test.target, test.passHostHeader, test.preservePath)
|
rewriteRequest := rewriteRequestBuilder(test.target, test.passHostHeader, test.preservePath)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody)
|
ctx := t.Context()
|
||||||
director(req)
|
if test.notAppendXFF {
|
||||||
|
ctx = SetNotAppendXFF(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, test.expectedScheme, req.URL.Scheme)
|
reqIn := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody)
|
||||||
assert.Equal(t, test.expectedHost, req.Host)
|
reqIn = reqIn.WithContext(ctx)
|
||||||
assert.Equal(t, test.expectedPath, req.URL.Path)
|
reqIn.Header.Add("X-Forwarded-For", "1.2.3.4")
|
||||||
assert.Equal(t, test.expectedRawPath, req.URL.RawPath)
|
reqIn.RemoteAddr = "127.0.0.1:1234"
|
||||||
assert.Equal(t, test.expectedQuery, req.URL.RawQuery)
|
|
||||||
assert.Empty(t, req.RequestURI)
|
reqOut := httptest.NewRequest(http.MethodGet, test.incomingURL, http.NoBody)
|
||||||
assert.Equal(t, "HTTP/1.1", req.Proto)
|
pr := &httputil.ProxyRequest{
|
||||||
assert.Equal(t, 1, req.ProtoMajor)
|
In: reqIn,
|
||||||
assert.Equal(t, 1, req.ProtoMinor)
|
Out: reqOut,
|
||||||
assert.False(t, !test.passHostHeader && req.Host != req.URL.Host)
|
}
|
||||||
|
rewriteRequest(pr)
|
||||||
|
|
||||||
|
if test.notAppendXFF {
|
||||||
|
assert.Equal(t, "1.2.3.4", reqOut.Header.Get("X-Forwarded-For"))
|
||||||
|
} else {
|
||||||
|
// When not disabled, X-Forwarded-For should have RemoteAddr appended
|
||||||
|
assert.Equal(t, "1.2.3.4, 127.0.0.1", reqOut.Header.Get("X-Forwarded-For"))
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.expectedScheme, reqOut.URL.Scheme)
|
||||||
|
assert.Equal(t, test.expectedHost, reqOut.Host)
|
||||||
|
assert.Equal(t, test.expectedPath, reqOut.URL.Path)
|
||||||
|
assert.Equal(t, test.expectedRawPath, reqOut.URL.RawPath)
|
||||||
|
assert.Equal(t, test.expectedQuery, reqOut.URL.RawQuery)
|
||||||
|
assert.Empty(t, reqOut.RequestURI)
|
||||||
|
assert.Equal(t, "HTTP/1.1", reqOut.Proto)
|
||||||
|
assert.Equal(t, 1, reqOut.ProtoMajor)
|
||||||
|
assert.Equal(t, 1, reqOut.ProtoMinor)
|
||||||
|
assert.False(t, !test.passHostHeader && reqOut.Host != reqOut.URL.Host)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -650,6 +650,7 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
|
|||||||
configuration.ForwardedHeaders.Insecure,
|
configuration.ForwardedHeaders.Insecure,
|
||||||
configuration.ForwardedHeaders.TrustedIPs,
|
configuration.ForwardedHeaders.TrustedIPs,
|
||||||
configuration.ForwardedHeaders.Connection,
|
configuration.ForwardedHeaders.Connection,
|
||||||
|
configuration.ForwardedHeaders.NotAppendXForwardedFor,
|
||||||
next)
|
next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in New Issue
Block a user