From dd781f666dd7bf21162fb127dfa17b48debb4603 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 2 Sep 2020 18:03:21 +0100 Subject: [PATCH] Configurable backoff --- clientapi/routing/rate_limiting.go | 38 ++++++++++++++++++++--------- clientapi/routing/routing.go | 2 +- dendrite-config.yaml | 8 ++++++ internal/config/config_clientapi.go | 31 +++++++++++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/clientapi/routing/rate_limiting.go b/clientapi/routing/rate_limiting.go index abbb20e59..0045c1595 100644 --- a/clientapi/routing/rate_limiting.go +++ b/clientapi/routing/rate_limiting.go @@ -6,23 +6,28 @@ import ( "time" "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/internal/config" "github.com/matrix-org/util" ) type rateLimits struct { - limits map[string]chan struct{} - limitsMutex sync.RWMutex - maxRequests int - timeInterval time.Duration + limits map[string]chan struct{} + limitsMutex sync.RWMutex + enabled bool + requestThreshold int64 + cooloffDuration time.Duration } -func newRateLimits() *rateLimits { +func newRateLimits(cfg *config.RateLimiting) *rateLimits { l := &rateLimits{ - limits: make(map[string]chan struct{}), - maxRequests: 10, - timeInterval: 250 * time.Millisecond, + limits: make(map[string]chan struct{}), + enabled: cfg.Enabled, + requestThreshold: cfg.Threshold, + cooloffDuration: time.Duration(cfg.Cooloff) * time.Millisecond, + } + if l.enabled { + go l.clean() } - go l.clean() return l } @@ -45,6 +50,15 @@ func (l *rateLimits) clean() { } func (l *rateLimits) rateLimit(req *http.Request) *util.JSONResponse { + // If rate limiting is disabled then do nothing. + if !l.enabled { + return nil + } + + // Lock the map long enough to check for rate limiting. We hold it + // for longer here than we really need to but it makes sure that we + // also don't conflict with the cleaner goroutine which might clean + // up a channel after we have retrieved it otherwise. l.limitsMutex.RLock() defer l.limitsMutex.RUnlock() @@ -59,7 +73,7 @@ func (l *rateLimits) rateLimit(req *http.Request) *util.JSONResponse { // let's create one. rateLimit, ok := l.limits[caller] if !ok { - l.limits[caller] = make(chan struct{}, l.maxRequests) + l.limits[caller] = make(chan struct{}, l.requestThreshold) rateLimit = l.limits[caller] } @@ -71,14 +85,14 @@ func (l *rateLimits) rateLimit(req *http.Request) *util.JSONResponse { // We hit the rate limit. Tell the client to back off. return &util.JSONResponse{ Code: http.StatusTooManyRequests, - JSON: jsonerror.LimitExceeded("You are sending too many requests too quickly!", l.timeInterval.Milliseconds()), + JSON: jsonerror.LimitExceeded("You are sending too many requests too quickly!", l.cooloffDuration.Milliseconds()), } } // After the time interval, drain a resource from the rate limiting // channel. This will free up space in the channel for new requests. go func() { - <-time.After(l.timeInterval) + <-time.After(l.cooloffDuration) <-rateLimit }() return nil diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index d052635a2..0c63f9686 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -60,7 +60,7 @@ func Setup( keyAPI keyserverAPI.KeyInternalAPI, extRoomsProvider api.ExtraPublicRoomsProvider, ) { - rateLimits := newRateLimits() + rateLimits := newRateLimits(&cfg.RateLimiting) userInteractiveAuth := auth.NewUserInteractive(accountDB.GetAccountByPassword, cfg) publicAPIMux.Handle("/versions", diff --git a/dendrite-config.yaml b/dendrite-config.yaml index 23f142a83..570669c1a 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -133,6 +133,14 @@ client_api: turn_username: "" turn_password: "" + # Settings for rate-limited endpoints. Rate limiting will kick in after the + # threshold number of "slots" have been taken by requests from a specific + # host. Each "slot" will be released after the cooloff time in milliseconds. + rate_limiting: + enabled: true + threshold: 5 + cooloff_ms: 500 + # Configuration for the Current State Server. current_state_server: internal_api: diff --git a/internal/config/config_clientapi.go b/internal/config/config_clientapi.go index f7878276a..3e21cd31f 100644 --- a/internal/config/config_clientapi.go +++ b/internal/config/config_clientapi.go @@ -34,6 +34,9 @@ type ClientAPI struct { // TURN options TURN TURN `yaml:"turn"` + + // Rate-limiting options + RateLimiting RateLimiting `yaml:"rate_limiting"` } func (c *ClientAPI) Defaults() { @@ -47,6 +50,7 @@ func (c *ClientAPI) Defaults() { c.RecaptchaBypassSecret = "" c.RecaptchaSiteVerifyAPI = "" c.RegistrationDisabled = false + c.RateLimiting.Defaults() } func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { @@ -61,6 +65,7 @@ func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { checkNotEmpty(configErrs, "client_api.recaptcha_siteverify_api", string(c.RecaptchaSiteVerifyAPI)) } c.TURN.Verify(configErrs) + c.RateLimiting.Verify(configErrs) } type TURN struct { @@ -90,3 +95,29 @@ func (c *TURN) Verify(configErrs *ConfigErrors) { } } } + +type RateLimiting struct { + // Is rate limiting enabled or disabled? + Enabled bool `yaml:"enabled"` + + // How many "slots" a user can occupy sending requests to a rate-limited + // endpoint before we apply rate-limiting + Threshold int64 `yaml:"threshold"` + + // The cooloff period in milliseconds after a request before the "slot" + // is freed again + Cooloff int64 `yaml:"cooloff_ms"` +} + +func (r *RateLimiting) Verify(configErrs *ConfigErrors) { + if r.Enabled { + checkPositive(configErrs, "client_api.rate_limiting.threshold", r.Threshold) + checkPositive(configErrs, "client_api.rate_limiting.cooloff_ms", r.Cooloff) + } +} + +func (r *RateLimiting) Defaults() { + r.Enabled = true + r.Threshold = 5 + r.Cooloff = 500 +}