diff --git a/changelog.d/18514.feature b/changelog.d/18514.feature new file mode 100644 index 00000000000..2bdac2d0767 --- /dev/null +++ b/changelog.d/18514.feature @@ -0,0 +1 @@ +Add configurable rate limiting for the creation of rooms. \ No newline at end of file diff --git a/docker/complement/conf/workers-shared-extra.yaml.j2 b/docker/complement/conf/workers-shared-extra.yaml.j2 index 48b44ddf904..168c385191e 100644 --- a/docker/complement/conf/workers-shared-extra.yaml.j2 +++ b/docker/complement/conf/workers-shared-extra.yaml.j2 @@ -98,6 +98,10 @@ rc_delayed_event_mgmt: per_second: 9999 burst_count: 9999 +rc_room_creation: + per_second: 9999 + burst_count: 9999 + federation_rr_transactions_per_room_per_second: 9999 allow_device_name_lookup_over_federation: true diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index f1ab9968105..96be5c27892 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1996,6 +1996,31 @@ rc_reports: burst_count: 20.0 ``` --- +### `rc_room_creation` + +*(object)* Sets rate limits for how often users are able to create rooms. + +This setting has the following sub-options: + +* `per_second` (number): Maximum number of requests a client can send per second. + +* `burst_count` (number): Maximum number of requests a client can send before being throttled. + +Default configuration: +```yaml +rc_room_creation: + per_user: + per_second: 0.016 + burst_count: 10.0 +``` + +Example configuration: +```yaml +rc_room_creation: + per_second: 1.0 + burst_count: 5.0 +``` +--- ### `federation_rr_transactions_per_room_per_second` *(integer)* Sets outgoing federation transaction frequency for sending read-receipts, per-room. diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 45807a81f19..5b9ff1864fa 100644 --- a/schema/synapse-config.schema.yaml +++ b/schema/synapse-config.schema.yaml @@ -2228,6 +2228,17 @@ properties: examples: - per_second: 2.0 burst_count: 20.0 + rc_room_creation: + $ref: "#/$defs/rc" + description: >- + Sets rate limits for how often users are able to create rooms. + default: + per_user: + per_second: 0.016 + burst_count: 10.0 + examples: + - per_second: 1.0 + burst_count: 5.0 federation_rr_transactions_per_room_per_second: type: integer description: >- diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index 290701615f5..b082daa8f75 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -241,6 +241,12 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: defaults={"per_second": 1, "burst_count": 5}, ) + self.rc_room_creation = RatelimitSettings.parse( + config, + "rc_room_creation", + defaults={"per_second": 0.016, "burst_count": 10}, + ) + self.rc_reports = RatelimitSettings.parse( config, "rc_reports", diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index d8c4d0c20e7..820140b28f5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -66,6 +66,7 @@ SynapseError, ) from synapse.api.filtering import Filter +from synapse.api.ratelimiting import Ratelimiter from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.event_auth import validate_event_for_room_version from synapse.events import EventBase @@ -131,7 +132,12 @@ def __init__(self, hs: "HomeServer"): self.room_member_handler = hs.get_room_member_handler() self._event_auth_handler = hs.get_event_auth_handler() self.config = hs.config - self.request_ratelimiter = hs.get_request_ratelimiter() + self.common_request_ratelimiter = hs.get_request_ratelimiter() + self.creation_ratelimiter = Ratelimiter( + store=self.store, + clock=self.clock, + cfg=self.config.ratelimiting.rc_room_creation, + ) # Room state based off defined presets self._presets_dict: Dict[str, Dict[str, Any]] = { @@ -203,7 +209,11 @@ async def upgrade_room( Raises: ShadowBanError if the requester is shadow-banned. """ - await self.request_ratelimiter.ratelimit(requester) + await self.creation_ratelimiter.ratelimit(requester, update=False) + + # then apply the ratelimits + await self.common_request_ratelimiter.ratelimit(requester) + await self.creation_ratelimiter.ratelimit(requester) user_id = requester.user.to_string() @@ -809,11 +819,23 @@ async def create_room( ) if ratelimit: - # Rate limit once in advance, but don't rate limit the individual - # events in the room — room creation isn't atomic and it's very - # janky if half the events in the initial state don't make it because - # of rate limiting. - await self.request_ratelimiter.ratelimit(requester) + # Limit the rate of room creations, + # using both the limiter specific to room creations as well + # as the general request ratelimiter. + # + # Note that we don't rate limit the individual + # events in the room — room creation isn't atomic and + # historically it was very janky if half the events in the + # initial state don't make it because of rate limiting. + + # First check the room creation ratelimiter without updating it + # (this is so we don't consume a token if the other ratelimiter doesn't + # allow us to proceed) + await self.creation_ratelimiter.ratelimit(requester, update=False) + + # then apply the ratelimits + await self.common_request_ratelimiter.ratelimit(requester) + await self.creation_ratelimiter.ratelimit(requester) room_version_id = config.get( "room_version", self.config.server.default_room_version.identifier diff --git a/tests/handlers/test_room_summary.py b/tests/handlers/test_room_summary.py index bf18c1e72a5..1fdb58ae074 100644 --- a/tests/handlers/test_room_summary.py +++ b/tests/handlers/test_room_summary.py @@ -45,6 +45,7 @@ from synapse.util import Clock from tests import unittest +from tests.unittest import override_config def _create_event( @@ -245,6 +246,7 @@ def test_simple_space(self) -> None: ) self._assert_hierarchy(result, expected) + @override_config({"rc_room_creation": {"burst_count": 1000, "per_second": 1}}) def test_large_space(self) -> None: """Test a space with a large number of rooms.""" rooms = [self.room] @@ -527,6 +529,7 @@ def test_complex_space(self) -> None: ) self._assert_hierarchy(result, expected) + @override_config({"rc_room_creation": {"burst_count": 1000, "per_second": 1}}) def test_pagination(self) -> None: """Test simple pagination works.""" room_ids = [] @@ -564,6 +567,7 @@ def test_pagination(self) -> None: self._assert_hierarchy(result, expected) self.assertNotIn("next_batch", result) + @override_config({"rc_room_creation": {"burst_count": 1000, "per_second": 1}}) def test_invalid_pagination_token(self) -> None: """An invalid pagination token, or changing other parameters, shoudl be rejected.""" room_ids = [] @@ -615,6 +619,7 @@ def test_invalid_pagination_token(self) -> None: SynapseError, ) + @override_config({"rc_room_creation": {"burst_count": 1000, "per_second": 1}}) def test_max_depth(self) -> None: """Create a deep tree to test the max depth against.""" spaces = [self.space]