]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: quic: limit handshake per listener
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Mon, 6 Nov 2023 15:34:38 +0000 (16:34 +0100)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Thu, 9 Nov 2023 15:23:52 +0000 (16:23 +0100)
Implement a limit per listener for concurrent number of QUIC
connections. When reached, INITIAL packets for new connections are
automatically dropped until the number of handshakes is reduced.

The limit value is automatically based on listener backlog, which itself
defaults to maxconn.

This feature is important to ensure CPU and memory resources are not
consume if too many handshakes attempt are started in parallel.

Special care is taken if a connection is released before handshake
completion. In this case, counter must be decremented. This forces to
ensure that member <qc.state> is set early in qc_new_conn() before any
quic_conn_release() invocation.

doc/configuration.txt
include/haproxy/quic_sock.h
include/haproxy/receiver-t.h
src/cfgparse.c
src/quic_conn.c
src/quic_rx.c
src/quic_sock.c
src/quic_ssl.c

index 0d192d1beaa8f3f5bc27f615c9bac845bb3abb00..f1b2d0eec2967045b9280632de4092f449a86bd1 100644 (file)
@@ -4659,19 +4659,26 @@ backlog <conns>
               system, it may represent the number of already acknowledged
               connections, of non-acknowledged ones, or both.
 
-  In order to protect against SYN flood attacks, one solution is to increase
-  the system's SYN backlog size. Depending on the system, sometimes it is just
-  tunable via a system parameter, sometimes it is not adjustable at all, and
-  sometimes the system relies on hints given by the application at the time of
-  the listen() syscall. By default, HAProxy passes the frontend's maxconn value
-  to the listen() syscall. On systems which can make use of this value, it can
-  sometimes be useful to be able to specify a different value, hence this
-  backlog parameter.
+  This option is both used for stream and datagram listeners.
+
+  In order to protect against SYN flood attacks on a stream-based listener, one
+  solution is to increase the system's SYN backlog size. Depending on the
+  system, sometimes it is just tunable via a system parameter, sometimes it is
+  not adjustable at all, and sometimes the system relies on hints given by the
+  application at the time of the listen() syscall. By default, HAProxy passes
+  the frontend's maxconn value to the listen() syscall. On systems which can
+  make use of this value, it can sometimes be useful to be able to specify a
+  different value, hence this backlog parameter.
 
   On Linux 2.4, the parameter is ignored by the system. On Linux 2.6, it is
   used as a hint and the system accepts up to the smallest greater power of
   two, and never more than some limits (usually 32768).
 
+  When using a QUIC listener, this option has a similar albeit not quite
+  equivalent meaning. It will set the maximum number of connections waiting for
+  handshake completion. When this limit is reached, INITIAL packets are dropped
+  to prevent creation of a new QUIC connection.
+
   See also : "maxconn" and the target operating system's tuning guide.
 
 
index acbe45e689ad2ee4948af1af73a4bb1c86d7d0e9..577eea2740d7e53f3536a155bfcf64ea8e694fb8 100644 (file)
@@ -69,6 +69,8 @@ void qc_want_recv(struct quic_conn *qc);
 
 void quic_accept_push_qc(struct quic_conn *qc);
 
+int quic_listener_max_handshake(const struct listener *l);
+
 #endif /* USE_QUIC */
 #endif /* _HAPROXY_QUIC_SOCK_H */
 
