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

Commit 62a1a9b

Browse files
author
David Robertson
authored
Describe which rate limiter was hit in logs (#16135)
1 parent e9235d9 commit 62a1a9b

File tree

18 files changed

+235
-121
lines changed

18 files changed

+235
-121
lines changed

changelog.d/16135.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Describe which rate limiter was hit in logs.

synapse/api/errors.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ def __init__(
211211
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
212212
return cs_error(self.msg, self.errcode, **self._additional_fields)
213213

214+
@property
215+
def debug_context(self) -> Optional[str]:
216+
"""Override this to add debugging context that shouldn't be sent to clients."""
217+
return None
218+
214219

215220
class InvalidAPICallError(SynapseError):
216221
"""You called an existing API endpoint, but fed that endpoint
@@ -508,8 +513,8 @@ class LimitExceededError(SynapseError):
508513

509514
def __init__(
510515
self,
516+
limiter_name: str,
511517
code: int = 429,
512-
msg: str = "Too Many Requests",
513518
retry_after_ms: Optional[int] = None,
514519
errcode: str = Codes.LIMIT_EXCEEDED,
515520
):
@@ -518,12 +523,17 @@ def __init__(
518523
if self.include_retry_after_header and retry_after_ms is not None
519524
else None
520525
)
521-
super().__init__(code, msg, errcode, headers=headers)
526+
super().__init__(code, "Too Many Requests", errcode, headers=headers)
522527
self.retry_after_ms = retry_after_ms
528+
self.limiter_name = limiter_name
523529

524530
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
525531
return cs_error(self.msg, self.errcode, retry_after_ms=self.retry_after_ms)
526532

533+
@property
534+
def debug_context(self) -> Optional[str]:
535+
return self.limiter_name
536+
527537

528538
class RoomKeysVersionError(SynapseError):
529539
"""A client has tried to upload to a non-current version of the room_keys store"""

synapse/api/ratelimiting.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,16 @@ class Ratelimiter:
6161
"""
6262

6363
def __init__(
64-
self, store: DataStore, clock: Clock, rate_hz: float, burst_count: int
64+
self,
65+
store: DataStore,
66+
clock: Clock,
67+
cfg: RatelimitSettings,
6568
):
6669
self.clock = clock
67-
self.rate_hz = rate_hz
68-
self.burst_count = burst_count
70+
self.rate_hz = cfg.per_second
71+
self.burst_count = cfg.burst_count
6972
self.store = store
73+
self._limiter_name = cfg.key
7074

7175
# An ordered dictionary representing the token buckets tracked by this rate
7276
# limiter. Each entry maps a key of arbitrary type to a tuple representing:
@@ -305,7 +309,8 @@ async def ratelimit(
305309

306310
if not allowed:
307311
raise LimitExceededError(
308-
retry_after_ms=int(1000 * (time_allowed - time_now_s))
312+
limiter_name=self._limiter_name,
313+
retry_after_ms=int(1000 * (time_allowed - time_now_s)),
309314
)
310315

311316

@@ -322,7 +327,9 @@ def __init__(
322327

323328
# The rate_hz and burst_count are overridden on a per-user basis
324329
self.request_ratelimiter = Ratelimiter(
325-
store=self.store, clock=self.clock, rate_hz=0, burst_count=0
330+
store=self.store,
331+
clock=self.clock,
332+
cfg=RatelimitSettings(key=rc_message.key, per_second=0, burst_count=0),
326333
)
327334
self._rc_message = rc_message
328335

@@ -332,8 +339,7 @@ def __init__(
332339
self.admin_redaction_ratelimiter: Optional[Ratelimiter] = Ratelimiter(
333340
store=self.store,
334341
clock=self.clock,
335-
rate_hz=rc_admin_redaction.per_second,
336-
burst_count=rc_admin_redaction.burst_count,
342+
cfg=rc_admin_redaction,
337343
)
338344
else:
339345
self.admin_redaction_ratelimiter = None

synapse/config/ratelimiting.py

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Any, Dict, Optional
15+
from typing import Any, Dict, Optional, cast
1616

1717
import attr
1818

@@ -21,16 +21,47 @@
2121
from ._base import Config
2222

2323

24+
@attr.s(slots=True, frozen=True, auto_attribs=True)
2425
class RatelimitSettings:
25-
def __init__(
26-
self,
27-
config: Dict[str, float],
26+
key: str
27+
per_second: float
28+
burst_count: int
29+
30+
@classmethod
31+
def parse(
32+
cls,
33+
config: Dict[str, Any],
34+
key: str,
2835
defaults: Optional[Dict[str, float]] = None,
29-
):
36+
) -> "RatelimitSettings":
37+
"""Parse config[key] as a new-style rate limiter config.
38+
39+
The key may refer to a nested dictionary using a full stop (.) to separate
40+
each nested key. For example, use the key "a.b.c" to parse the following:
41+
42+
a:
43+
b:
44+
c:
45+
per_second: 10
46+
burst_count: 200
47+
48+
If this lookup fails, we'll fallback to the defaults.
49+
"""
3050
defaults = defaults or {"per_second": 0.17, "burst_count": 3.0}
3151

32-
self.per_second = config.get("per_second", defaults["per_second"])
33-
self.burst_count = int(config.get("burst_count", defaults["burst_count"]))
52+
rl_config = config
53+
for part in key.split("."):
54+
rl_config = rl_config.get(part, {})
55+
56+
# By this point we should have hit the rate limiter parameters.
57+
# We don't actually check this though!
58+
rl_config = cast(Dict[str, float], rl_config)
59+
60+
return cls(
61+
key=key,
62+
per_second=rl_config.get("per_second", defaults["per_second"]),
63+
burst_count=int(rl_config.get("burst_count", defaults["burst_count"])),
64+
)
3465

3566

3667
@attr.s(auto_attribs=True)
@@ -49,15 +80,14 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
4980
# Load the new-style messages config if it exists. Otherwise fall back
5081
# to the old method.
5182
if "rc_message" in config:
52-
self.rc_message = RatelimitSettings(
53-
config["rc_message"], defaults={"per_second": 0.2, "burst_count": 10.0}
83+
self.rc_message = RatelimitSettings.parse(
84+
config, "rc_message", defaults={"per_second": 0.2, "burst_count": 10.0}
5485
)
5586
else:
5687
self.rc_message = RatelimitSettings(
57-
{
58-
"per_second": config.get("rc_messages_per_second", 0.2),
59-
"burst_count": config.get("rc_message_burst_count", 10.0),
60-
}
88+
key="rc_messages",
89+
per_second=config.get("rc_messages_per_second", 0.2),
90+
burst_count=config.get("rc_message_burst_count", 10.0),
6191
)
6292

6393
# Load the new-style federation config, if it exists. Otherwise, fall
@@ -79,51 +109,59 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
79109
}
80110
)
81111

82-
self.rc_registration = RatelimitSettings(config.get("rc_registration", {}))
112+
self.rc_registration = RatelimitSettings.parse(config, "rc_registration", {})
83113

84-
self.rc_registration_token_validity = RatelimitSettings(
85-
config.get("rc_registration_token_validity", {}),
114+
self.rc_registration_token_validity = RatelimitSettings.parse(
115+
config,
116+
"rc_registration_token_validity",
86117
defaults={"per_second": 0.1, "burst_count": 5},
87118
)
88119

89120
# It is reasonable to login with a bunch of devices at once (i.e. when
90121
# setting up an account), but it is *not* valid to continually be
91122
# logging into new devices.
92-
rc_login_config = config.get("rc_login", {})
93-
self.rc_login_address = RatelimitSettings(
94-
rc_login_config.get("address", {}),
123+
self.rc_login_address = RatelimitSettings.parse(
124+
config,
125+
"rc_login.address",
95126
defaults={"per_second": 0.003, "burst_count": 5},
96127
)
97-
self.rc_login_account = RatelimitSettings(
98-
rc_login_config.get("account", {}),
128+
self.rc_login_account = RatelimitSettings.parse(
129+
config,
130+
"rc_login.account",
99131
defaults={"per_second": 0.003, "burst_count": 5},
100132
)
101-
self.rc_login_failed_attempts = RatelimitSettings(
102-
rc_login_config.get("failed_attempts", {})
133+
self.rc_login_failed_attempts = RatelimitSettings.parse(
134+
config,
135+
"rc_login.failed_attempts",
136+
{},
103137
)
104138

105139
self.federation_rr_transactions_per_room_per_second = config.get(
106140
"federation_rr_transactions_per_room_per_second", 50
107141
)
108142

109-
rc_admin_redaction = config.get("rc_admin_redaction")
110143
self.rc_admin_redaction = None
111-
if rc_admin_redaction:
112-
self.rc_admin_redaction = RatelimitSettings(rc_admin_redaction)
144+
if "rc_admin_redaction" in config:
145+
self.rc_admin_redaction = RatelimitSettings.parse(
146+
config, "rc_admin_redaction", {}
147+
)
113148

114-
self.rc_joins_local = RatelimitSettings(
115-
config.get("rc_joins", {}).get("local", {}),
149+
self.rc_joins_local = RatelimitSettings.parse(
150+
config,
151+
"rc_joins.local",
116152
defaults={"per_second": 0.1, "burst_count": 10},
117153
)
118-
self.rc_joins_remote = RatelimitSettings(
119-
config.get("rc_joins", {}).get("remote", {}),
154+
self.rc_joins_remote = RatelimitSettings.parse(
155+
config,
156+
"rc_joins.remote",
120157
defaults={"per_second": 0.01, "burst_count": 10},
121158
)
122159

123160
# Track the rate of joins to a given room. If there are too many, temporarily
124161
# prevent local joins and remote joins via this server.
125-
self.rc_joins_per_room = RatelimitSettings(
126-
config.get("rc_joins_per_room", {}),
162+
self.rc_joins_per_room = RatelimitSettings.parse(
163+
config,
164+
"rc_joins_per_room",
127165
defaults={"per_second": 1, "burst_count": 10},
128166
)
129167

@@ -132,31 +170,37 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
132170
# * For requests received over federation this is keyed by the origin.
133171
#
134172
# Note that this isn't exposed in the configuration as it is obscure.
135-
self.rc_key_requests = RatelimitSettings(
136-
config.get("rc_key_requests", {}),
173+
self.rc_key_requests = RatelimitSettings.parse(
174+
config,
175+
"rc_key_requests",
137176
defaults={"per_second": 20, "burst_count": 100},
138177
)
139178

140-
self.rc_3pid_validation = RatelimitSettings(
141-
config.get("rc_3pid_validation") or {},
179+
self.rc_3pid_validation = RatelimitSettings.parse(
180+
config,
181+
"rc_3pid_validation",
142182
defaults={"per_second": 0.003, "burst_count": 5},
143183
)
144184

145-
self.rc_invites_per_room = RatelimitSettings(
146-
config.get("rc_invites", {}).get("per_room", {}),
185+
self.rc_invites_per_room = RatelimitSettings.parse(
186+
config,
187+
"rc_invites.per_room",
147188
defaults={"per_second": 0.3, "burst_count": 10},
148189
)
149-
self.rc_invites_per_user = RatelimitSettings(
150-
config.get("rc_invites", {}).get("per_user", {}),
190+
self.rc_invites_per_user = RatelimitSettings.parse(
191+
config,
192+
"rc_invites.per_user",
151193
defaults={"per_second": 0.003, "burst_count": 5},
152194
)
153195

154-
self.rc_invites_per_issuer = RatelimitSettings(
155-
config.get("rc_invites", {}).get("per_issuer", {}),
196+
self.rc_invites_per_issuer = RatelimitSettings.parse(
197+
config,
198+
"rc_invites.per_issuer",
156199
defaults={"per_second": 0.3, "burst_count": 10},
157200
)
158201

159-
self.rc_third_party_invite = RatelimitSettings(
160-
config.get("rc_third_party_invite", {}),
202+
self.rc_third_party_invite = RatelimitSettings.parse(
203+
config,
204+
"rc_third_party_invite",
161205
defaults={"per_second": 0.0025, "burst_count": 5},
162206
)

synapse/handlers/auth.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,17 @@ def __init__(self, hs: "HomeServer"):
218218
self._failed_uia_attempts_ratelimiter = Ratelimiter(
219219
store=self.store,
220220
clock=self.clock,
221-
rate_hz=self.hs.config.ratelimiting.rc_login_failed_attempts.per_second,
222-
burst_count=self.hs.config.ratelimiting.rc_login_failed_attempts.burst_count,
221+
cfg=self.hs.config.ratelimiting.rc_login_failed_attempts,
223222
)
224223

225224
# The number of seconds to keep a UI auth session active.
226225
self._ui_auth_session_timeout = hs.config.auth.ui_auth_session_timeout
227226

228-
# Ratelimitier for failed /login attempts
227+
# Ratelimiter for failed /login attempts
229228
self._failed_login_attempts_ratelimiter = Ratelimiter(
230229
store=self.store,
231230
clock=hs.get_clock(),
232-
rate_hz=self.hs.config.ratelimiting.rc_login_failed_attempts.per_second,
233-
burst_count=self.hs.config.ratelimiting.rc_login_failed_attempts.burst_count,
231+
cfg=self.hs.config.ratelimiting.rc_login_failed_attempts,
234232
)
235233

236234
self._clock = self.hs.get_clock()

synapse/handlers/devicemessage.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ def __init__(self, hs: "HomeServer"):
9090
self._ratelimiter = Ratelimiter(
9191
store=self.store,
9292
clock=hs.get_clock(),
93-
rate_hz=hs.config.ratelimiting.rc_key_requests.per_second,
94-
burst_count=hs.config.ratelimiting.rc_key_requests.burst_count,
93+
cfg=hs.config.ratelimiting.rc_key_requests,
9594
)
9695

9796
async def on_direct_to_device_edu(self, origin: str, content: JsonDict) -> None:

synapse/handlers/identity.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,12 @@ def __init__(self, hs: "HomeServer"):
6666
self._3pid_validation_ratelimiter_ip = Ratelimiter(
6767
store=self.store,
6868
clock=hs.get_clock(),
69-
rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
70-
burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
69+
cfg=hs.config.ratelimiting.rc_3pid_validation,
7170
)
7271
self._3pid_validation_ratelimiter_address = Ratelimiter(
7372
store=self.store,
7473
clock=hs.get_clock(),
75-
rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
76-
burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
74+
cfg=hs.config.ratelimiting.rc_3pid_validation,
7775
)
7876

7977
async def ratelimit_request_token_requests(

0 commit comments

Comments
 (0)