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

Add rate-limiting on registration #4735

Merged
merged 26 commits into from
Mar 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2c95be5
Rate-limiting for registration
babolivier Feb 25, 2019
3399c1d
Add unit test for registration rate limiting
babolivier Feb 25, 2019
b4d6353
Add config parameters for rate limiting on auth endpoints
babolivier Feb 25, 2019
27e29be
Doc
babolivier Feb 25, 2019
aa0eaab
Fix doc of rate limiting function
erikjohnston Feb 25, 2019
55b733c
Incorporate review
babolivier Feb 25, 2019
b2da433
Fix config parsing
babolivier Feb 25, 2019
7f3b495
wtf is this doing here
babolivier Feb 25, 2019
769ad50
Merge branch 'babolivier/ratelimit' of github.com:matrix-org/synapse …
babolivier Feb 25, 2019
7afc780
Fix linting errors
babolivier Feb 25, 2019
f4bc0fc
Set default config for auth rate limiting
babolivier Feb 25, 2019
500a099
Fix tests
babolivier Feb 25, 2019
76ca3f2
Add changelog
babolivier Feb 25, 2019
e11a462
Advance reactor instead of mocked clock
babolivier Feb 26, 2019
9e60f2d
Move parameters to registration specific config and give them more se…
babolivier Feb 26, 2019
fd5c5a1
Remove unused config options
babolivier Feb 26, 2019
69b8760
Don't mock the rate limiter un MAU tests
babolivier Mar 4, 2019
bdefae5
Rename _register_with_store into register_with_store
babolivier Mar 5, 2019
fd60670
Merge branch 'develop' into babolivier/ratelimit
babolivier Mar 5, 2019
49dbed8
Make CI happy
babolivier Mar 5, 2019
f817783
Make linter happy
babolivier Mar 5, 2019
fd12eeb
Remove unused import
babolivier Mar 5, 2019
7ced9c7
rly
babolivier Mar 5, 2019
e9181e3
Update sample config
babolivier Mar 5, 2019
30ed834
Fix ratelimiting test for py2
babolivier Mar 5, 2019
0509108
Add non-guest test
babolivier Mar 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/4735.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add configurable rate limiting to the /register endpoint.
11 changes: 11 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,17 @@ trusted_third_party_id_servers:
#
autocreate_auto_join_rooms: true

# Number of registration requests a client can send per second.
# Defaults to 1/minute (0.17).
#
#rc_registration_requests_per_second: 0.17

# Number of registration requests a client can send before being
# throttled.
# Defaults to 3.
#
#rc_registration_request_burst_count: 3.0


## Metrics ###

Expand Down
31 changes: 16 additions & 15 deletions synapse/api/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ class Ratelimiter(object):
def __init__(self):
self.message_counts = collections.OrderedDict()

def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count, update=True):
"""Can the user send a message?
def can_do_action(self, key, time_now_s, rate_hz, burst_count, update=True):
"""Can the entity (e.g. user or IP address) perform the action?
Args:
user_id: The user sending a message.
key: The key we should use when rate limiting. Can be a user ID
(when sending events), an IP address, etc.
time_now_s: The time now.
msg_rate_hz: The long term number of messages a user can send in a
rate_hz: The long term number of messages a user can send in a
second.
burst_count: How many messages the user can send before being
limited.
Expand All @@ -41,10 +42,10 @@ def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count, update=Tru
"""
self.prune_message_counts(time_now_s)
message_count, time_start, _ignored = self.message_counts.get(
user_id, (0., time_now_s, None),
key, (0., time_now_s, None),
)
time_delta = time_now_s - time_start
sent_count = message_count - time_delta * msg_rate_hz
sent_count = message_count - time_delta * rate_hz
if sent_count < 0:
allowed = True
time_start = time_now_s
Expand All @@ -56,13 +57,13 @@ def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count, update=Tru
message_count += 1

if update:
self.message_counts[user_id] = (
message_count, time_start, msg_rate_hz
self.message_counts[key] = (
message_count, time_start, rate_hz
)

if msg_rate_hz > 0:
if rate_hz > 0:
time_allowed = (
time_start + (message_count - burst_count + 1) / msg_rate_hz
time_start + (message_count - burst_count + 1) / rate_hz
)
if time_allowed < time_now_s:
time_allowed = time_now_s
Expand All @@ -72,12 +73,12 @@ def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count, update=Tru
return allowed, time_allowed

def prune_message_counts(self, time_now_s):
for user_id in list(self.message_counts.keys()):
message_count, time_start, msg_rate_hz = (
self.message_counts[user_id]
for key in list(self.message_counts.keys()):
message_count, time_start, rate_hz = (
self.message_counts[key]
)
time_delta = time_now_s - time_start
if message_count - time_delta * msg_rate_hz > 0:
if message_count - time_delta * rate_hz > 0:
break
else:
del self.message_counts[user_id]
del self.message_counts[key]
18 changes: 18 additions & 0 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ def read_config(self, config):
config.get("disable_msisdn_registration", False)
)

self.rc_registration_requests_per_second = config.get(
"rc_registration_requests_per_second", 0.17,
)
self.rc_registration_request_burst_count = config.get(
"rc_registration_request_burst_count", 3,
)

def default_config(self, generate_secrets=False, **kwargs):
if generate_secrets:
registration_shared_secret = 'registration_shared_secret: "%s"' % (
Expand Down Expand Up @@ -140,6 +147,17 @@ def default_config(self, generate_secrets=False, **kwargs):
# users cannot be auto-joined since they do not exist.
#
autocreate_auto_join_rooms: true

# Number of registration requests a client can send per second.
# Defaults to 1/minute (0.17).
#
#rc_registration_requests_per_second: 0.17

# Number of registration requests a client can send before being
# throttled.
# Defaults to 3.
#
#rc_registration_request_burst_count: 3.0
""" % locals()