index e21f06b190d717e4b1e55022eb7a86d18ce2f15f..f98df0d8989f32224bbf4ab94bde017935bd0770 100644 (file)
@@ -81,6 +81,7 @@ struct receiver {
 #ifdef USE_QUIC
        struct mt_list rxbuf_list;       /* list of buffers to receive and dispatch QUIC datagrams. */
        enum quic_sock_mode quic_mode;   /* QUIC socket allocation strategy */
+       unsigned int quic_curr_handshake; /* count of active QUIC handshakes */
 #endif
        struct {
                struct task *task;  /* Task used to open connection for reverse. */
index 2d5b78369fb191666bf4589af80652134e2590f2..93a7afeb8c4d6e416d234fcbfaf846e58d3f37f7 100644 (file)
@@ -4187,6 +4187,7 @@ init_proxies_list_stage2:
                        if (listener->bind_conf->xprt == xprt_get(XPRT_QUIC)) {
                                /* quic_conn are counted against maxconn. */
                                listener->bind_conf->options |= BC_O_XPRT_MAXCONN;
+                               listener->rx.quic_curr_handshake = 0;
 
 # ifdef USE_QUIC_OPENSSL_COMPAT
                                /* store the last checked bind_conf in bind_conf */
index b21e6eb1eea5dd1619ae68a5d29c2d89590650d2..2b13c18b2ca0d3d3dd5638503c30f0a436900539 100644 (file)
@@ -1137,6 +1137,30 @@ struct task *qc_process_timer(struct task *task, void *ctx, unsigned int state)
        return task;
 }
 
+/* Try to increment <l> handshake current counter. If listener limit is
+ * reached, incrementation is rejected and 0 is returned.
+ */
+static int quic_increment_curr_handshake(struct listener *l)
+{
+       unsigned int count, next;
+       const int max = quic_listener_max_handshake(l);
+
+       do {
+               count = l->rx.quic_curr_handshake;
+               if (count >= max) {
+                       /* maxconn reached */
+                       next = 0;
+                       goto end;
+               }
+
+               /* try to increment quic_curr_handshake */
+               next = count + 1;
+       } while (!_HA_ATOMIC_CAS(&l->rx.quic_curr_handshake, &count, next) && __ha_cpu_relax());
+
+ end:
+       return next;
+}
+
 /* Allocate a new QUIC connection with <version> as QUIC version. <ipv4>
  * boolean is set to 1 for IPv4 connection, 0 for IPv6. <server> is set to 1
  * for QUIC servers (or haproxy listeners).
@@ -1161,7 +1185,7 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4,
        struct quic_conn *qc = NULL;
        struct listener *l = NULL;
        struct quic_cc_algo *cc_algo = NULL;
-       unsigned int next_actconn = 0, next_sslconn = 0;
+       unsigned int next_actconn = 0, next_sslconn = 0, next_handshake = 0;
 
        TRACE_ENTER(QUIC_EV_CONN_INIT);
 
@@ -1178,6 +1202,14 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4,
                goto err;
        }
 
+       if (server) {
+               next_handshake = quic_increment_curr_handshake(owner);
+               if (!next_handshake) {
+                       TRACE_STATE("max handshake reached", QUIC_EV_CONN_INIT);
+                       goto err;
+               }
+       }
+
        qc = pool_alloc(pool_head_quic_conn);
        if (!qc) {
                TRACE_ERROR("Could not allocate a new connection", QUIC_EV_CONN_INIT);
@@ -1187,7 +1219,7 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4,
        /* Now that quic_conn instance is allocated, quic_conn_release() will
         * ensure global accounting is decremented.
         */
-       next_sslconn = next_actconn = 0;
+       next_handshake = next_sslconn = next_actconn = 0;
 
        /* Initialize in priority qc members required for a safe dealloc. */
        qc->nictx = NULL;
@@ -1237,20 +1269,6 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4,
        /* Required to safely call quic_conn_prx_cntrs_update() from quic_conn_release(). */
        qc->prx_counters = NULL;
 
-       /* Now proceeds to allocation of qc members. */
-       qc->rx.buf.area = pool_alloc(pool_head_quic_conn_rxbuf);
-       if (!qc->rx.buf.area) {
-               TRACE_ERROR("Could not allocate a new RX buffer", QUIC_EV_CONN_INIT, qc);
-               goto err;
-       }
-
-       qc->cids = pool_alloc(pool_head_quic_cids);
-       if (!qc->cids) {
-               TRACE_ERROR("Could not allocate a new CID tree", QUIC_EV_CONN_INIT, qc);
-               goto err;
-       }
-
-       *qc->cids = EB_ROOT;
        /* QUIC Server (or listener). */
        if (server) {
                struct proxy *prx;
@@ -1281,6 +1299,20 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4,
        qc->mux_state = QC_MUX_NULL;
        qc->err = quic_err_transport(QC_ERR_NO_ERROR);
 
+       /* Now proceeds to allocation of qc members. */
+       qc->rx.buf.area = pool_alloc(pool_head_quic_conn_rxbuf);
+       if (!qc->rx.buf.area) {
+               TRACE_ERROR("Could not allocate a new RX buffer", QUIC_EV_CONN_INIT, qc);
+               goto err;
+       }
+
+       qc->cids = pool_alloc(pool_head_quic_cids);
+       if (!qc->cids) {
+               TRACE_ERROR("Could not allocate a new CID tree", QUIC_EV_CONN_INIT, qc);
+               goto err;
+       }
+       *qc->cids = EB_ROOT;
+
        conn_id->qc = qc;
 
        if (HA_ATOMIC_LOAD(&l->rx.quic_mode) == QUIC_SOCK_MODE_CONN &&
@@ -1401,6 +1433,8 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4,
                _HA_ATOMIC_DEC(&actconn);
        if (next_sslconn)
                _HA_ATOMIC_DEC(&global.sslconns);
+       if (next_handshake)
+               _HA_ATOMIC_DEC(&l->rx.quic_curr_handshake);
 
        TRACE_LEAVE(QUIC_EV_CONN_INIT);
        return NULL;
@@ -1537,6 +1571,14 @@ void quic_conn_release(struct quic_conn *qc)
                HA_ATOMIC_DEC(&qc->prx_counters->half_open_conn);
        }
 
+       /* Connection released before handshake completion. */
+       if (unlikely(qc->state < QUIC_HS_ST_COMPLETE)) {
+               if (qc_is_listener(qc)) {
+                       BUG_ON(qc->li->rx.quic_curr_handshake == 0);
+                       HA_ATOMIC_DEC(&qc->li->rx.quic_curr_handshake);
+               }
+       }
+
        pool_free(pool_head_quic_conn, qc);
        qc = NULL;
 
index c652c9465db6c2886fe72855905a6048bec4747d..96754ebe4a6503a3f0d43a7c769a2cdf71ba035f 100644 (file)
@@ -1958,6 +1958,14 @@ static struct quic_conn *quic_rx_pkt_retrieve_conn(struct quic_rx_packet *pkt,
                        struct quic_connection_id *conn_id;
                        int ipv4;
 
+                       /* Reject INITIAL early if listener limits reached. */
+                       if (unlikely(HA_ATOMIC_LOAD(&l->rx.quic_curr_handshake) >=
+                                    quic_listener_max_handshake(l))) {
+                               TRACE_DATA("Drop INITIAL on max handshake",
+                                           QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version);
+                               goto out;
+                       }
+
                        if (!pkt->token_len && !(l->bind_conf->options & BC_O_QUIC_FORCE_RETRY) &&
                            HA_ATOMIC_LOAD(&prx_counters->half_open_conn) >= global.tune.quic_retry_threshold) {
                                TRACE_PROTO("Initial without token, sending retry",
index 08ad726efad4bea7ea745315a220a0fb6e167550..035aa7832fa8e98046efde522fef48e95f4d1117 100644 (file)
@@ -966,6 +966,15 @@ struct task *quic_accept_run(struct task *t, void *ctx, unsigned int i)
        return NULL;
 }
 
+/* Returns the maximum number of QUIC connections waiting for handshake to
+ * complete in parallel on listener <l> instance. This reuses the listener
+ * backlog value.
+ */
+int quic_listener_max_handshake(const struct listener *l)
+{
+       return listener_backlog(l);
+}
+
 static int quic_alloc_accept_queues(void)
 {
        int i;
index a5b0900cb6fdd1364494e71aca4e8351b2d3e63f..88943e8a292237b3fddd7b2d70387ff32fb81f07 100644 (file)
@@ -572,6 +572,9 @@ int qc_ssl_provide_quic_data(struct ncbuf *ncbuf,
                        qc->state = QUIC_HS_ST_CONFIRMED;
                        /* The connection is ready to be accepted. */
                        quic_accept_push_qc(qc);
+
+                       BUG_ON(qc->li->rx.quic_curr_handshake == 0);
+                       HA_ATOMIC_DEC(&qc->li->rx.quic_curr_handshake);
                }
                else {
                        qc->state = QUIC_HS_ST_COMPLETE;