[PATCH 3 of 5] HTTP/3: traffic-based flood detection

Roman Arutyunyan arut at nginx.com
Thu Oct 7 11:36:16 UTC 2021


# HG changeset patch
# User Roman Arutyunyan <arut at nginx.com>
# Date 1633602162 -10800
#      Thu Oct 07 13:22:42 2021 +0300
# Branch quic
# Node ID 31561ac584b74d29af9a442afca47821a98217b2
# Parent  1b87f4e196cce2b7aae33a63ca6dfc857b99f2b7
HTTP/3: traffic-based flood detection.

With this patch, all traffic over HTTP/3 bidi and uni streams is counted in
the h3c->total_bytes field, and payload traffic is counted in the
h3c->payload_bytes field.  As long as total traffic is many times larger than
payload traffic, we consider this to be a flood.

Request header traffic is counted as if all fields are literal.  Response
header traffic is counted as is.

diff --git a/src/http/v3/ngx_http_v3.c b/src/http/v3/ngx_http_v3.c
--- a/src/http/v3/ngx_http_v3.c
+++ b/src/http/v3/ngx_http_v3.c
@@ -86,3 +86,22 @@ ngx_http_v3_cleanup_session(void *data)
         ngx_del_timer(&h3c->keepalive);
     }
 }
