[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