Add support for X-Forwarded-Proto (#9472)

rewrite XForwardedForRequest to set `isSecure()` based on
`X-Forwarded-Proto`. Also implement `getClientAddress()` while we're here.
This commit is contained in:
Richard van der Hoff 2021-02-24 18:11:33 +00:00 committed by GitHub
parent 00bf80cb8e
commit d8e95e5452
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 28 deletions

1
changelog.d/9472.feature Normal file
View file

@ -0,0 +1 @@
Add support for `X-Forwarded-Proto` header when using a reverse proxy. Administrators using a reverse proxy should ensure this header is set to avoid warnings. See [docs/workers.md](docs/workers.md) for example configurations.

View file

@ -9,23 +9,23 @@ of doing so is that it means that you can expose the default https port
(443) to Matrix clients without needing to run Synapse with root (443) to Matrix clients without needing to run Synapse with root
privileges. privileges.
**NOTE**: Your reverse proxy must not `canonicalise` or `normalise` You should configure your reverse proxy to forward requests to `/_matrix` or
the requested URI in any way (for example, by decoding `%xx` escapes). `/_synapse/client` to Synapse, and have it set the `X-Forwarded-For` and
Beware that Apache *will* canonicalise URIs unless you specify `X-Forwarded-Proto` request headers.
`nocanon`.
When setting up a reverse proxy, remember that Matrix clients and other You should remember that Matrix clients and other Matrix servers do not
Matrix servers do not necessarily need to connect to your server via the necessarily need to connect to your server via the same server name or
same server name or port. Indeed, clients will use port 443 by default, port. Indeed, clients will use port 443 by default, whereas servers default to
whereas servers default to port 8448. Where these are different, we port 8448. Where these are different, we refer to the 'client port' and the
refer to the 'client port' and the 'federation port'. See [the Matrix 'federation port'. See [the Matrix
specification](https://matrix.org/docs/spec/server_server/latest#resolving-server-names) specification](https://matrix.org/docs/spec/server_server/latest#resolving-server-names)
for more details of the algorithm used for federation connections, and for more details of the algorithm used for federation connections, and
[delegate.md](<delegate.md>) for instructions on setting up delegation. [delegate.md](<delegate.md>) for instructions on setting up delegation.
Endpoints that are part of the standardised Matrix specification are **NOTE**: Your reverse proxy must not `canonicalise` or `normalise`
located under `/_matrix`, whereas endpoints specific to Synapse are the requested URI in any way (for example, by decoding `%xx` escapes).
located under `/_synapse/client`. Beware that Apache *will* canonicalise URIs unless you specify
`nocanon`.
Let's assume that we expect clients to connect to our server at Let's assume that we expect clients to connect to our server at
`https://matrix.example.com`, and other servers to connect at `https://matrix.example.com`, and other servers to connect at
@ -52,6 +52,7 @@ server {
location ~* ^(\/_matrix|\/_synapse\/client) { location ~* ^(\/_matrix|\/_synapse\/client) {
proxy_pass http://localhost:8008; proxy_pass http://localhost:8008;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
# Nginx by default only allows file uploads up to 1M in size # Nginx by default only allows file uploads up to 1M in size
# Increase client_max_body_size to match max_upload_size defined in homeserver.yaml # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
client_max_body_size 50M; client_max_body_size 50M;
@ -102,6 +103,7 @@ example.com:8448 {
SSLEngine on SSLEngine on
ServerName matrix.example.com; ServerName matrix.example.com;
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
AllowEncodedSlashes NoDecode AllowEncodedSlashes NoDecode
ProxyPass /_matrix http://127.0.0.1:8008/_matrix nocanon ProxyPass /_matrix http://127.0.0.1:8008/_matrix nocanon
ProxyPassReverse /_matrix http://127.0.0.1:8008/_matrix ProxyPassReverse /_matrix http://127.0.0.1:8008/_matrix
@ -113,6 +115,7 @@ example.com:8448 {
SSLEngine on SSLEngine on
ServerName example.com; ServerName example.com;
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
AllowEncodedSlashes NoDecode AllowEncodedSlashes NoDecode
ProxyPass /_matrix http://127.0.0.1:8008/_matrix nocanon ProxyPass /_matrix http://127.0.0.1:8008/_matrix nocanon
ProxyPassReverse /_matrix http://127.0.0.1:8008/_matrix ProxyPassReverse /_matrix http://127.0.0.1:8008/_matrix
@ -134,6 +137,9 @@ example.com:8448 {
``` ```
frontend https frontend https
bind :::443 v4v6 ssl crt /etc/ssl/haproxy/ strict-sni alpn h2,http/1.1 bind :::443 v4v6 ssl crt /etc/ssl/haproxy/ strict-sni alpn h2,http/1.1
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
http-request set-header X-Forwarded-For %[src]
# Matrix client traffic # Matrix client traffic
acl matrix-host hdr(host) -i matrix.example.com acl matrix-host hdr(host) -i matrix.example.com
@ -144,6 +150,10 @@ frontend https
frontend matrix-federation frontend matrix-federation
bind :::8448 v4v6 ssl crt /etc/ssl/haproxy/synapse.pem alpn h2,http/1.1 bind :::8448 v4v6 ssl crt /etc/ssl/haproxy/synapse.pem alpn h2,http/1.1
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
http-request set-header X-Forwarded-For %[src]
default_backend matrix default_backend matrix
backend matrix backend matrix

View file

@ -16,6 +16,10 @@ import logging
import time import time
from typing import Optional, Union from typing import Optional, Union
import attr
from zope.interface import implementer
from twisted.internet.interfaces import IAddress
from twisted.python.failure import Failure from twisted.python.failure import Failure
from twisted.web.server import Request, Site from twisted.web.server import Request, Site
@ -333,27 +337,78 @@ class SynapseRequest(Request):
class XForwardedForRequest(SynapseRequest): class XForwardedForRequest(SynapseRequest):
def __init__(self, *args, **kw): """Request object which honours proxy headers
SynapseRequest.__init__(self, *args, **kw)
""" Extends SynapseRequest to replace getClientIP, getClientAddress, and isSecure with
Add a layer on top of another request that only uses the value of an information from request headers.
X-Forwarded-For header as the result of C{getClientIP}.
""" """
def getClientIP(self): # the client IP and ssl flag, as extracted from the headers.
""" _forwarded_for = None # type: Optional[_XForwardedForAddress]
@return: The client address (the first address) in the value of the _forwarded_https = False # type: bool
I{X-Forwarded-For header}. If the header is not present, return
C{b"-"}. def requestReceived(self, command, path, version):
""" # this method is called by the Channel once the full request has been
return ( # received, to dispatch the request to a resource.
self.requestHeaders.getRawHeaders(b"x-forwarded-for", [b"-"])[0] # We can use it to set the IP address and protocol according to the
.split(b",")[0] # headers.
.strip() self._process_forwarded_headers()
.decode("ascii") return super().requestReceived(command, path, version)
def _process_forwarded_headers(self):
headers = self.requestHeaders.getRawHeaders(b"x-forwarded-for")
if not headers:
return
# for now, we just use the first x-forwarded-for header. Really, we ought
# to start from the client IP address, and check whether it is trusted; if it
# is, work backwards through the headers until we find an untrusted address.
# see https://github.com/matrix-org/synapse/issues/9471
self._forwarded_for = _XForwardedForAddress(
headers[0].split(b",")[0].strip().decode("ascii")
) )
# if we got an x-forwarded-for header, also look for an x-forwarded-proto header
header = self.getHeader(b"x-forwarded-proto")
if header is not None:
self._forwarded_https = header.lower() == b"https"
else:
# this is done largely for backwards-compatibility so that people that
# haven't set an x-forwarded-proto header don't get a redirect loop.
logger.warning(
"forwarded request lacks an x-forwarded-proto header: assuming https"
)
self._forwarded_https = True
def isSecure(self):
if self._forwarded_https:
return True
return super().isSecure()
def getClientIP(self) -> str:
"""
Return the IP address of the client who submitted this request.
This method is deprecated. Use getClientAddress() instead.
"""
if self._forwarded_for is not None:
return self._forwarded_for.host
return super().getClientIP()
def getClientAddress(self) -> IAddress:
"""
Return the address of the client who submitted this request.
"""
if self._forwarded_for is not None:
return self._forwarded_for
return super().getClientAddress()
@implementer(IAddress)
@attr.s(frozen=True, slots=True)
class _XForwardedForAddress:
host = attr.ib(type=str)
class SynapseSite(Site): class SynapseSite(Site):
""" """