From: Amaury Denoyelle Date: Wed, 8 Nov 2023 13:29:31 +0000 (+0100) Subject: MEDIUM: quic: define an accept queue limit X-Git-Tag: v2.9-dev10~109 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bb28215d9b75e75a286e0252fd8db46def497ed5;p=thirdparty%2Fhaproxy.git MEDIUM: quic: define an accept queue limit 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. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index f1b2d0eec2..7b3364f92d 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -4659,10 +4659,11 @@ backlog 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 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. diff --git a/include/haproxy/quic_sock.h b/include/haproxy/quic_sock.h index 577eea2740..c1053f9c67 100644 --- a/include/haproxy/quic_sock.h +++ b/include/haproxy/quic_sock.h @@ -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 */ diff --git a/include/haproxy/receiver-t.h b/include/haproxy/receiver-t.h index f98df0d898..4527b5f412 100644 --- a/include/haproxy/receiver-t.h +++ b/include/haproxy/receiver-t.h @@ -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. */ diff --git a/src/cfgparse.c b/src/cfgparse.c index 93a7afeb8c..11d262455d 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -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 */ diff --git a/src/quic_conn.c b/src/quic_conn.c index 2b13c18b2c..efd02cda87 100644 --- a/src/quic_conn.c +++ b/src/quic_conn.c @@ -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); diff --git a/src/quic_rx.c b/src/quic_rx.c index 96754ebe4a..9f11a3e168 100644 --- a/src/quic_rx.c +++ b/src/quic_rx.c @@ -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", diff --git a/src/quic_sock.c b/src/quic_sock.c index 035aa7832f..beb291c4f9 100644 --- a/src/quic_sock.c +++ b/src/quic_sock.c @@ -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 instance. This reuses the listener - * backlog value. + * complete in parallel on listener 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 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)