Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 1c802de

Browse files
Re-introduce the outbound federation proxy (#15913)
Allow configuring the set of workers to proxy outbound federation traffic through (`outbound_federation_restricted_to`). This is useful when you have a worker setup with `federation_sender` instances responsible for sending outbound federation requests and want to make sure *all* outbound federation traffic goes through those instances. Before this change, the generic workers would still contact federation themselves for things like profile lookups, backfill, etc. This PR allows you to set more strict access controls/firewall for all workers and only allow the `federation_sender`'s to contact the outside world.
1 parent c692283 commit 1c802de

32 files changed

+1128
-96
lines changed

changelog.d/15913.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow configuring the set of workers to proxy outbound federation traffic through via `outbound_federation_restricted_to`.

docs/usage/configuration/config_documentation.md

+26-7
Original file line numberDiff line numberDiff line change
@@ -3960,13 +3960,14 @@ federation_sender_instances:
39603960
---
39613961
### `instance_map`
39623962

3963-
When using workers this should be a map from [`worker_name`](#worker_name) to the
3964-
HTTP replication listener of the worker, if configured, and to the main process.
3965-
Each worker declared under [`stream_writers`](../../workers.md#stream-writers) needs
3966-
a HTTP replication listener, and that listener should be included in the `instance_map`.
3967-
The main process also needs an entry on the `instance_map`, and it should be listed under
3968-
`main` **if even one other worker exists**. Ensure the port matches with what is declared
3969-
inside the `listener` block for a `replication` listener.
3963+
When using workers this should be a map from [`worker_name`](#worker_name) to the HTTP
3964+
replication listener of the worker, if configured, and to the main process. Each worker
3965+
declared under [`stream_writers`](../../workers.md#stream-writers) and
3966+
[`outbound_federation_restricted_to`](#outbound_federation_restricted_to) needs a HTTP
3967+
replication listener, and that listener should be included in the `instance_map`. The
3968+
main process also needs an entry on the `instance_map`, and it should be listed under
3969+
`main` **if even one other worker exists**. Ensure the port matches with what is
3970+
declared inside the `listener` block for a `replication` listener.
39703971

39713972

39723973
Example configuration:
@@ -4004,6 +4005,24 @@ stream_writers:
40044005
typing: worker1
40054006
```
40064007
---
4008+
### `outbound_federation_restricted_to`
4009+
4010+
When using workers, you can restrict outbound federation traffic to only go through a
4011+
specific subset of workers. Any worker specified here must also be in the
4012+
[`instance_map`](#instance_map).
4013+
[`worker_replication_secret`](#worker_replication_secret) must also be configured to
4014+
authorize inter-worker communication.
4015+
4016+
```yaml
4017+
outbound_federation_restricted_to:
4018+
- federation_sender1
4019+
- federation_sender2
4020+
```
4021+
4022+
Also see the [worker
4023+
documentation](../../workers.md#restrict-outbound-federation-traffic-to-a-specific-set-of-workers)
4024+
for more info.
4025+
---
40074026
### `run_background_tasks_on`
40084027

40094028
The [worker](../../workers.md#background-tasks) that is used to run

docs/workers.md

+24
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,30 @@ the stream writer for the `presence` stream:
531531

532532
^/_matrix/client/(api/v1|r0|v3|unstable)/presence/
533533

534+
#### Restrict outbound federation traffic to a specific set of workers
535+
536+
The
537+
[`outbound_federation_restricted_to`](usage/configuration/config_documentation.md#outbound_federation_restricted_to)
538+
configuration is useful to make sure outbound federation traffic only goes through a
539+
specified subset of workers. This allows you to set more strict access controls (like a
540+
firewall) for all workers and only allow the `federation_sender`'s to contact the
541+
outside world.
542+
543+
```yaml
544+
instance_map:
545+
main:
546+
host: localhost
547+
port: 8030
548+
federation_sender1:
549+
host: localhost
550+
port: 8034
551+
552+
outbound_federation_restricted_to:
553+
- federation_sender1
554+
555+
worker_replication_secret: "secret_secret"
556+
```
557+
534558
#### Background tasks
535559

536560
There is also support for moving background tasks to a separate

synapse/api/errors.py

+7
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ def __init__(self, msg: str):
217217
super().__init__(HTTPStatus.BAD_REQUEST, msg, Codes.BAD_JSON)
218218

219219

220+
class InvalidProxyCredentialsError(SynapseError):
221+
"""Error raised when the proxy credentials are invalid."""
222+
223+
def __init__(self, msg: str, errcode: str = Codes.UNKNOWN):
224+
super().__init__(401, msg, errcode)
225+
226+
220227
class ProxiedRequestError(SynapseError):
221228
"""An error from a general matrix endpoint, eg. from a proxied Matrix API call.
222229

synapse/app/_base.py

+2
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ def listen_unix(
386386

387387

388388
def listen_http(
389+
hs: "HomeServer",
389390
listener_config: ListenerConfig,
390391
root_resource: Resource,
391392
version_string: str,
@@ -406,6 +407,7 @@ def listen_http(
406407
version_string,
407408
max_request_body_size=max_request_body_size,
408409
reactor=reactor,
410+
hs=hs,
409411
)
410412

411413
if isinstance(listener_config, TCPListenerConfig):

synapse/app/generic_worker.py

+1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ def _listen_http(self, listener_config: ListenerConfig) -> None:
221221
root_resource = create_resource_tree(resources, OptionsResource())
222222

223223
_base.listen_http(
224+
self,
224225
listener_config,
225226
root_resource,
226227
self.version_string,

synapse/app/homeserver.py

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def _listener_http(
139139
root_resource = OptionsResource()
140140

141141
ports = listen_http(
142+
self,
142143
listener_config,
143144
create_resource_tree(resources, root_resource),
144145
self.version_string,

synapse/config/workers.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import argparse
1717
import logging
18-
from typing import Any, Dict, List, Union
18+
from typing import Any, Dict, List, Optional, Union
1919

2020
import attr
2121
from pydantic import BaseModel, Extra, StrictBool, StrictInt, StrictStr
@@ -171,6 +171,27 @@ class WriterLocations:
171171
)
172172

173173

174+
@attr.s(auto_attribs=True)
175+
class OutboundFederationRestrictedTo:
176+
"""Whether we limit outbound federation to a certain set of instances.
177+
178+
Attributes:
179+
instances: optional list of instances that can make outbound federation
180+
requests. If None then all instances can make federation requests.
181+
locations: list of instance locations to connect to proxy via.
182+
"""
183+
184+
instances: Optional[List[str]]
185+
locations: List[InstanceLocationConfig] = attr.Factory(list)
186+
187+
def __contains__(self, instance: str) -> bool:
188+
# It feels a bit dirty to return `True` if `instances` is `None`, but it makes
189+
# sense in downstream usage in the sense that if
190+
# `outbound_federation_restricted_to` is not configured, then any instance can
191+
# talk to federation (no restrictions so always return `True`).
192+
return self.instances is None or instance in self.instances
193+
194+
174195
class WorkerConfig(Config):
175196
"""The workers are processes run separately to the main synapse process.
176197
They have their own pid_file and listener configuration. They use the
@@ -385,6 +406,28 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
385406
new_option_name="update_user_directory_from_worker",
386407
)
387408

409+
outbound_federation_restricted_to = config.get(
410+
"outbound_federation_restricted_to", None
411+
)
412+
self.outbound_federation_restricted_to = OutboundFederationRestrictedTo(
413+
outbound_federation_restricted_to
414+
)
415+
if outbound_federation_restricted_to:
416+
if not self.worker_replication_secret:
417+
raise ConfigError(
418+
"`worker_replication_secret` must be configured when using `outbound_federation_restricted_to`."
419+
)
420+
421+
for instance in outbound_federation_restricted_to:
422+
if instance not in self.instance_map:
423+
raise ConfigError(
424+
"Instance %r is configured in 'outbound_federation_restricted_to' but does not appear in `instance_map` config."
425+
% (instance,)
426+
)
427+
self.outbound_federation_restricted_to.locations.append(
428+
self.instance_map[instance]
429+
)
430+
388431
def _should_this_worker_perform_duty(
389432
self,
390433
config: Dict[str, Any],

synapse/http/client.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,12 @@ def connectionLost(self, reason: Failure = connectionDone) -> None:
10371037
if reason.check(ResponseDone):
10381038
self.deferred.callback(self.length)
10391039
elif reason.check(PotentialDataLoss):
1040-
# stolen from https://github.com/twisted/treq/pull/49/files
1040+
# This applies to requests which don't set `Content-Length` or a
1041+
# `Transfer-Encoding` in the response because in this case the end of the
1042+
# response is indicated by the connection being closed, an event which may
1043+
# also be due to a transient network problem or other error. But since this
1044+
# behavior is expected of some servers (like YouTube), let's ignore it.
1045+
# Stolen from https://github.com/twisted/treq/pull/49/files
10411046
# http://twistedmatrix.com/trac/ticket/4840
10421047
self.deferred.callback(self.length)
10431048
else:

synapse/http/connectproxyclient.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import abc
1516
import base64
1617
import logging
1718
from typing import Optional, Union
@@ -39,8 +40,14 @@ class ProxyConnectError(ConnectError):
3940
pass
4041

4142

42-
@attr.s(auto_attribs=True)
4343
class ProxyCredentials:
44+
@abc.abstractmethod
45+
def as_proxy_authorization_value(self) -> bytes:
46+
raise NotImplementedError()
47+
48+
49+
@attr.s(auto_attribs=True)
50+
class BasicProxyCredentials(ProxyCredentials):
4451
username_password: bytes
4552

4653
def as_proxy_authorization_value(self) -> bytes:
@@ -55,6 +62,17 @@ def as_proxy_authorization_value(self) -> bytes:
5562
return b"Basic " + base64.encodebytes(self.username_password)
5663

5764

65+
@attr.s(auto_attribs=True)
66+
class BearerProxyCredentials(ProxyCredentials):
67+
access_token: bytes
68+
69+
def as_proxy_authorization_value(self) -> bytes:
70+
"""
71+
Return the value for a Proxy-Authorization header (i.e. 'Bearer xxx').
72+
"""
73+
return b"Bearer " + self.access_token
74+
75+
5876
@implementer(IStreamClientEndpoint)
5977
class HTTPConnectProxyEndpoint:
6078
"""An Endpoint implementation which will send a CONNECT request to an http proxy

0 commit comments

Comments
 (0)