[PATCH 0 of 5] QUIC flood detection

Roman Arutyunyan arut at nginx.com
Thu Oct 7 11:45:05 UTC 2021


On Thu, Oct 07, 2021 at 02:36:13PM +0300, Roman Arutyunyan wrote:
> This series adds support for flood detection in QUIC and HTTP/3 smilar to
> HTTP/2.
> 
>  - patch 1 removes client-side encoder support from HTTP/3 for simplicity
>  - patch 2 fixes a minor issue with $request_length calculation
>  - patch 3 adds HTTP/3 traffic-based flood detection
>  - patch 4 adds QUIC traffic-based flood detection
>  - patch 5 adds a limit on frames number similar to HTTP/2
>  
> As for the patch 3, both input and output traffic is analyzed similar to HTTP/2.
> Probably only input should be analyzed because current HTTP/3 implementation
> does not seem to allow amplification (the only exception is Stream Cancellation,
> but keepalive_requests limits the damage anyway).  Also, we can never be sure
> the output traffic we counted actually reached the client and was not rejected
> by stream reset.  We can discuss this later.

Testing:

I patched nghttp3/ngtcp2 to enable flooding:

  examples/client --http3-flood=10000000 127.0.0.1 8443 https://example.com:8443/bar

  examples/client --quic-flood=10000000 127.0.0.1 8443 https://example.com:8443/bar

Patches (quite dirty) are attached.  With --http3-flood, a big reserved
(0x1f + 0x21, see quic-http 34, section 7.2.8) frame is sent before
HEADERS frame.  With --quic-flood, a big number of PING frames are sent.

-- 
Roman Arutyunyan
-------------- next part --------------
diff --git a/lib/nghttp3_conn.c b/lib/nghttp3_conn.c
index ab608a2..93f8b52 100644
--- a/lib/nghttp3_conn.c
+++ b/lib/nghttp3_conn.c
@@ -2012,7 +2012,7 @@ int nghttp3_conn_add_ack_offset(nghttp3_conn *conn, int64_t stream_id,
 static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream,
                                     const nghttp3_nv *nva, size_t nvlen,
                                     const nghttp3_data_reader *dr) {
-  int rv;
+  int rv, i;
   nghttp3_nv *nnva;
   nghttp3_frame_entry frent;
 
@@ -2021,6 +2021,22 @@ static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream,
     return rv;
   }
 
+  for (i = 0; i < nvlen; i++) {
+    if (strcmp((char *) nva[i].name, "x-flood") == 0) {
+      break;
+    }
+  }
+
+  if (i < nvlen) {
+    frent.fr.hd.type = NGHTTP3_FRAME_FLOOD;
+    frent.fr.flood.size = atoi((char *) nva[i].value);
+
+    rv = nghttp3_stream_frq_add(stream, &frent);
+    if (rv != 0) {
+      return rv;
+    }
+  }
+
   frent.fr.hd.type = NGHTTP3_FRAME_HEADERS;
   frent.fr.headers.nva = nnva;
   frent.fr.headers.nvlen = nvlen;
diff --git a/lib/nghttp3_frame.c b/lib/nghttp3_frame.c
index 38c395e..dd27f36 100644
--- a/lib/nghttp3_frame.c
+++ b/lib/nghttp3_frame.c
@@ -79,6 +79,17 @@ uint8_t *nghttp3_frame_write_goaway(uint8_t *p,
   return p;
 }
 
