From dd5c8078781978dc16a12a062cfc2d80bee31243 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 30 Oct 2026 10:16:25 +0100 Subject: [PATCH] Rescind a knock through the remembered server MSC4233 "Rescinding the knock": with the feature on, `/leave` on a pending remote knock runs the `make_leave`/`send_leave` dance through the server the knock went through, so the room sees the knock end. Until now we only generated a local leave, and the knock stayed pending in the room for ever. The dance is the one we already do to reject an out-of-band invite, so this reuses `do_remotely_reject_invite` and documents that it serves both cases. With no remembered server, or if the dance fails for any reason, we fall back to the local leave. The handler catches every exception because MSC4233 makes the local leave the answer to any failure of the dance, DNS errors included. Fixes #18030. Co-authored-by: Quentin Gliech Co-Authored-By: Claude Fable 5.1 --- synapse/handlers/federation.py | 16 +++++++++++++++ synapse/handlers/room_member.py | 36 +++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e817e4f020..fe3f22fc6d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1663,6 +1663,22 @@ class FederationHandler: async def do_remotely_reject_invite( self, target_hosts: Iterable[str], room_id: str, user_id: str, content: JsonDict ) -> tuple[EventBase, int]: + """Leave a room we are not resident in, via the make_leave/send_leave dance. + + Used both to reject an out-of-band invite and, under MSC4233, to + rescind an out-of-band knock through the server the knock was + fulfilled through: the two are the same federation dance and differ + only in which hosts we are given. + + Args: + target_hosts: The servers to try, in order. + room_id: The room to leave. + user_id: The local user leaving. + content: The content for the leave event. + + Returns: + The persisted leave event and its stream ID. + """ origin, event, room_version = await self._make_and_verify_event( target_hosts, room_id, user_id, "leave", content=content ) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 43eb29989e..3f5ab689b5 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -2098,13 +2098,37 @@ class RoomMemberMasterHandler(RoomMemberHandler): Implements RoomMemberHandler.remote_rescind_knock """ - # TODO: We don't yet support rescinding knocks over federation - # as we don't know which homeserver to send it to. An obvious - # candidate is the remote homeserver we originally knocked through, - # however we don't currently store that information. - - # Just rescind the knock locally knock_event = await self.store.get_event(knock_event_id) + + if self.hs.config.experimental.msc4233_enabled: + # MSC4233: route the rescission through the server the knock was + # fulfilled through, which we remembered at knock time. + via_server = await self.store.get_local_knock_via_server( + knock_event.state_key, knock_event.room_id, knock_event_id + ) + if via_server is not None: + try: + ( + event, + stream_id, + ) = await self.federation_handler.do_remotely_reject_invite( + [via_server], + knock_event.room_id, + knock_event.state_key, + content, + ) + return event.event_id, stream_id + except Exception as e: + # MSC4233 makes the local rescission below the fallback + # for any failure of the dance, from DNS errors upwards. + logger.warning( + "Failed to rescind knock on %s via %s: %s", + knock_event.room_id, + via_server, + e, + ) + + # Rescind the knock locally return await self._generate_local_out_of_band_leave( knock_event, txn_id, requester, content )