Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trust Forwarded headers from the peer socket for PROXY Protocol connection #10492

Open
wants to merge 11 commits into
base: v2.11
Choose a base branch
from
5 changes: 5 additions & 0 deletions docs/content/middlewares/http/inflightreq.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ The `ipStrategy` option defines two parameters that configures how Traefik deter

!!! important "As a middleware, InFlightReq happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through the middleware. Therefore, during InFlightReq, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be used and/or relied upon."

!!! important PROXY Protocol

If no strategy is set, the default is to use the request's remote address field (as an ipStrategy).
In case of a PROXY Protocol connection, the request's remote address is the PROXY Protocol header `sourceAddr` value.

##### `ipStrategy.depth`

The `depth` option tells Traefik to use the `X-Forwarded-For` header and select the IP located at the `depth` position (starting from the right).
Expand Down
5 changes: 5 additions & 0 deletions docs/content/middlewares/http/ipallowlist.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ If no strategy is set, the default behavior is to match `sourceRange` against th

!!! important "As a middleware, whitelisting happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through whitelisting. Therefore, during whitelisting, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be matched against `sourceRange`."

!!! important PROXY Protocol

If no strategy is set, the default is to use the request's remote address field (as an ipStrategy).
In case of a PROXY Protocol connection, the request's remote address is the PROXY Protocol header `sourceAddr` value.

#### `ipStrategy.depth`

The `depth` option tells Traefik to use the `X-Forwarded-For` header and take the IP located at the `depth` position (starting from the right).
Expand Down
5 changes: 5 additions & 0 deletions docs/content/middlewares/http/ipwhitelist.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ If no strategy is set, the default behavior is to match `sourceRange` against th

!!! important "As a middleware, whitelisting happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through whitelisting. Therefore, during whitelisting, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be matched against `sourceRange`."

!!! important PROXY Protocol

If no strategy is set, the default is to use the request's remote address field (as an ipStrategy).
In case of a PROXY Protocol connection, the request's remote address is the PROXY Protocol header `sourceAddr` value.

#### `ipStrategy.depth`

The `depth` option tells Traefik to use the `X-Forwarded-For` header and take the IP located at the `depth` position (starting from the right).
Expand Down
4 changes: 4 additions & 0 deletions docs/content/middlewares/http/ratelimit.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ The `sourceCriterion` option defines what criterion is used to group requests as
If several strategies are defined at the same time, an error will be raised.
If none are set, the default is to use the request's remote address field (as an `ipStrategy`).

!!! important PROXY Protocol

In case of a PROXY Protocol connection, the request's remote address is the PROXY Protocol header `sourceAddr` value.

#### `sourceCriterion.ipStrategy`

The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`.
Expand Down
5 changes: 5 additions & 0 deletions docs/content/routing/entrypoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
--entryPoints.web.forwardedHeaders.trustedIPs=127.0.0.1/32,192.168.1.7
```

??? warning "`forwardedHeaders.trustedIPs` with PROXY Protocol"

Configured IPs are checked against the PROXY Protocol header's `sourceAddr` value (if any),
and also against the peer socket address.

??? info "`forwardedHeaders.insecure`"

Insecure Mode (Always Trusting Forwarded Headers).
Expand Down
12 changes: 12 additions & 0 deletions integration/fixtures/proxy-protocol/proxy-protocol.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@
address = ":8000"
[entryPoints.trust.proxyProtocol]
trustedIPs = ["127.0.0.1"]
[entryPoints.trust.forwardedHeaders]
trustedIPs = ["127.0.0.1"]

[entryPoints.trustPROXY]
address = ":8001"
[entryPoints.trustPROXY.proxyProtocol]
trustedIPs = ["127.0.0.1"]
[entryPoints.trustPROXY.forwardedHeaders]
trustedIPs = ["1.2.3.4"]

[entryPoints.nottrust]
address = ":9000"
[entryPoints.nottrust.proxyProtocol]
trustedIPs = ["1.2.3.4"]
[entryPoints.nottrust.forwardedHeaders]
trustedIPs = ["1.2.3.4"]

[api]
insecure = true
Expand Down
38 changes: 28 additions & 10 deletions integration/proxy_protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ func (s *ProxyProtocolSuite) TearDownSuite() {
}