+uint8_t *nghttp3_frame_write_flood(uint8_t *p,
+                                    const nghttp3_frame_flood *fr) {
+  p = nghttp3_frame_write_hd(p, &fr->hd);
+
+  for (int i = 0; i < fr->size; i++) {
+      *p++ = 0xfe;
+  }
+
+  return p;
+}
+
 size_t nghttp3_frame_write_goaway_len(int64_t *ppayloadlen,
                                       const nghttp3_frame_goaway *fr) {
   size_t payloadlen = nghttp3_put_varint_len(fr->id);
@@ -89,6 +100,16 @@ size_t nghttp3_frame_write_goaway_len(int64_t *ppayloadlen,
          nghttp3_put_varint_len((int64_t)payloadlen) + payloadlen;
 }
 
+size_t nghttp3_frame_write_flood_len(int64_t *ppayloadlen,
+                                      const nghttp3_frame_flood *fr) {
+  size_t payloadlen = fr->size;
+
+  *ppayloadlen = (int64_t)payloadlen;
+
+  return nghttp3_put_varint_len(NGHTTP3_FRAME_FLOOD) +
+         nghttp3_put_varint_len((int64_t)payloadlen) + payloadlen;
+}
+
 uint8_t *
 nghttp3_frame_write_priority_update(uint8_t *p,
                                     const nghttp3_frame_priority_update *fr) {
diff --git a/lib/nghttp3_frame.h b/lib/nghttp3_frame.h
index 216b346..06861a0 100644
--- a/lib/nghttp3_frame.h
+++ b/lib/nghttp3_frame.h
@@ -46,6 +46,7 @@ typedef enum nghttp3_frame_type {
      https://tools.ietf.org/html/draft-ietf-httpbis-priority-03 */
   NGHTTP3_FRAME_PRIORITY_UPDATE = 0x0f0700,
   NGHTTP3_FRAME_PRIORITY_UPDATE_PUSH_ID = 0x0f0701,
+  NGHTTP3_FRAME_FLOOD = 0x1f + 0x21
 } nghttp3_frame_type;
 
 typedef enum nghttp3_h2_reserved_type {
@@ -95,6 +96,11 @@ typedef struct nghttp3_frame_goaway {
   int64_t id;
 } nghttp3_frame_goaway;
 
+typedef struct nghttp3_frame_flood {
+  nghttp3_frame_hd hd;
+  size_t size;
+} nghttp3_frame_flood;
+
 typedef struct nghttp3_frame_priority_update {
   nghttp3_frame_hd hd;
   /* pri_elem_id is stream ID if hd.type ==
@@ -111,6 +117,7 @@ typedef union nghttp3_frame {
   nghttp3_frame_headers headers;
   nghttp3_frame_settings settings;
   nghttp3_frame_goaway goaway;
+  nghttp3_frame_flood flood;
   nghttp3_frame_priority_update priority_update;
 } nghttp3_frame;
 
@@ -154,6 +161,15 @@ size_t nghttp3_frame_write_settings_len(int64_t *pppayloadlen,
 uint8_t *nghttp3_frame_write_goaway(uint8_t *dest,
                                     const nghttp3_frame_goaway *fr);
 
+/*
+ * nghttp3_frame_write_flood writes FLOOD frame |fr| to |dest|.
+ * This function assumes that |dest| has enough space to write |fr|.
+ *
+ * This function returns |dest| plus the number of bytes written.
+ */
+uint8_t *nghttp3_frame_write_flood(uint8_t *dest,
+                                    const nghttp3_frame_flood *fr);
+
 /*
  * nghttp3_frame_write_goaway_len returns the number of bytes required
  * to write |fr|.  fr->hd.length is ignored.  This function stores
@@ -162,6 +178,14 @@ uint8_t *nghttp3_frame_write_goaway(uint8_t *dest,
 size_t nghttp3_frame_write_goaway_len(int64_t *ppayloadlen,
                                       const nghttp3_frame_goaway *fr);
 
+/*
+ * nghttp3_frame_write_flood_len returns the number of bytes required
+ * to write |fr|.  fr->hd.length is ignored.  This function stores
+ * payload length in |*ppayloadlen|.
+ */
+size_t nghttp3_frame_write_flood_len(int64_t *ppayloadlen,
+                                      const nghttp3_frame_flood *fr);
+
 /*
  * nghttp3_frame_write_priority_update writes PRIORITY_UPDATE frame
  * |fr| to |dest|.  This function assumes that |dest| has enough space
diff --git a/lib/nghttp3_stream.c b/lib/nghttp3_stream.c
index c91be7e..19ddc46 100644
--- a/lib/nghttp3_stream.c
+++ b/lib/nghttp3_stream.c
@@ -281,6 +281,12 @@ int nghttp3_stream_fill_outq(nghttp3_stream *stream) {
         return rv;
       }
       break;
+    case NGHTTP3_FRAME_FLOOD:
+      rv = nghttp3_stream_write_flood(stream, frent);
+      if (rv != 0) {
+        return rv;
+      }
+      break;
     case NGHTTP3_FRAME_PRIORITY_UPDATE:
       rv = nghttp3_stream_write_priority_update(stream, frent);
       if (rv != 0) {
@@ -390,6 +396,31 @@ int nghttp3_stream_write_goaway(nghttp3_stream *stream,
   return nghttp3_stream_outq_add(stream, &tbuf);
 }
 
+int nghttp3_stream_write_flood(nghttp3_stream *stream,
+                                nghttp3_frame_entry *frent) {
+  nghttp3_frame_flood *fr = &frent->fr.flood;
+  size_t len;
+  int rv;
+  nghttp3_buf *chunk;
+  nghttp3_typed_buf tbuf;
+
+  len = nghttp3_frame_write_flood_len(&fr->hd.length, fr);
+
+  rv = nghttp3_stream_ensure_chunk(stream, len);
+  if (rv != 0) {
+    return rv;
+  }
+
+  chunk = nghttp3_stream_get_chunk(stream);
+  typed_buf_shared_init(&tbuf, chunk);
+
+  chunk->last = nghttp3_frame_write_flood(chunk->last, fr);
+
+  tbuf.buf.last = chunk->last;
+
+  return nghttp3_stream_outq_add(stream, &tbuf);
+}
+
 int nghttp3_stream_write_priority_update(nghttp3_stream *stream,
                                          nghttp3_frame_entry *frent) {
   nghttp3_frame_priority_update *fr = &frent->fr.priority_update;
diff --git a/lib/nghttp3_stream.h b/lib/nghttp3_stream.h
index 047475e..a07a763 100644
--- a/lib/nghttp3_stream.h
+++ b/lib/nghttp3_stream.h
@@ -301,6 +301,9 @@ int nghttp3_stream_write_settings(nghttp3_stream *stream,
 int nghttp3_stream_write_goaway(nghttp3_stream *stream,
                                 nghttp3_frame_entry *frent);
 
+int nghttp3_stream_write_flood(nghttp3_stream *stream,
+                                nghttp3_frame_entry *frent);
+
 int nghttp3_stream_write_priority_update(nghttp3_stream *stream,
                                          nghttp3_frame_entry *frent);
 
-------------- next part --------------
diff --git a/examples/client.cc b/examples/client.cc
index 990688eb..556a5168 100644
--- a/examples/client.cc
+++ b/examples/client.cc
@@ -1611,7 +1611,7 @@ int Client::submit_http_request(const Stream *stream) {
 
   const auto &req = stream->req;
 
-  std::array<nghttp3_nv, 6> nva{
+  std::array<nghttp3_nv, 7> nva{
       util::make_nv(":method", config.http_method),
       util::make_nv(":scheme", req.scheme),
       util::make_nv(":authority", req.authority),
@@ -1623,6 +1623,11 @@ int Client::submit_http_request(const Stream *stream) {
     content_length_str = std::to_string(config.datalen);
     nva[nvlen++] = util::make_nv("content-length", content_length_str);
   }
+  if (config.http3_flood) {
+    static char buf[1024];
+    snprintf(buf, sizeof(buf), "%d", (int) config.http3_flood);
+    nva[nvlen++] = util::make_nv("x-flood", std::string(buf));
+  }
 
   if (!config.quiet) {
     debug::print_http_request_headers(stream->stream_id, nva.data(), nvlen);
@@ -1937,6 +1942,10 @@ int Client::setup_httpconn() {
     return -1;
   }
 
+  if (config.quic_flood) {
+      ngxtcp2_conn_flood(conn_, config.quic_flood);
+  }
+
   nghttp3_callbacks callbacks{
       ::http_acked_stream_data, ::http_stream_close,      ::http_recv_data,
       ::http_deferred_consume,  ::http_begin_headers,     ::http_recv_header,
@@ -2384,6 +2393,8 @@ int main(int argc, char **argv) {
         {"max-window", required_argument, &flag, 32},
         {"max-stream-window", required_argument, &flag, 33},
         {"scid", required_argument, &flag, 34},
+        {"http3-flood", required_argument, &flag, 35},
+        {"quic-flood", required_argument, &flag, 36},
         {nullptr, 0, nullptr, 0},
     };
 
@@ -2672,6 +2683,24 @@ int main(int argc, char **argv) {
         config.scid_present = true;
         break;
       }
+      case 35:
+        // --http3-flood
+        if (auto n = util::parse_uint_iec(optarg); !n) {
+          std::cerr << "http3-flood: invalid argument" << std::endl;
+          exit(EXIT_FAILURE);
+        } else {
+          config.http3_flood = *n;
+        }
+        break;
+      case 36:
+        // --quic-flood
+        if (auto n = util::parse_uint_iec(optarg); !n) {
+          std::cerr << "quic-flood: invalid argument" << std::endl;
+          exit(EXIT_FAILURE);
+        } else {
+          config.quic_flood = *n;
+        }
+        break;
       }
       break;
     default:
diff --git a/examples/client_base.h b/examples/client_base.h
index e118bac2..87ba246a 100644
--- a/examples/client_base.h
+++ b/examples/client_base.h
@@ -163,6 +163,10 @@ struct Config {
   std::string_view sni;
   // initial_rtt is an initial RTT.
   ngtcp2_duration initial_rtt;
+  // HTTP/3 flood
+  uint64_t http3_flood;
+  // QUIC flood
+  uint64_t quic_flood;
 };
 
 struct Buffer {
diff --git a/lib/includes/ngtcp2/ngtcp2.h b/lib/includes/ngtcp2/ngtcp2.h
index 235d6e6b..e737215a 100644
--- a/lib/includes/ngtcp2/ngtcp2.h
+++ b/lib/includes/ngtcp2/ngtcp2.h
@@ -5173,6 +5173,13 @@ NGTCP2_EXTERN void ngtcp2_path_copy(ngtcp2_path *dest, const ngtcp2_path *src);
  */
 NGTCP2_EXTERN int ngtcp2_path_eq(const ngtcp2_path *a, const ngtcp2_path *b);
 
+/**
+ * @function
+ *
+ * `ngtcp2_conn_flood` floods with PING frames.
+ */
+NGTCP2_EXTERN int ngxtcp2_conn_flood(ngtcp2_conn *conn, size_t size);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/ngtcp2_conn.c b/lib/ngtcp2_conn.c
index b4b4145b..2ea05132 100644
--- a/lib/ngtcp2_conn.c
+++ b/lib/ngtcp2_conn.c
@@ -8844,6 +8844,28 @@ static int conn_enqueue_handshake_done(ngtcp2_conn *conn) {
   return 0;
 }
 
+/*
+ * ngtcp2_conn_flood floods with PING frames
+ */
+int ngxtcp2_conn_flood(ngtcp2_conn *conn, size_t size) {
+  ngtcp2_pktns *pktns = &conn->pktns;
+  ngtcp2_frame_chain *nfrc;
+  int rv;
+
+  while (size--) {
+      rv = ngtcp2_frame_chain_new(&nfrc, conn->mem);
+      if (rv != 0) {
+        return rv;
+      }
+
+      nfrc->fr.type = NGTCP2_FRAME_PING;
+      nfrc->next = pktns->tx.frq;
+      pktns->tx.frq = nfrc;
+  }
+
+  return 0;
+}
+
 /**
  * @function
  *


More information about the nginx-devel mailing list