def add_arguments(self, parser):
Expand Down
4 changes: 2 additions & 2 deletions synapse/handlers/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ def ratelimit(self, requester, update=True):
messages_per_second = self.hs.config.rc_messages_per_second
burst_count = self.hs.config.rc_message_burst_count

allowed, time_allowed = self.ratelimiter.send_message(
allowed, time_allowed = self.ratelimiter.can_do_action(
user_id, time_now,
msg_rate_hz=messages_per_second,
rate_hz=messages_per_second,
burst_count=burst_count,
update=update,
)
Expand Down
39 changes: 31 additions & 8 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
AuthError,
Codes,
InvalidCaptchaError,
LimitExceededError,
RegistrationError,
SynapseError,
)
Expand Down Expand Up @@ -60,6 +61,7 @@ def __init__(self, hs):
self.user_directory_handler = hs.get_user_directory_handler()
self.captcha_client = CaptchaServerHttpClient(hs)
self.identity_handler = self.hs.get_handlers().identity_handler
self.ratelimiter = hs.get_ratelimiter()

self._next_generated_user_id = None

Expand Down Expand Up @@ -149,6 +151,7 @@ def register(
threepid=None,
user_type=None,
default_display_name=None,
address=None,
):
"""Registers a new client on the server.

Expand All @@ -167,6 +170,7 @@ def register(
api.constants.UserTypes, or None for a normal user.
default_display_name (unicode|None): if set, the new user's displayname
will be set to this. Defaults to 'localpart'.
address (str|None): the IP address used to perform the regitration.
Returns:
A tuple of (user_id, access_token).
Raises:
Expand Down Expand Up @@ -206,7 +210,7 @@ def register(
token = None
if generate_token:
token = self.macaroon_gen.generate_access_token(user_id)
yield self._register_with_store(
yield self.register_with_store(
user_id=user_id,
token=token,
password_hash=password_hash,
Expand All @@ -215,6 +219,7 @@ def register(
create_profile_with_displayname=default_display_name,
admin=admin,
user_type=user_type,
address=address,
)

if self.hs.config.user_directory_search_all_users:
Expand All @@ -238,12 +243,13 @@ def register(
if default_display_name is None:
default_display_name = localpart
try:
yield self._register_with_store(
yield self.register_with_store(
user_id=user_id,
token=token,
password_hash=password_hash,
make_guest=make_guest,
create_profile_with_displayname=default_display_name,
address=address,
)
except SynapseError:
# if user id is taken, just generate another
Expand Down Expand Up @@ -337,7 +343,7 @@ def appservice_register(self, user_localpart, as_token):
user_id, allowed_appservice=service
)

yield self._register_with_store(
yield self.register_with_store(
user_id=user_id,
password_hash="",
appservice_id=service_id,
Expand Down Expand Up @@ -513,7 +519,7 @@ def get_or_create_user(self, requester, localpart, displayname,
token = self.macaroon_gen.generate_access_token(user_id)

if need_register:
yield self._register_with_store(
yield self.register_with_store(
user_id=user_id,
token=token,
password_hash=password_hash,
Expand Down Expand Up @@ -590,10 +596,10 @@ def _join_user_to_room(self, requester, room_identifier):
ratelimit=False,
)

def _register_with_store(self, user_id, token=None, password_hash=None,
was_guest=False, make_guest=False, appservice_id=None,
create_profile_with_displayname=None, admin=False,
user_type=None):
def register_with_store(self, user_id, token=None, password_hash=None,
was_guest=False, make_guest=False, appservice_id=None,
create_profile_with_displayname=None, admin=False,
user_type=None, address=None):
"""Register user in the datastore.

