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

Commit a608ac8

Browse files
authored
add SpamChecker callback for silently dropping inbound federated events (#12744)
Signed-off-by: jesopo <[email protected]>
1 parent 7a68203 commit a608ac8

File tree

5 files changed

+108
-4
lines changed

5 files changed

+108
-4
lines changed

changelog.d/12744.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a `drop_federated_event` callback to `SpamChecker` to disregard inbound federated events before they take up much processing power, in an emergency.

docs/modules/spam_checker_callbacks.md

+18
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,24 @@ callback returns `False`, Synapse falls through to the next one. The value of th
249249
callback that does not return `False` will be used. If this happens, Synapse will not call
250250
any of the subsequent implementations of this callback.
251251

252+
### `should_drop_federated_event`
253+
254+
_First introduced in Synapse v1.60.0_
255+
256+
```python
257+
async def should_drop_federated_event(event: "synapse.events.EventBase") -> bool
258+
```
259+
260+
Called when checking whether a remote server can federate an event with us. **Returning
261+
`True` from this function will silently drop a federated event and split-brain our view
262+
of a room's DAG, and thus you shouldn't use this callback unless you know what you are
263+
doing.**
264+
265+
If multiple modules implement this callback, they will be considered in order. If a
266+
callback returns `False`, Synapse falls through to the next one. The value of the first
267+
callback that does not return `False` will be used. If this happens, Synapse will not call
268+
any of the subsequent implementations of this callback.
269+
252270
## Example
253271

254272
The example below is a module that implements the spam checker callback

synapse/events/spamcheck.py

+40
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
["synapse.events.EventBase"],
4545
Awaitable[Union[bool, str]],
4646
]
47+
SHOULD_DROP_FEDERATED_EVENT_CALLBACK = Callable[
48+
["synapse.events.EventBase"],
49+
Awaitable[Union[bool, str]],
50+
]
4751
USER_MAY_JOIN_ROOM_CALLBACK = Callable[[str, str, bool], Awaitable[bool]]
4852
USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]]
4953
USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[[str, str, str, str], Awaitable[bool]]
@@ -168,6 +172,9 @@ def __init__(self, hs: "synapse.server.HomeServer") -> None:
168172
self.clock = hs.get_clock()
169173

