[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