-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Route key requests to federation senders #15121
Changes from all commits
9a7f925
e0841c5
0919513
cdec544
8c50676
bab3b58
3a9aa53
4e3d2e8
15b357e
5fc4155
3452c2a
b4517d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Route remote key requests via federation senders. | ||
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -590,10 +590,18 @@ It is likely this option will be deprecated in the future and not recommended fo | |||||||||||||||||||||||||||||
new installations. Instead, [use `synapse.app.generic_worker` with the `federation_sender_instances`](usage/configuration/config_documentation.md#federation_sender_instances). | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Handles sending federation traffic to other servers. Doesn't handle any | ||||||||||||||||||||||||||||||
REST endpoints itself, but you should set | ||||||||||||||||||||||||||||||
client-facing REST endpoints itself, but you should set | ||||||||||||||||||||||||||||||
[`send_federation: false`](usage/configuration/config_documentation.md#send_federation) | ||||||||||||||||||||||||||||||
in the shared configuration file to stop the main synapse sending this traffic. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Federation senders should have a replication [`http` listener]( | ||||||||||||||||||||||||||||||
usage/configuration/config_documentation.md#listeners | ||||||||||||||||||||||||||||||
) configured, and | ||||||||||||||||||||||||||||||
should be present in the [`instance_map`]( | ||||||||||||||||||||||||||||||
usage/configuration/config_documentation.md#instance_map | ||||||||||||||||||||||||||||||
) so that other workers can make internal | ||||||||||||||||||||||||||||||
http requests to the federation senders. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
If running multiple federation senders then you must list each | ||||||||||||||||||||||||||||||
instance in the | ||||||||||||||||||||||||||||||
[`federation_sender_instances`](usage/configuration/config_documentation.md#federation_sender_instances) | ||||||||||||||||||||||||||||||
|
@@ -607,6 +615,13 @@ send_federation: false | |||||||||||||||||||||||||||||
federation_sender_instances: | ||||||||||||||||||||||||||||||
- federation_sender1 | ||||||||||||||||||||||||||||||
- federation_sender2 | ||||||||||||||||||||||||||||||
instance_map: | ||||||||||||||||||||||||||||||
- federation_sender1: | ||||||||||||||||||||||||||||||
- host: localhost | ||||||||||||||||||||||||||||||
- port: 1001 | ||||||||||||||||||||||||||||||
- federation_sender2: | ||||||||||||||||||||||||||||||
- host: localhost | ||||||||||||||||||||||||||||||
- port: 1002 | ||||||||||||||||||||||||||||||
Comment on lines
+618
to
+624
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
An example for a federation sender instance: | ||||||||||||||||||||||||||||||
|
@@ -615,6 +630,9 @@ An example for a federation sender instance: | |||||||||||||||||||||||||||||
{{#include systemd-with-workers/workers/federation_sender.yaml}} | ||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
_Changed in Synapse 1.79: Federation senders should now have an http listener | ||||||||||||||||||||||||||||||
listening for `replication`, and should be present in the `instance_map`._ | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
### `synapse.app.media_repository` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Handles the media repository. It can handle all endpoints starting with: | ||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
|
||
import abc | ||
import logging | ||
import random | ||
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Tuple | ||
|
||
import attr | ||
|
@@ -40,10 +41,16 @@ | |
RequestSendFailed, | ||
SynapseError, | ||
) | ||
from synapse.config import ConfigError | ||
from synapse.config.key import TrustedKeyServer | ||
from synapse.crypto.types import _FetchKeyRequest | ||
from synapse.events import EventBase | ||
from synapse.events.utils import prune_event_dict | ||
from synapse.logging.context import make_deferred_yieldable, run_in_background | ||
from synapse.replication.http.keys import ( | ||
ReplicationFetchKeysEndpoint, | ||
deserialise_fetch_key_result, | ||
) | ||
from synapse.storage.keys import FetchKeyResult | ||
from synapse.types import JsonDict | ||
from synapse.util import unwrapFirstError | ||
|
@@ -123,25 +130,6 @@ class KeyLookupError(ValueError): | |
pass | ||
|
||
|
||
@attr.s(slots=True, frozen=True, auto_attribs=True) | ||
class _FetchKeyRequest: | ||
"""A request for keys for a given server. | ||
|
||
We will continue to try and fetch until we have all the keys listed under | ||
`key_ids` (with an appropriate `valid_until_ts` property) or we run out of | ||
places to fetch keys from. | ||
|
||
Attributes: | ||
server_name: The name of the server that owns the keys. | ||
minimum_valid_until_ts: The timestamp which the keys must be valid until. | ||
key_ids: The IDs of the keys to attempt to fetch | ||
""" | ||
|
||
server_name: str | ||
minimum_valid_until_ts: int | ||
key_ids: List[str] | ||
|
||
|
||
class Keyring: | ||
"""Handles verifying signed JSON objects and fetching the keys needed to do | ||
so. | ||
|
@@ -153,14 +141,22 @@ def __init__( | |
self.clock = hs.get_clock() | ||
|
||
if key_fetchers is None: | ||
key_fetchers = ( | ||
# Fetch keys from the database. | ||
StoreKeyFetcher(hs), | ||
# Fetch keys from a configured Perspectives server. | ||
PerspectivesKeyFetcher(hs), | ||
# Fetch keys from the origin server directly. | ||
ServerKeyFetcher(hs), | ||
) | ||
if hs.config.worker.send_federation: | ||
key_fetchers = ( | ||
# Fetch keys from the database. | ||
StoreKeyFetcher(hs), | ||
# Fetch keys from a configured Perspectives server. | ||
PerspectivesKeyFetcher(hs), | ||
# Fetch keys from the origin server directly. | ||
ServerKeyFetcher(hs), | ||
) | ||
else: | ||
key_fetchers = ( | ||
# Fetch keys from the database. | ||
StoreKeyFetcher(hs), | ||
# Ask a federation sender to fetch the keys for us. | ||
InternalWorkerRequestKeyFetcher(hs), | ||
) | ||
self._key_fetchers = key_fetchers | ||
|
||
self._fetch_keys_queue: BatchingQueue[ | ||
|
@@ -291,9 +287,7 @@ async def process_request(self, verify_request: VerifyJsonRequest) -> None: | |
minimum_valid_until_ts=verify_request.minimum_valid_until_ts, | ||
key_ids=list(key_ids_to_find), | ||
) | ||
found_keys_by_server = await self._fetch_keys_queue.add_to_queue( | ||
key_request, key=verify_request.server_name | ||
) | ||
found_keys_by_server = await self.fetch_keys(key_request) | ||
|
||
# Since we batch up requests the returned set of keys may contain keys | ||
# from other servers, so we pull out only the ones we care about. | ||
|
@@ -320,6 +314,15 @@ async def process_request(self, verify_request: VerifyJsonRequest) -> None: | |
Codes.UNAUTHORIZED, | ||
) | ||
|
||
async def fetch_keys( | ||
self, key_request: _FetchKeyRequest | ||
) -> Dict[str, Dict[str, FetchKeyResult]]: | ||
"""Returns: {server name: {key id: fetch key result}}""" | ||
found_keys_by_server = await self._fetch_keys_queue.add_to_queue( | ||
key_request, key=key_request.server_name | ||
) | ||
return found_keys_by_server | ||
|
||
async def _process_json( | ||
self, verify_key: VerifyKey, verify_request: VerifyJsonRequest | ||
) -> None: | ||
|
@@ -469,6 +472,8 @@ async def _inner_fetch_key_request( | |
|
||
|
||
class KeyFetcher(metaclass=abc.ABCMeta): | ||
"""Abstract gadget for fetching keys to validate other homeservers' signatures.""" | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
self._queue = BatchingQueue( | ||
self.__class__.__name__, hs.get_clock(), self._fetch_keys | ||
|
@@ -490,11 +495,15 @@ async def get_keys( | |
async def _fetch_keys( | ||
self, keys_to_fetch: List[_FetchKeyRequest] | ||
) -> Dict[str, Dict[str, FetchKeyResult]]: | ||
""" | ||
Returns: | ||
Map from server_name -> key_id -> FetchKeyResult | ||
""" | ||
pass | ||
|
||
|
||
class StoreKeyFetcher(KeyFetcher): | ||
"""KeyFetcher impl which fetches keys from our data store""" | ||
"""Try to retrieve a previously-fetched key from the DB.""" | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__(hs) | ||
|
@@ -518,6 +527,8 @@ async def _fetch_keys( | |
|
||
|
||
class BaseV2KeyFetcher(KeyFetcher): | ||
"""Abstract helper. Fetch keys by requesting it from some server.""" | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__(hs) | ||
|
||
|
@@ -620,7 +631,10 @@ async def process_v2_response( | |
|
||
|
||
class PerspectivesKeyFetcher(BaseV2KeyFetcher): | ||
"""KeyFetcher impl which fetches keys from the "perspectives" servers""" | ||
"""Fetch keys for some homeserver X by requesting them from a trusted key server Y. | ||
|
||
These trusted key servers were seemingly once known as "perspectives" servers. | ||
""" | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__(hs) | ||
|
@@ -803,7 +817,7 @@ def _validate_perspectives_response( | |
|
||
|
||
class ServerKeyFetcher(BaseV2KeyFetcher): | ||
"""KeyFetcher impl which fetches keys from the origin servers""" | ||
"""Fetch keys for some homeserver X by requesting them directly from X.""" | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__(hs) | ||
|
@@ -903,3 +917,37 @@ async def get_server_verify_keys_v2_direct( | |
response_json=response, | ||
time_added_ms=time_now_ms, | ||
) | ||
|
||
|
||
class InternalWorkerRequestKeyFetcher(KeyFetcher): | ||
"""Ask a federation_sender worker to request keys for some homeserver X. | ||
|
||
It may choose to do so via a notary or directly from X itself; we don't care. | ||
""" | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__(hs) | ||
self._federation_shard_config = hs.config.worker.federation_shard_config | ||
if not self._federation_shard_config.instances: | ||
raise ConfigError("No federation senders configured") | ||
self._client = ReplicationFetchKeysEndpoint.make_client(hs) | ||
|
||
async def _fetch_keys( | ||
self, keys_to_fetch: List[_FetchKeyRequest] | ||
) -> Dict[str, Dict[str, FetchKeyResult]]: | ||
# For simplicity's sake, pick a random federation sender | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we would want to choose the same federation sender that's talking to that instance? 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what I thought, but Personally I find this all incomprehensible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My reading of it is that if all workers have the same |
||
instance_name = random.choice(self._federation_shard_config.instances) | ||
response = await self._client( | ||
keys_to_fetch=keys_to_fetch, | ||
instance_name=instance_name, | ||
) | ||
|
||
parsed_response: Dict[str, Dict[str, FetchKeyResult]] = {} | ||
for server_name, keys in response["server_keys"].items(): | ||
deserialised_keys = { | ||
key_id: deserialise_fetch_key_result(key_id, verify_key) | ||
for key_id, verify_key in keys.items() | ||
} | ||
parsed_response.setdefault(server_name, {}).update(deserialised_keys) | ||
|
||
return parsed_response |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Copyright 2023- The Matrix.org Foundation C.I.C. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
from typing import List | ||
|
||
import attr | ||
|
||
|
||
@attr.s(slots=True, frozen=True, auto_attribs=True) | ||
class _FetchKeyRequest: | ||
"""A request for keys for a given server. | ||
|
||
We will continue to try and fetch until we have all the keys listed under | ||
`key_ids` (with an appropriate `valid_until_ts` property) or we run out of | ||
places to fetch keys from. | ||
|
||
Attributes: | ||
server_name: The name of the server that owns the keys. | ||
minimum_valid_until_ts: The timestamp which the keys must be valid until. | ||
key_ids: The IDs of the keys to attempt to fetch | ||
""" | ||
|
||
server_name: str | ||
minimum_valid_until_ts: int | ||
key_ids: List[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(As I've left this comment on #15133 and #15134 I should just copy it here...)
I think we also need to update:
docker/configure_workers_and_start.py
so complement uses thisdocs/workers.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The upgrade notes will need amending too; I think federation senders will need to be declared in the
instance_map
.