170174
self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
175+
self._should_drop_federated_event_callbacks: List[
176+
SHOULD_DROP_FEDERATED_EVENT_CALLBACK
177+
] = []
171178
self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
172179
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
173180
self._user_may_send_3pid_invite_callbacks: List[
@@ -191,6 +198,9 @@ def __init__(self, hs: "synapse.server.HomeServer") -> None:
191198
def register_callbacks(
192199
self,
193200
check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
201+
should_drop_federated_event: Optional[
202+
SHOULD_DROP_FEDERATED_EVENT_CALLBACK
203+
] = None,
194204
user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
195205
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
196206
user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
@@ -209,6 +219,11 @@ def register_callbacks(
209219
if check_event_for_spam is not None:
210220
self._check_event_for_spam_callbacks.append(check_event_for_spam)
211221

222+
if should_drop_federated_event is not None:
223+
self._should_drop_federated_event_callbacks.append(
224+
should_drop_federated_event
225+
)
226+
212227
if user_may_join_room is not None:
213228
self._user_may_join_room_callbacks.append(user_may_join_room)
214229

@@ -268,6 +283,31 @@ async def check_event_for_spam(
268283

269284
return False
270285

286+
async def should_drop_federated_event(
287+
self, event: "synapse.events.EventBase"
288+
) -> Union[bool, str]:
289+
"""Checks if a given federated event is considered "spammy" by this
290+
server.
291+
292+
If the server considers an event spammy, it will be silently dropped,
293+
and in doing so will split-brain our view of the room's DAG.
294+
295+
Args:
296+
event: the event to be checked
297+
298+
Returns:
299+
True if the event should be silently dropped
300+
"""
301+
for callback in self._should_drop_federated_event_callbacks:
302+
with Measure(
303+
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
304+
):
305+
res: Union[bool, str] = await delay_cancellation(callback(event))
306+
if res:
307+
return res
308+
309+
return False
310+
271311
async def user_may_join_room(
272312
self, user_id: str, room_id: str, is_invited: bool
273313
) -> bool:

synapse/federation/federation_server.py

+44-4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__(self, hs: "HomeServer"):
110110

111111
self.handler = hs.get_federation_handler()
112112
self.storage = hs.get_storage()
113+
self._spam_checker = hs.get_spam_checker()
113114
self._federation_event_handler = hs.get_federation_event_handler()
114115
self.state = hs.get_state_handler()
115116
self._event_auth_handler = hs.get_event_auth_handler()
@@ -1019,6 +1020,12 @@ async def _handle_received_pdu(self, origin: str, pdu: EventBase) -> None:
10191020
except SynapseError as e:
10201021
raise FederationError("ERROR", e.code, e.msg, affected=pdu.event_id)
10211022

1023+
if await self._spam_checker.should_drop_federated_event(pdu):
1024+
logger.warning(
1025+
"Unstaged federated event contains spam, dropping %s", pdu.event_id
1026+
)
1027+
return
1028+
10221029
# Add the event to our staging area
10231030
await self.store.insert_received_event_to_staging(origin, pdu)
10241031

@@ -1032,6 +1039,41 @@ async def _handle_received_pdu(self, origin: str, pdu: EventBase) -> None:
10321039
pdu.room_id, room_version, lock, origin, pdu
10331040
)
10341041

1042+
async def _get_next_nonspam_staged_event_for_room(
1043+
self, room_id: str, room_version: RoomVersion
1044+
) -> Optional[Tuple[str, EventBase]]:
1045+
"""Fetch the first non-spam event from staging queue.
1046+
1047+
Args:
1048+
room_id: the room to fetch the first non-spam event in.
1049+
room_version: the version of the room.
1050+
1051+
Returns:
1052+
The first non-spam event in that room.
1053+
"""
1054+
1055+
while True:
1056+
# We need to do this check outside the lock to avoid a race between
1057+
# a new event being inserted by another instance and it attempting
1058+
# to acquire the lock.
1059+
next = await self.store.get_next_staged_event_for_room(
1060+
room_id, room_version
1061+
)
1062+
1063+
if next is None:
1064+
return None
1065+
1066+
origin, event = next
1067+
1068+
if await self._spam_checker.should_drop_federated_event(event):
1069+
logger.warning(
1070+
"Staged federated event contains spam, dropping %s",
1071+
event.event_id,
1072+
)
1073+
continue
1074+
1075+
return next
1076+
10351077
@wrap_as_background_process("_process_incoming_pdus_in_room_inner")
10361078
async def _process_incoming_pdus_in_room_inner(
10371079
self,
@@ -1109,12 +1151,10 @@ async def _process_incoming_pdus_in_room_inner(
11091151
(self._clock.time_msec() - received_ts) / 1000
11101152
)
11111153

1112-
# We need to do this check outside the lock to avoid a race between
1113-
# a new event being inserted by another instance and it attempting
1114-
# to acquire the lock.
1115-
next = await self.store.get_next_staged_event_for_room(
1154+
next = await self._get_next_nonspam_staged_event_for_room(
11161155
room_id, room_version
11171156
)
1157+
11181158
if not next:
11191159
break
11201160

synapse/module_api/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK,
4848
CHECK_REGISTRATION_FOR_SPAM_CALLBACK,
4949
CHECK_USERNAME_FOR_SPAM_CALLBACK,
50+
SHOULD_DROP_FEDERATED_EVENT_CALLBACK,
5051
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK,
5152
USER_MAY_CREATE_ROOM_CALLBACK,
5253
USER_MAY_INVITE_CALLBACK,
@@ -234,6 +235,9 @@ def register_spam_checker_callbacks(
234235
self,
235236
*,
236237
check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
238+
should_drop_federated_event: Optional[
239+
SHOULD_DROP_FEDERATED_EVENT_CALLBACK
240+
] = None,
237241
user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
238242
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
239243
user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
@@ -254,6 +258,7 @@ def register_spam_checker_callbacks(
254258
"""
255259
return self._spam_checker.register_callbacks(
256260
check_event_for_spam=check_event_for_spam,
261+
should_drop_federated_event=should_drop_federated_event,
257262
user_may_join_room=user_may_join_room,
258263
user_may_invite=user_may_invite,
259264
user_may_send_3pid_invite=user_may_send_3pid_invite,

0 commit comments

Comments
 (0)