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.
This commit is contained in:
Amaury Denoyelle 2023-11-08 14:29:31 +01:00
parent 3df6a60113
commit bb28215d9b
7 changed files with 49 additions and 12 deletions

View 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.

View 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 */

View 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. */

View File

@ -4188,6 +4188,7 @@ int check_config_validity()
/* 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 */

View 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);

View 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",

View 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)