func (s *ProxyProtocolSuite) TestProxyProtocolTrusted() {
file := s.adaptFile("fixtures/proxy-protocol/proxy-protocol.toml", struct {
HaproxyIP string
WhoamiIP string
}{WhoamiIP: s.whoamiIP})
file := s.adaptFile("fixtures/proxy-protocol/proxy-protocol.toml", struct{ WhoamiIP string }{
WhoamiIP: s.whoamiIP,
})

s.traefikCmd(withConfigFile(file))

Expand All @@ -48,18 +47,36 @@ func (s *ProxyProtocolSuite) TestProxyProtocolTrusted() {

content, err := proxyProtoRequest("127.0.0.1:8000", 1)
require.NoError(s.T(), err)
assert.Contains(s.T(), content, "X-Forwarded-For: 1.2.3.4")
assert.Contains(s.T(), content, "X-Forwarded-For: 5.6.7.8, 127.0.0.1")

content, err = proxyProtoRequest("127.0.0.1:8000", 2)
require.NoError(s.T(), err)
assert.Contains(s.T(), content, "X-Forwarded-For: 1.2.3.4")
assert.Contains(s.T(), content, "X-Forwarded-For: 5.6.7.8, 127.0.0.1")
}

func (s *ProxyProtocolSuite) TestProxyProtocolPROXYForwardedHeadersTrusted() {
file := s.adaptFile("fixtures/proxy-protocol/proxy-protocol.toml", struct{ WhoamiIP string }{
WhoamiIP: s.whoamiIP,
})

s.traefikCmd(withConfigFile(file))

err := try.GetRequest("http://127.0.0.1:8001/whoami", 10*time.Second)
require.NoError(s.T(), err)

content, err := proxyProtoRequest("127.0.0.1:8001", 1)
require.NoError(s.T(), err)
assert.Contains(s.T(), content, "X-Forwarded-For: 5.6.7.8, 1.2.3.4")

content, err = proxyProtoRequest("127.0.0.1:8001", 2)
require.NoError(s.T(), err)
assert.Contains(s.T(), content, "X-Forwarded-For: 5.6.7.8, 1.2.3.4")
}

func (s *ProxyProtocolSuite) TestProxyProtocolNotTrusted() {
file := s.adaptFile("fixtures/proxy-protocol/proxy-protocol.toml", struct {
HaproxyIP string
WhoamiIP string
}{WhoamiIP: s.whoamiIP})
file := s.adaptFile("fixtures/proxy-protocol/proxy-protocol.toml", struct{ WhoamiIP string }{
WhoamiIP: s.whoamiIP,
})

s.traefikCmd(withConfigFile(file))

Expand Down Expand Up @@ -108,6 +125,7 @@ func proxyProtoRequest(address string, version byte) (string, error) {
request := "GET /whoami HTTP/1.1\r\n" +
"Host: 127.0.0.1\r\n" +
"Connection: close\r\n" +
"X-Forwarded-For: 5.6.7.8\r\n" +
"\r\n"

// Write the HTTP request to the TCP connection
Expand Down
25 changes: 24 additions & 1 deletion pkg/middlewares/forwardedheaders/forwarded_header.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package forwardedheaders

import (
"context"
"net"
"net/http"
"os"
Expand Down Expand Up @@ -37,6 +38,14 @@ var xHeaders = []string{
xRealIP,
}

type key string

// PeerSocketAddrKey is the peer socket address which only exists in case of a proxy proto connection.
const PeerSocketAddrKey key = "peerSocketAddr"

// XForwardedForAddr is the previous hop address (trusted) to be added to the X-Forwarded-For header.
const XForwardedForAddr key = "xForwardedForAddr"

// XForwarded is an HTTP handler wrapper that sets the X-Forwarded headers,
// and other relevant headers for a reverse-proxy.
// Unless insecure is set,
Expand Down Expand Up @@ -181,12 +190,26 @@ func (x *XForwarded) rewrite(outreq *http.Request) {

// ServeHTTP implements http.Handler.
func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !x.insecure && !x.isTrustedIP(r.RemoteAddr) {
var isTrusted bool
forwardedForAddr := r.RemoteAddr
if x.isTrustedIP(r.RemoteAddr) {
isTrusted = true

// In case of a ProxyProtocol connection the http.Request#RemoteAddr is the original one.
// To check if Forwarded headers are trusted we have to use the peer socket address.
} else if peerSocketAddr, ok := r.Context().Value(PeerSocketAddrKey).(string); ok && x.isTrustedIP(peerSocketAddr) {
isTrusted = true
forwardedForAddr = peerSocketAddr
}

if !x.insecure && !isTrusted {
for _, h := range xHeaders {
unsafeHeader(r.Header).Del(h)
}
}

r = r.WithContext(context.WithValue(r.Context(), XForwardedForAddr, forwardedForAddr))

x.rewrite(r)

x.next.ServeHTTP(w, r)
Expand Down
48 changes: 48 additions & 0 deletions pkg/middlewares/forwardedheaders/forwarded_header_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package forwardedheaders

import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
Expand All @@ -21,6 +22,7 @@ func TestServeHTTP(t *testing.T) {
tls bool
websocket bool
host string
peerSocketAddr string
}{
{
desc: "all Empty",
Expand Down Expand Up @@ -269,6 +271,48 @@ func TestServeHTTP(t *testing.T) {
xForwardedServer: "foo.com:8080",
},
},
{
desc: "insecure false with incoming X-Forwarded headers and valid Trusted IP with PeerSocketAddr in context",
insecure: false,
trustedIps: []string{"10.0.1.100"},
remoteAddr: "10.0.1.101:80",
peerSocketAddr: "10.0.1.100:80",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
},
expectedHeaders: map[string]string{
xForwardedFor: "10.0.1.0, 10.0.1.12",
xForwardedURI: "/bar",
xForwardedMethod: "GET",
xForwardedTLSClientCert: "Cert",
xForwardedTLSClientCertInfo: "CertInfo",
},
},
{
desc: "insecure false with incoming X-Forwarded headers and invalid Trusted IP with PeerSocketAddr in context",
insecure: false,
trustedIps: []string{"10.0.1.102"},
remoteAddr: "10.0.1.100:80",
peerSocketAddr: "10.0.1.101:80",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
},
expectedHeaders: map[string]string{
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
},
},
}

for _, test := range testCases {
Expand All @@ -280,6 +324,10 @@ func TestServeHTTP(t *testing.T) {

req.RemoteAddr = test.remoteAddr

if test.peerSocketAddr != "" {
req = req.WithContext(context.WithValue(req.Context(), PeerSocketAddrKey, test.peerSocketAddr))
}

if test.tls {
req.TLS = &tls.ConnectionState{}
}
Expand Down
32 changes: 27 additions & 5 deletions pkg/server/server_entrypoint_tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,25 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
}
}

// writeCloserWrapper wraps together a connection, and the concrete underlying
// proxyProtoConn wraps together a connection, and the concrete underlying
// connection type that was found to satisfy WriteCloser.
type writeCloserWrapper struct {
type proxyProtoConn struct {
net.Conn
writeCloser tcp.WriteCloser
remoteAddr proxyProtoAddr
}

func (c *writeCloserWrapper) CloseWrite() error {
return c.writeCloser.CloseWrite()
type proxyProtoAddr struct {
net.Addr
peerSocketAddr string
}

func (p *proxyProtoConn) CloseWrite() error {
return p.writeCloser.CloseWrite()
}

func (p *proxyProtoConn) RemoteAddr() net.Addr {
return p.remoteAddr
}

// writeCloser returns the given connection, augmented with the WriteCloser
Expand All @@ -383,7 +393,15 @@ func writeCloser(conn net.Conn) (tcp.WriteCloser, error) {
if !ok {
return nil, errors.New("underlying connection is not a tcp connection")
}
return &writeCloserWrapper{writeCloser: underlying, Conn: typedConn}, nil

return &proxyProtoConn{
Conn: typedConn,
writeCloser: underlying,
remoteAddr: proxyProtoAddr{
Addr: typedConn.RemoteAddr(),
peerSocketAddr: typedConn.Raw().RemoteAddr().String(),
},
}, nil
case *net.TCPConn:
return typedConn, nil
default:
Expand Down Expand Up @@ -616,6 +634,10 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati

prevConnContext := serverHTTP.ConnContext
serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
if proxyProtoAddr, ok := c.RemoteAddr().(proxyProtoAddr); ok {
ctx = context.WithValue(ctx, forwardedheaders.PeerSocketAddrKey, proxyProtoAddr.peerSocketAddr)
}

// This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM.
ctx = service.AddTransportOnContext(ctx)
if prevConnContext != nil {
Expand Down
Loading
Loading