]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: quic: define an accept queue limit
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Wed, 8 Nov 2023 13:29:31 +0000 (14:29 +0100)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Thu, 9 Nov 2023 15:24:00 +0000 (16:24 +0100)
QUIC connections are pushed manually into a dedicated listener queue
when they are ready to be accepted. This happens after handshake
finalization or on 0-RTT packet reception. Listener is then woken up to
dequeue them with listener_accept().

This patch comptabilizes the number of connections currently stored in
the accept queue. If reaching a certain limit, INITIAL packets are
dropped on reception to prevent further QUIC connections allocation.
This should help to preserve system resources.

This limit is automatically derived from the listener backlog. Half of
its value is reserved for handshakes and the other half for accept
queues. By default, backlog is equal to maxconn which guarantee that
there can't be no more than maxconn connections in handshake or waiting
to be accepted.

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

index f1b2d0eec2967045b9280632de4092f449a86bd1..7b3364f92ddc56c9d99b105e4a8c9dea0d7cff50 100644 (file)
@@ -4659,10 +4659,11 @@ backlog <conns>
               system, it may represent the number of already acknowledged
               connections, of non-acknowledged ones, or both.
 
-  This option is both used for stream and datagram listeners.
+  This option is only meaningful for stream listeners, including QUIC ones. Its
+  behavior however is not identical with QUIC instances.
 
-  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
+  For all listeners but QUIC, 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
@@ -4674,10 +4675,14 @@ backlog <conns>
   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.
+  For QUIC listeners, backlog sets a shared limits for both the maximum count
+  of active handshakes and connections waiting to be accepted. The handshake
+  phase relies primarily of the network latency with the remote peer, whereas
+  the second phase depends solely on haproxy load. When either one of this
+  limit is reached, haproxy starts to drop reception of INITIAL packets,
+  preventing any new connection allocation, until the connection excess starts
+  to decrease. This situation may cause browsers to silently downgrade the HTTP
+  versions and switching to TCP.
 
   See also : "maxconn" and the target operating system's tuning guide.
 
index 577eea2740d7e53f3536a155bfcf64ea8e694fb8..c1053f9c6753c8d5f68e4625399ee15e11ab4afe 100644 (file)
@@ -70,6 +70,7 @@ 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);
+int quic_listener_max_accept(const struct listener *l);
 
 #endif /* USE_QUIC */
 #endif /* _HAPROXY_QUIC_SOCK_H */
index f98df0d8989f32224bbf4ab94bde017935bd0770..4527b5f412a3a860a6cb6de72b377ef3333bba46 100644 (file)
@@ -82,6 +82,7 @@ struct receiver {
        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 */
+       unsigned int quic_curr_accept;   /* count of QUIC conns waiting for accept */
 #endif
        struct {
                struct task *task;  /* Task used to open connection for reverse. */
index 93a7afeb8c4d6e416d234fcbfaf846e58d3f37f7..11d262455dd24e2825edcfb4a7ca2a09acf36561 100644 (file)
@@ -4188,6 +4188,7 @@ init_proxies_list_stage2:
                                /* quic_conn are counted against maxconn. */
                                listener->bind_conf->options |= BC_O_XPRT_MAXCONN;
                                listener->rx.quic_curr_handshake = 0;
+                               listener->rx.quic_curr_accept = 0;
 
 # ifdef USE_QUIC_OPENSSL_COMPAT
                                /* store the last checked bind_conf in bind_conf */
index 2b13c18b2ca0d3d3dd5638503c30f0a436900539..efd02cda876f064c7d6c07ba03ba90e5535d0a1f 100644 (file)
@@ -1511,7 +1511,11 @@ void quic_conn_release(struct quic_conn *qc)
        /* in the unlikely (but possible) case the connection was just added to
         * the accept_list we must delete it from there.
         */
-       MT_LIST_DELETE(&qc->accept_list);
+       if (MT_LIST_INLIST(&qc->accept_list)) {
+               MT_LIST_DELETE(&qc->accept_list);
+               BUG_ON(qc->li->rx.quic_curr_accept == 0);
+               HA_ATOMIC_DEC(&qc->li->rx.quic_curr_accept);
+       }
 
        /* free remaining stream descriptors */
        node = eb64_first(&qc->streams_by_id);
index 96754ebe4a6503a3f0d43a7c769a2cdf71ba035f..9f11a3e16879562fb126c05653d621bb117fb514 100644 (file)
@@ -1966,6 +1966,13 @@ static struct quic_conn *quic_rx_pkt_retrieve_conn(struct quic_rx_packet *pkt,
                                goto out;
                        }
 
+                       if (unlikely(HA_ATOMIC_LOAD(&l->rx.quic_curr_accept) >=
+                                    quic_listener_max_accept(l))) {
+                               TRACE_DATA("Drop INITIAL on max accept",
+                                           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 035aa7832fa8e98046efde522fef48e95f4d1117..beb291c4f9e4a02839ae5d23735bce12cd4bef3e 100644 (file)
@@ -158,7 +158,15 @@ struct connection *quic_sock_accept_conn(struct listener *l, int *status)
 
  done:
        *status = CO_AC_DONE;
-       return qc ? qc->conn : NULL;
+
+       if (qc) {
+               BUG_ON(l->rx.quic_curr_accept <= 0);
+               HA_ATOMIC_DEC(&l->rx.quic_curr_accept);
+               return qc->conn;
+       }
+       else {
+               return NULL;
+       }
 
  err:
        /* in case of error reinsert the element to process it later. */
@@ -928,6 +936,7 @@ void quic_accept_push_qc(struct quic_conn *qc)
                return;
 
        BUG_ON(MT_LIST_INLIST(&qc->accept_list));
+       HA_ATOMIC_INC(&qc->li->rx.quic_curr_accept);
 
        qc->flags |= QUIC_FL_CONN_ACCEPT_REGISTERED;
        /* 1. insert the listener in the accept queue
@@ -967,12 +976,21 @@ struct task *quic_accept_run(struct task *t, void *ctx, unsigned int i)
 }
 
 /* Returns the maximum number of QUIC connections waiting for handshake to
- * complete in parallel on listener <l> instance. This reuses the listener
- * backlog value.
+ * complete in parallel on listener <l> instance. This is directly based on
+ * listener backlog value.
  */
 int quic_listener_max_handshake(const struct listener *l)
 {
-       return listener_backlog(l);
+       return listener_backlog(l) / 2;
+}
+
+/* Returns the value which is considered as the maximum number of QUIC
+ * connections waiting to be accepted for listener <l> instance. This is
+ * directly based on listener backlog value.
+ */
+int quic_listener_max_accept(const struct listener *l)
+{
+       return listener_backlog(l) / 2;
 }
 
 static int quic_alloc_accept_queues(void)