[PATCH] Dynamic rate limiting for limit_req module
J Carter
jordanc.carter at outlook.com
Fri Dec 30 22:23:12 UTC 2022
Hello,
Please find below a patch to enable dynamic rate limiting for limit_req module.
/* ----------------------------EXAMPLE---------------------------------*/
geo $traffic_tier {
default free;
127.0.1.0/24 basic;
127.0.2.0/24 premium;
}
map $traffic_tier $rate {
free 1r/m;
basic 2r/m;
premium 1r/s;
}
limit_req_zone $binary_remote_addr zone=one:10m rate=$rate;
server {
limit_req zone=one;
listen 80;
server_name localhost;
return 200;
}
curl --interface 127.0.X.X localhost
/* ----------------------------NGINX-TESTS---------------------------------*/
debian at debian:~/projects/nginx-merc/nginx-tests$ prove limit_req*
limit_req2.t ......... ok
limit_req_delay.t .... ok
limit_req_dry_run.t .. ok
limit_req.t .......... ok
All tests successful.
Files=4, Tests=40, 4 wallclock secs ( 0.04 usr 0.00 sys + 0.28 cusr 0.11 csys = 0.43 CPU)
Result: PASS
/* ----------------------------CHANGES OF BEHAVIOUR---------------------------------*/
It is backwards compatible with the syntax of existing configurations, either a rate=$variable can be used or the existing syntax of rate=xy/s.
- 'rate=' can be assigned empty value, which results in an unlimited(maximum) rate limits value.
- 'rate=' set to an invalid value also results in an unlimited(maximum) rate limit value.
- The value of rate is now limited to prevent integer overflow in certain operations.
- The maximum time between consecutive requests that is determined is now limited to 60s (60000ms) to prevent integer overflow/underflow.
/* ----------------------------USE-CASES---------------------------------*/
Allow rate limits for a given user to be determined by mapping trusted request values to a rate, such as:
- Source IP CIDR.
- Client Certificate identifiers.
- JWT claims.
This could also be performed dynamically at runtime with key_val zone to alter rate limits on the fly without a reload.
/* ----------------------------PATCHBOMB---------------------------------*/
# HG changeset patch
# User jordanc.carter at outlook.com
# Date 1672437935 0
# Fri Dec 30 22:05:35 2022 +0000
# Branch dynamic-rate-limiting
# Node ID b2bd50efa81e5aeeb9b8f84ee0af34463add07fa
# Parent 07b0bee87f32be91a33210bc06973e07c4c1dac9
Changed 'rate=' to complex value and added limits to the rate value to prevent integer overflow/underflow
diff -r 07b0bee87f32 -r b2bd50efa81e src/http/modules/ngx_http_limit_req_module.c
--- a/src/http/modules/ngx_http_limit_req_module.c Wed Dec 21 14:53:27 2022 +0300
+++ b/src/http/modules/ngx_http_limit_req_module.c Fri Dec 30 22:05:35 2022 +0000
@@ -26,6 +26,7 @@
/* integer value, 1 corresponds to 0.001 r/s */
ngx_uint_t excess;
ngx_uint_t count;
+ ngx_uint_t rate;
u_char data[1];
} ngx_http_limit_req_node_t;
@@ -41,7 +42,7 @@
ngx_http_limit_req_shctx_t *sh;
ngx_slab_pool_t *shpool;
/* integer value, 1 corresponds to 0.001 r/s */
- ngx_uint_t rate;
+ ngx_http_complex_value_t rate;
ngx_http_complex_value_t key;
ngx_http_limit_req_node_t *node;
} ngx_http_limit_req_ctx_t;
@@ -66,9 +67,9 @@
static void ngx_http_limit_req_delay(ngx_http_request_t *r);
static ngx_int_t ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit,
- ngx_uint_t hash, ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account);
+ ngx_uint_t hash, ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account, ngx_uint_t rate);
static ngx_msec_t ngx_http_limit_req_account(ngx_http_limit_req_limit_t *limits,
- ngx_uint_t n, ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit);
+ ngx_uint_t n, ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit, ngx_uint_t rate);
static void ngx_http_limit_req_unlock(ngx_http_limit_req_limit_t *limits,
ngx_uint_t n);
static void ngx_http_limit_req_expire(ngx_http_limit_req_ctx_t *ctx,
@@ -195,10 +196,13 @@
ngx_http_limit_req_handler(ngx_http_request_t *r)
{
uint32_t hash;
- ngx_str_t key;
+ ngx_str_t key, rate_s;
ngx_int_t rc;
ngx_uint_t n, excess;
+ ngx_uint_t scale;
+ ngx_uint_t rate;
ngx_msec_t delay;
+ u_char *p;
ngx_http_limit_req_ctx_t *ctx;
ngx_http_limit_req_conf_t *lrcf;
ngx_http_limit_req_limit_t *limit, *limits;
@@ -243,10 +247,34 @@
hash = ngx_crc32_short(key.data, key.len);
+ if (ngx_http_complex_value(r, &ctx->rate, &rate_s) != NGX_OK) {
+ ngx_http_limit_req_unlock(limits, n);
+ return NGX_HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ scale = 1;
+ rate = NGX_ERROR;
+
+ if (rate_s.len > 8) {
+
+ rate = (ngx_uint_t) ngx_atoi(rate_s.data + 5, rate_s.len - 8);
+
+ p = rate_s.data + rate_s.len - 3;
+ if (ngx_strncmp(p, "r/m", 3) == 0) {
+ scale = 60;
+ } else if (ngx_strncmp(p, "r/s", 3) != 0){
+ rate = NGX_ERROR;
+ }
+ }
+
+ rate = (rate != 0 && rate < NGX_MAX_INT_T_VALUE / 60000000 - 1001) ?
+ rate * 1000 / scale :
+ NGX_MAX_INT_T_VALUE / 60000000 - 1001;
+
ngx_shmtx_lock(&ctx->shpool->mutex);
rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess,
- (n == lrcf->limits.nelts - 1));
+ (n == lrcf->limits.nelts - 1), rate);
ngx_shmtx_unlock(&ctx->shpool->mutex);
@@ -291,7 +319,7 @@
excess = 0;
}
- delay = ngx_http_limit_req_account(limits, n, &excess, &limit);
+ delay = ngx_http_limit_req_account(limits, n, &excess, &limit, rate);
if (!delay) {
r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_PASSED;
@@ -403,7 +431,7 @@
static ngx_int_t
ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_t hash,
- ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account)
+ ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account, ngx_uint_t rate)
{
size_t size;
ngx_int_t rc, excess;
@@ -412,7 +440,6 @@
ngx_rbtree_node_t *node, *sentinel;
ngx_http_limit_req_ctx_t *ctx;
ngx_http_limit_req_node_t *lr;
-
now = ngx_current_msec;
ctx = limit->shm_zone->data;
@@ -446,12 +473,14 @@
if (ms < -60000) {
ms = 1;
-
} else if (ms < 0) {
ms = 0;
+ } else if (ms > 60000) {
+ ms = 60000;
}
- excess = lr->excess - ctx->rate * ms / 1000 + 1000;
+ lr->rate = rate;
+ excess = lr->excess - lr->rate * ms / 1000 + 1000;
if (excess < 0) {
excess = 0;
@@ -510,6 +539,7 @@
lr->len = (u_short) key->len;
lr->excess = 0;
+ lr->rate = rate;
ngx_memcpy(lr->data, key->data, key->len);
@@ -534,7 +564,7 @@
static ngx_msec_t
ngx_http_limit_req_account(ngx_http_limit_req_limit_t *limits, ngx_uint_t n,
- ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit)
+ ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit, ngx_uint_t rate)
{
ngx_int_t excess;
ngx_msec_t now, delay, max_delay;
@@ -543,13 +573,13 @@
ngx_http_limit_req_node_t *lr;
excess = *ep;
+ max_delay = 0;
if ((ngx_uint_t) excess <= (*limit)->delay) {
max_delay = 0;
} else {
- ctx = (*limit)->shm_zone->data;
- max_delay = (excess - (*limit)->delay) * 1000 / ctx->rate;
+ max_delay = (excess - (*limit)->delay) * 1000 / rate;
}
while (n--) {
@@ -570,9 +600,11 @@
} else if (ms < 0) {
ms = 0;
+ } else if (ms > 60000) {
+ ms = 60000;
}
- excess = lr->excess - ctx->rate * ms / 1000 + 1000;
+ excess = lr->excess - lr->rate * ms / 1000 + 1000;
if (excess < 0) {
excess = 0;
@@ -593,7 +625,7 @@
continue;
}
- delay = (excess - limits[n].delay) * 1000 / ctx->rate;
+ delay = (excess - limits[n].delay) * 1000 / lr->rate;
if (delay > max_delay) {
max_delay = delay;
@@ -674,9 +706,11 @@
if (ms < 60000) {
return;
+ } else {
+ ms = 60000;
}
- excess = lr->excess - ctx->rate * ms / 1000;
+ excess = lr->excess - lr->rate * ms / 1000;
if (excess > 0) {
return;
@@ -833,14 +867,12 @@
ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
u_char *p;
- size_t len;
ssize_t size;
ngx_str_t *value, name, s;
- ngx_int_t rate, scale;
ngx_uint_t i;
ngx_shm_zone_t *shm_zone;
ngx_http_limit_req_ctx_t *ctx;
- ngx_http_compile_complex_value_t ccv;
+ ngx_http_compile_complex_value_t key, rate;
value = cf->args->elts;
@@ -849,19 +881,17 @@
return NGX_CONF_ERROR;
}
- ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
+ ngx_memzero(&key, sizeof(ngx_http_compile_complex_value_t));
- ccv.cf = cf;
- ccv.value = &value[1];
- ccv.complex_value = &ctx->key;
+ key.cf = cf;
+ key.value = &value[1];
+ key.complex_value = &ctx->key;
- if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
+ if (ngx_http_compile_complex_value(&key) != NGX_OK) {
return NGX_CONF_ERROR;
}
size = 0;
- rate = 1;
- scale = 1;
name.len = 0;
for (i = 2; i < cf->args->nelts; i++) {
@@ -902,25 +932,14 @@
if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {
- len = value[i].len;
- p = value[i].data + len - 3;
-
- if (ngx_strncmp(p, "r/s", 3) == 0) {
- scale = 1;
- len -= 3;
+ ngx_memzero(&rate, sizeof(ngx_http_compile_complex_value_t));
+ rate.cf = cf;
+ rate.value = &value[i];
+ rate.complex_value = &ctx->rate;
- } else if (ngx_strncmp(p, "r/m", 3) == 0) {
- scale = 60;
- len -= 3;
- }
-
- rate = ngx_atoi(value[i].data + 5, len - 5);
- if (rate <= 0) {
- ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
- "invalid rate \"%V\"", &value[i]);
+ if (ngx_http_compile_complex_value(&rate) != NGX_OK) {
return NGX_CONF_ERROR;
}
-
continue;
}
@@ -936,8 +955,6 @@
return NGX_CONF_ERROR;
}
- ctx->rate = rate * 1000 / scale;
-
shm_zone = ngx_shared_memory_add(cf, &name, size,
&ngx_http_limit_req_module);
if (shm_zone == NULL) {
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mailman.nginx.org/pipermail/nginx-devel/attachments/20221230/1c468dc3/attachment-0001.htm>
More information about the nginx-devel
mailing list