Args:
Expand All @@ -612,10 +618,26 @@ def _register_with_store(self, user_id, token=None, password_hash=None,
admin (boolean): is an admin user?
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the regitration.

Returns:
Deferred
"""
# Don't rate limit for app services
if appservice_id is None and address is not None:
time_now = self.clock.time()

allowed, time_allowed = self.ratelimiter.can_do_action(
address, time_now_s=time_now,
rate_hz=self.hs.config.rc_registration_requests_per_second,
burst_count=self.hs.config.rc_registration_request_burst_count,
)

if not allowed:
raise LimitExceededError(
retry_after_ms=int(1000 * (time_allowed - time_now)),
)

if self.hs.config.worker_app:
return self._register_client(
user_id=user_id,
Expand All @@ -627,6 +649,7 @@ def _register_with_store(self, user_id, token=None, password_hash=None,
create_profile_with_displayname=create_profile_with_displayname,
admin=admin,
user_type=user_type,
address=address,
)
else:
return self.store.register(
Expand Down
8 changes: 6 additions & 2 deletions synapse/replication/http/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
def __init__(self, hs):
super(ReplicationRegisterServlet, self).__init__(hs)
self.store = hs.get_datastore()
self.registration_handler = hs.get_registration_handler()

@staticmethod
def _serialize_payload(
user_id, token, password_hash, was_guest, make_guest, appservice_id,
create_profile_with_displayname, admin, user_type,
create_profile_with_displayname, admin, user_type, address,
):
"""
Args:
Expand All @@ -56,6 +57,7 @@ def _serialize_payload(
admin (boolean): is an admin user?
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the regitration.
"""
return {
"token": token,
Expand All @@ -66,13 +68,14 @@ def _serialize_payload(
"create_profile_with_displayname": create_profile_with_displayname,
"admin": admin,
"user_type": user_type,
"address": address,
}

@defer.inlineCallbacks
def _handle_request(self, request, user_id):
content = parse_json_object_from_request(request)

yield self.store.register(
yield self.registration_handler.register_with_store(
user_id=user_id,
token=content["token"],
password_hash=content["password_hash"],
Expand All @@ -82,6 +85,7 @@ def _handle_request(self, request, user_id):
create_profile_with_displayname=content["create_profile_with_displayname"],
admin=content["admin"],
user_type=content["user_type"],
address=content["address"]
)

defer.returnValue((200, {}))
Expand Down
33 changes: 29 additions & 4 deletions synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
import synapse
import synapse.types
from synapse.api.constants import LoginType
from synapse.api.errors import Codes, SynapseError, UnrecognizedRequestError
from synapse.api.errors import (
Codes,
LimitExceededError,
SynapseError,
UnrecognizedRequestError,
)
from synapse.config.server import is_threepid_reserved
from synapse.http.servlet import (
RestServlet,
Expand Down Expand Up @@ -191,18 +196,36 @@ def __init__(self, hs):
self.identity_handler = hs.get_handlers().identity_handler
self.room_member_handler = hs.get_room_member_handler()
self.macaroon_gen = hs.get_macaroon_generator()
self.ratelimiter = hs.get_ratelimiter()
self.clock = hs.get_clock()

@interactive_auth_handler
@defer.inlineCallbacks
def on_POST(self, request):
body = parse_json_object_from_request(request)

client_addr = request.getClientIP()

time_now = self.clock.time()

allowed, time_allowed = self.ratelimiter.can_do_action(
client_addr, time_now_s=time_now,
rate_hz=self.hs.config.rc_registration_requests_per_second,
burst_count=self.hs.config.rc_registration_request_burst_count,
update=False,
)

if not allowed:
raise LimitExceededError(
retry_after_ms=int(1000 * (time_allowed - time_now)),
)

kind = b"user"
if b"kind" in request.args:
kind = request.args[b"kind"][0]

if kind == b"guest":
ret = yield self._do_guest_registration(body)
ret = yield self._do_guest_registration(body, address=client_addr)
defer.returnValue(ret)
return
elif kind != b"user":
Expand Down Expand Up @@ -411,6 +434,7 @@ def on_POST(self, request):
guest_access_token=guest_access_token,
generate_token=False,
threepid=threepid,
address=client_addr,
)
# Necessary due to auth checks prior to the threepid being
# written to the db
Expand Down Expand Up @@ -522,12 +546,13 @@ def _create_registration_details(self, user_id, params):
defer.returnValue(result)

@defer.inlineCallbacks
def _do_guest_registration(self, params):
def _do_guest_registration(self, params, address=None):
if not self.hs.config.allow_guest_access:
raise SynapseError(403, "Guest access is disabled")
user_id, _ = yield self.registration_handler.register(
generate_token=False,
make_guest=True
make_guest=True,
address=address,
)

# we don't allow guests to specify their own device_id, because
Expand Down
Loading