+
+
+ngx_int_t
+ngx_http_v3_check_flood(ngx_connection_t *c)
+{
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(c);
+
+    if (h3c->total_bytes / 8 > h3c->payload_bytes + 1048576) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "http3 flood detected");
+
+        ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_NO_ERROR,
+                                        "HTTP/3 flood detected");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
diff --git a/src/http/v3/ngx_http_v3.h b/src/http/v3/ngx_http_v3.h
--- a/src/http/v3/ngx_http_v3.h
+++ b/src/http/v3/ngx_http_v3.h
@@ -128,6 +128,9 @@ struct ngx_http_v3_session_s {
     uint64_t                      max_push_id;
     uint64_t                      goaway_push_id;
 
+    off_t                         total_bytes;
+    off_t                         payload_bytes;
+
     ngx_uint_t                    goaway;  /* unsigned  goaway:1; */
 
     ngx_connection_t             *known_streams[NGX_HTTP_V3_MAX_KNOWN_STREAM];
@@ -136,6 +139,7 @@ struct ngx_http_v3_session_s {
 
 void ngx_http_v3_init(ngx_connection_t *c);
 ngx_int_t ngx_http_v3_init_session(ngx_connection_t *c);
+ngx_int_t ngx_http_v3_check_flood(ngx_connection_t *c);
 
 ngx_int_t ngx_http_v3_read_request_body(ngx_http_request_t *r);
 ngx_int_t ngx_http_v3_read_unbuffered_request_body(ngx_http_request_t *r);
diff --git a/src/http/v3/ngx_http_v3_filter_module.c b/src/http/v3/ngx_http_v3_filter_module.c
--- a/src/http/v3/ngx_http_v3_filter_module.c
+++ b/src/http/v3/ngx_http_v3_filter_module.c
@@ -101,6 +101,7 @@ ngx_http_v3_header_filter(ngx_http_reque
     ngx_list_part_t           *part;
     ngx_table_elt_t           *header;
     ngx_connection_t          *c;
+    ngx_http_v3_session_t     *h3c;
     ngx_http_v3_filter_ctx_t  *ctx;
     ngx_http_core_loc_conf_t  *clcf;
     ngx_http_core_srv_conf_t  *cscf;
@@ -120,6 +121,8 @@ ngx_http_v3_header_filter(ngx_http_reque
         return NGX_OK;
     }
 
+    h3c = ngx_http_v3_get_session(r->connection);
+
     if (r->method == NGX_HTTP_HEAD) {
         r->header_only = 1;
     }
@@ -531,6 +534,8 @@ ngx_http_v3_header_filter(ngx_http_reque
 
     n = b->last - b->pos;
 
+    h3c->payload_bytes += n;
+
     len = ngx_http_v3_encode_varlen_int(NULL, NGX_HTTP_V3_FRAME_HEADERS)
           + ngx_http_v3_encode_varlen_int(NULL, n);
 
@@ -571,6 +576,9 @@ ngx_http_v3_header_filter(ngx_http_reque
         b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last,
                                               r->headers_out.content_length_n);
 
+        h3c->payload_bytes += r->headers_out.content_length_n;
+        h3c->total_bytes += r->headers_out.content_length_n;
+
         cl = ngx_alloc_chain_link(r->pool);
         if (cl == NULL) {
             return NGX_ERROR;
@@ -590,6 +598,10 @@ ngx_http_v3_header_filter(ngx_http_reque
         ngx_http_set_ctx(r, ctx, ngx_http_v3_filter_module);
     }
 
+    for (cl = out; cl; cl = cl->next) {
+        h3c->total_bytes += cl->buf->last - cl->buf->pos;
+    }
+
     return ngx_http_write_filter(r, out);
 }
 
@@ -1096,9 +1108,12 @@ static ngx_chain_t *
 ngx_http_v3_create_push_promise(ngx_http_request_t *r, ngx_str_t *path,
     uint64_t push_id)
 {
-    size_t        n, len;
-    ngx_buf_t    *b;
-    ngx_chain_t  *hl, *cl;
+    size_t                  n, len;
+    ngx_buf_t              *b;
+    ngx_chain_t            *hl, *cl;
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(r->connection);
 
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                    "http3 create push promise id:%uL", push_id);
@@ -1233,6 +1248,8 @@ ngx_http_v3_create_push_promise(ngx_http
 
     n = b->last - b->pos;
 
+    h3c->payload_bytes += n;
+
     len = ngx_http_v3_encode_varlen_int(NULL, NGX_HTTP_V3_FRAME_PUSH_PROMISE)
           + ngx_http_v3_encode_varlen_int(NULL, n);
 
@@ -1265,6 +1282,7 @@ ngx_http_v3_body_filter(ngx_http_request
     ngx_int_t                  rc;
     ngx_buf_t                 *b;
     ngx_chain_t               *out, *cl, *tl, **ll;
+    ngx_http_v3_session_t     *h3c;
     ngx_http_v3_filter_ctx_t  *ctx;
 
     if (in == NULL) {
@@ -1276,6 +1294,8 @@ ngx_http_v3_body_filter(ngx_http_request
         return ngx_http_next_body_filter(r, in);
     }
 
+    h3c = ngx_http_v3_get_session(r->connection);
+
     out = NULL;
     ll = &out;
 
@@ -1340,6 +1360,8 @@ ngx_http_v3_body_filter(ngx_http_request
 
         tl->next = out;
         out = tl;
+
+        h3c->payload_bytes += size;
     }
 
     if (cl->buf->last_buf) {
@@ -1356,6 +1378,10 @@ ngx_http_v3_body_filter(ngx_http_request
         *ll = NULL;
     }
 
+    for (cl = out; cl; cl = cl->next) {
+        h3c->total_bytes += cl->buf->last - cl->buf->pos;
+    }
+
     rc = ngx_http_next_body_filter(r, out);
 
     ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &out,
@@ -1369,13 +1395,16 @@ static ngx_chain_t *
 ngx_http_v3_create_trailers(ngx_http_request_t *r,
     ngx_http_v3_filter_ctx_t *ctx)
 {
-    size_t            len, n;
-    u_char           *p;
-    ngx_buf_t        *b;
-    ngx_uint_t        i;
-    ngx_chain_t      *cl, *hl;
-    ngx_list_part_t  *part;
-    ngx_table_elt_t  *header;
+    size_t                  len, n;
+    u_char                 *p;
+    ngx_buf_t              *b;
+    ngx_uint_t              i;
+    ngx_chain_t            *cl, *hl;
+    ngx_list_part_t        *part;
+    ngx_table_elt_t        *header;
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(r->connection);
 
     len = 0;
 
@@ -1461,6 +1490,8 @@ ngx_http_v3_create_trailers(ngx_http_req
 
     n = b->last - b->pos;
 
+    h3c->payload_bytes += n;
+
     hl = ngx_chain_get_free_buf(r->pool, &ctx->free);
     if (hl == NULL) {
         return NULL;
diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c
--- a/src/http/v3/ngx_http_v3_request.c
+++ b/src/http/v3/ngx_http_v3_request.c
@@ -218,6 +218,7 @@ ngx_http_v3_process_request(ngx_event_t 
     ngx_int_t                     rc;
     ngx_connection_t             *c;
     ngx_http_request_t           *r;
+    ngx_http_v3_session_t        *h3c;
     ngx_http_core_srv_conf_t     *cscf;
     ngx_http_v3_parse_headers_t  *st;
 
@@ -233,6 +234,8 @@ ngx_http_v3_process_request(ngx_event_t 
         return;
     }
 
+    h3c = ngx_http_v3_get_session(c);
+
     st = &r->v3_parse->headers;
 
     b = r->header_in;
@@ -298,6 +301,12 @@ ngx_http_v3_process_request(ngx_event_t 
         }
 
         r->request_length += b->pos - p;
+        h3c->total_bytes += b->pos - p;
+
+        if (ngx_http_v3_check_flood(c) != NGX_OK) {
+            ngx_http_close_request(r, NGX_HTTP_CLOSE);
+            break;
+        }
 
         if (rc == NGX_BUSY) {
             if (rev->error) {
@@ -318,6 +327,10 @@ ngx_http_v3_process_request(ngx_event_t 
 
         /* rc == NGX_OK || rc == NGX_DONE */
 
+        h3c->payload_bytes += ngx_http_v3_encode_field_l(NULL,
+                                                   &st->field_rep.field.name,
+                                                   &st->field_rep.field.value);
+
         if (ngx_http_v3_process_header(r, &st->field_rep.field.name,
                                        &st->field_rep.field.value)
             != NGX_OK)
@@ -1080,6 +1093,7 @@ ngx_http_v3_request_body_filter(ngx_http
     ngx_buf_t                 *b;
     ngx_uint_t                 last;
     ngx_chain_t               *cl, *out, *tl, **ll;
+    ngx_http_v3_session_t     *h3c;
     ngx_http_request_body_t   *rb;
     ngx_http_core_loc_conf_t  *clcf;
     ngx_http_core_srv_conf_t  *cscf;
@@ -1088,6 +1102,8 @@ ngx_http_v3_request_body_filter(ngx_http
     rb = r->request_body;
     st = &r->v3_parse->body;
 
+    h3c = ngx_http_v3_get_session(r->connection);
+
     if (rb->rest == -1) {
 
         ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
@@ -1135,6 +1151,11 @@ ngx_http_v3_request_body_filter(ngx_http
                 rc = ngx_http_v3_parse_data(r->connection, st, cl->buf);
 
                 r->request_length += cl->buf->pos - p;
+                h3c->total_bytes += cl->buf->pos - p;
+
+                if (ngx_http_v3_check_flood(r->connection) != NGX_OK) {
+                    return NGX_HTTP_CLOSE;
+                }
 
                 if (rc == NGX_AGAIN) {
                     continue;
@@ -1178,6 +1199,8 @@ ngx_http_v3_request_body_filter(ngx_http
             {
                 rb->received += st->length;
                 r->request_length += st->length;
+                h3c->total_bytes += st->length;
+                h3c->payload_bytes += st->length;
 
                 if (st->length < 8) {
 
@@ -1222,12 +1245,16 @@ ngx_http_v3_request_body_filter(ngx_http
                 cl->buf->pos += (size_t) st->length;
                 rb->received += st->length;
                 r->request_length += st->length;
+                h3c->total_bytes += st->length;
+                h3c->payload_bytes += st->length;
                 st->length = 0;
 
             } else {
                 st->length -= size;
                 rb->received += size;
                 r->request_length += size;
+                h3c->total_bytes += size;
+                h3c->payload_bytes += size;
                 cl->buf->pos = cl->buf->last;
             }
 
diff --git a/src/http/v3/ngx_http_v3_streams.c b/src/http/v3/ngx_http_v3_streams.c
--- a/src/http/v3/ngx_http_v3_streams.c
+++ b/src/http/v3/ngx_http_v3_streams.c
@@ -171,6 +171,7 @@ ngx_http_v3_uni_read_handler(ngx_event_t
     ngx_buf_t                  b;
     ngx_int_t                  rc;
     ngx_connection_t          *c;
+    ngx_http_v3_session_t     *h3c;
     ngx_http_v3_uni_stream_t  *us;
 
     c = rev->data;
@@ -207,6 +208,14 @@ ngx_http_v3_uni_read_handler(ngx_event_t
         b.pos = buf;
         b.last = buf + n;
 
+        h3c = ngx_http_v3_get_session(c);
+        h3c->total_bytes += n;
+
+        if (ngx_http_v3_check_flood(c) != NGX_OK) {
+            ngx_http_v3_close_uni_stream(c);
+            return;
+        }
+
         rc = ngx_http_v3_parse_uni(c, &us->parse, &b);
 
         if (rc == NGX_DONE) {
@@ -282,6 +291,9 @@ ngx_http_v3_create_push_stream(ngx_conne
     p = (u_char *) ngx_http_v3_encode_varlen_int(p, push_id);
     n = p - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (sc->send(sc, buf, n) != (ssize_t) n) {
         goto failed;
     }
@@ -291,7 +303,6 @@ ngx_http_v3_create_push_stream(ngx_conne
         goto failed;
     }
 
-    h3c = ngx_http_v3_get_session(c);
     h3c->npushing++;
 
     cln->handler = ngx_http_v3_push_cleanup;
@@ -383,6 +394,9 @@ ngx_http_v3_get_uni_stream(ngx_connectio
 
     n = (u_char *) ngx_http_v3_encode_varlen_int(buf, type) - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (sc->send(sc, buf, n) != (ssize_t) n) {
         goto failed;
     }
@@ -403,6 +417,7 @@ ngx_http_v3_send_settings(ngx_connection
     u_char                  *p, buf[NGX_HTTP_V3_VARLEN_INT_LEN * 6];
     size_t                   n;
     ngx_connection_t        *cc;
+    ngx_http_v3_session_t   *h3c;
     ngx_http_v3_srv_conf_t  *h3scf;
 
     ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 send settings");
@@ -431,6 +446,9 @@ ngx_http_v3_send_settings(ngx_connection
     p = (u_char *) ngx_http_v3_encode_varlen_int(p, h3scf->max_blocked_streams);
     n = p - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (cc->send(cc, buf, n) != (ssize_t) n) {
         goto failed;
     }
@@ -448,9 +466,10 @@ failed:
 ngx_int_t
 ngx_http_v3_send_goaway(ngx_connection_t *c, uint64_t id)
 {
-    u_char            *p, buf[NGX_HTTP_V3_VARLEN_INT_LEN * 3];
-    size_t             n;
-    ngx_connection_t  *cc;
+    u_char                 *p, buf[NGX_HTTP_V3_VARLEN_INT_LEN * 3];
+    size_t                  n;
+    ngx_connection_t       *cc;
+    ngx_http_v3_session_t  *h3c;
 
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 send goaway %uL", id);
 
@@ -465,6 +484,9 @@ ngx_http_v3_send_goaway(ngx_connection_t
     p = (u_char *) ngx_http_v3_encode_varlen_int(p, id);
     n = p - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (cc->send(cc, buf, n) != (ssize_t) n) {
         goto failed;
     }
@@ -482,9 +504,10 @@ failed:
 ngx_int_t
 ngx_http_v3_send_ack_section(ngx_connection_t *c, ngx_uint_t stream_id)
 {
-    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
-    size_t             n;
-    ngx_connection_t  *dc;
+    u_char                  buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t                  n;
+    ngx_connection_t       *dc;
+    ngx_http_v3_session_t  *h3c;
 
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http3 client ack section %ui", stream_id);
@@ -497,6 +520,9 @@ ngx_http_v3_send_ack_section(ngx_connect
     buf[0] = 0x80;
     n = (u_char *) ngx_http_v3_encode_prefix_int(buf, stream_id, 7) - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (dc->send(dc, buf, n) != (ssize_t) n) {
         ngx_http_v3_close_uni_stream(dc);
         return NGX_ERROR;
@@ -509,9 +535,10 @@ ngx_http_v3_send_ack_section(ngx_connect
 ngx_int_t
 ngx_http_v3_send_cancel_stream(ngx_connection_t *c, ngx_uint_t stream_id)
 {
-    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
-    size_t             n;
-    ngx_connection_t  *dc;
+    u_char                  buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t                  n;
+    ngx_connection_t       *dc;
+    ngx_http_v3_session_t  *h3c;
 
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http3 client cancel stream %ui", stream_id);
@@ -524,6 +551,9 @@ ngx_http_v3_send_cancel_stream(ngx_conne
     buf[0] = 0x40;
     n = (u_char *) ngx_http_v3_encode_prefix_int(buf, stream_id, 6) - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (dc->send(dc, buf, n) != (ssize_t) n) {
         ngx_http_v3_close_uni_stream(dc);
         return NGX_ERROR;
@@ -536,9 +566,10 @@ ngx_http_v3_send_cancel_stream(ngx_conne
 ngx_int_t
 ngx_http_v3_send_inc_insert_count(ngx_connection_t *c, ngx_uint_t inc)
 {
-    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
-    size_t             n;
-    ngx_connection_t  *dc;
+    u_char                  buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t                  n;
+    ngx_connection_t       *dc;
+    ngx_http_v3_session_t  *h3c;
 
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http3 client increment insert count %ui", inc);
@@ -551,6 +582,9 @@ ngx_http_v3_send_inc_insert_count(ngx_co
     buf[0] = 0;
     n = (u_char *) ngx_http_v3_encode_prefix_int(buf, inc, 6) - buf;
 
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
     if (dc->send(dc, buf, n) != (ssize_t) n) {
         ngx_http_v3_close_uni_stream(dc);
         return NGX_ERROR;


More information about the nginx-devel mailing list