From 497cabd9e5717c350d64254d4530a3926e483f68 Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Tue, 14 Apr 2026 18:04:19 +0200 Subject: [PATCH] MEDIUM: quic: implement fe.stream.max-total Implement a new setting to limit the total number of bidirectional streams that the client may use on a single connection. By default, it is set to 0 which means it is not limited at all. If a positive value is configured, the client can only open a fixed number of request streams per QUIC connection. Internally, this is implemented in two steps : * First, MAX_STREAMS_BIDI flow control advertizing will be reduced when approaching the limit before being completely turned off when reaching it. This guarantees that the client cannot exceed the limit without violating the flow control. * Second, when attaching the latest stream with ID matching max-total setting, connection graceful shutdown is initiated. In HTTP/3, this results in a GOAWAY emission. This allows the remaining streams to be completed before the connection becomes completely idle. --- doc/configuration.txt | 18 +++++++++++++++++ include/haproxy/quic_tune-t.h | 1 + src/cfgparse-quic.c | 5 +++++ src/h3.c | 16 +++++++++++---- src/mux_quic.c | 38 +++++++++++++++++++++-------------- src/quic_tp.c | 6 ++++-- 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index b62778423..905c06810 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -1967,6 +1967,7 @@ The following keywords are supported in the "global" section : - tune.quic.fe.sock-per-conn - tune.quic.fe.stream.data-ratio - tune.quic.fe.stream.max-concurrent + - tune.quic.fe.stream.max-total - tune.quic.fe.stream.rxbuf - tune.quic.fe.tx.pacing - tune.quic.fe.tx.udp-gso @@ -5259,6 +5260,23 @@ tune.quic.fe.stream.max-concurrent See also: "tune.quic.be.stream.rxbuf", "tune.quic.fe.stream.rxbuf", "tune.quic.be.stream.data-ratio", "tune.quic.fe.stream.data-ratio" +tune.quic.fe.stream.max-total + Sets the maximum number of requests that can be handled by a single QUIC + connection. Once this total is reached, the connection will be gracefully + shutdown. In HTTP/3, this translates in a GOAWAY frame. + + This setting is applied as a hard limit on the connection via the QUIC flow + control mechanism. If a peer violates it, the connection will be immediately + closed. + + This setting can be used to force clients to open new connections once in a + while to continue the emission of requests and avoid maintaining connections + for too many times. However, low values will increase latency on the client + side, as well as CPU consumption on both sides due to TLS handshakes. + + The default value is 0 which implies no specific limit outside of the QUIC + protocol encoding limitation (2^60, more that a billion billion). + tune.quic.frontend.max-streams-bidi (deprecated) This keyword has been deprecated in 3.3 and will be removed in 3.5. It is part of the streamlining process apply on QUIC configuration. If used, this diff --git a/include/haproxy/quic_tune-t.h b/include/haproxy/quic_tune-t.h index 2d2675aa6..56caab602 100644 --- a/include/haproxy/quic_tune-t.h +++ b/include/haproxy/quic_tune-t.h @@ -43,6 +43,7 @@ struct quic_tune { uint sec_retry_threshold; uint stream_data_ratio; uint stream_max_concurrent; + uint stream_max_total; uint stream_rxbuf; uint opts; /* QUIC_TUNE_FE_* options specific to FE side */ uint fb_opts; /* QUIC_TUNE_FB_* options shared by both side */ diff --git a/src/cfgparse-quic.c b/src/cfgparse-quic.c index cef99bfe4..047179152 100644 --- a/src/cfgparse-quic.c +++ b/src/cfgparse-quic.c @@ -32,6 +32,7 @@ struct quic_tune quic_tune = { .sec_retry_threshold = QUIC_DFLT_SEC_RETRY_THRESHOLD, .stream_data_ratio = QUIC_DFLT_FE_STREAM_DATA_RATIO, .stream_max_concurrent = QUIC_DFLT_FE_STREAM_MAX_CONCURRENT, + .stream_max_total = 0, .stream_rxbuf = 0, .fb_opts = QUIC_TUNE_FB_TX_PACING|QUIC_TUNE_FB_TX_UDP_GSO, .opts = QUIC_TUNE_FE_SOCK_PER_CONN, @@ -473,6 +474,9 @@ static int cfg_parse_quic_tune_setting(char **args, int section_type, &quic_tune.fe.stream_max_concurrent; *ptr = arg; } + else if (strcmp(suffix, "fe.stream.max-total") == 0) { + quic_tune.fe.stream_max_total = arg; + } else if (strcmp(suffix, "be.stream.rxbuf") == 0 || strcmp(suffix, "fe.stream.rxbuf") == 0) { uint *ptr = (suffix[0] == 'b') ? &quic_tune.be.stream_rxbuf : @@ -716,6 +720,7 @@ static struct cfg_kw_list cfg_kws = {ILH, { { CFG_GLOBAL, "tune.quic.fe.sock-per-conn", cfg_parse_quic_tune_sock_per_conn }, { CFG_GLOBAL, "tune.quic.fe.stream.data-ratio", cfg_parse_quic_tune_setting }, { CFG_GLOBAL, "tune.quic.fe.stream.max-concurrent", cfg_parse_quic_tune_setting }, + { CFG_GLOBAL, "tune.quic.fe.stream.max-total", cfg_parse_quic_tune_setting }, { CFG_GLOBAL, "tune.quic.fe.stream.rxbuf", cfg_parse_quic_tune_setting }, { CFG_GLOBAL, "tune.quic.fe.tx.pacing", cfg_parse_quic_tune_on_off }, { CFG_GLOBAL, "tune.quic.fe.tx.udp-gso", cfg_parse_quic_tune_on_off }, diff --git a/src/h3.c b/src/h3.c index 4e5d52f5b..6a4798791 100644 --- a/src/h3.c +++ b/src/h3.c @@ -611,6 +611,7 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, struct ist meth = IST_NULL, path = IST_NULL; struct ist scheme = IST_NULL, authority = IST_NULL; struct ist uri; + uint64_t id_goaway; int hdr_idx, ret; int cookie = -1, last_cookie = -1, i; int relaxed = !!(h3c->qcc->proxy->options2 & PR_O2_REQBUG_OK); @@ -1058,10 +1059,10 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, htx_to_buf(htx, &htx_buf); htx = NULL; - if (qcs_attach_sc(qcs, &htx_buf, fin)) { - len = -1; - goto out; - } + /* Stream attach may need the new GOAWAY ID, so update it before. + * Keep a copy of the older value to restore it in case of error. + */ + id_goaway = h3c->id_goaway; /* RFC 9114 5.2. Connection Shutdown * @@ -1076,6 +1077,13 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, if (qcs->id >= h3c->id_goaway) h3c->id_goaway = qcs->id + 4; + if (qcs_attach_sc(qcs, &htx_buf, fin)) { + /* Stream not handled, restore old GOAWAY ID. */ + h3c->id_goaway = id_goaway; + len = -1; + goto out; + } + out: /* HTX may be non NULL if error before previous htx_to_buf(). */ if (htx) diff --git a/src/mux_quic.c b/src/mux_quic.c index b812955ec..aed5b941e 100644 --- a/src/mux_quic.c +++ b/src/mux_quic.c @@ -826,6 +826,18 @@ int qcc_fctl_avail_streams(const struct qcc *qcc, int bidi) } } +/* Retrieves the maximum number of bidirectional remote streams that the peer + * will be allowed to use during connection lifetime. This is guaranteed + * to be a positive integer. + */ +static uint64_t qcc_max_strm_bidi_remote(const struct connection *conn) +{ + /* On FE side, streams may be limited by stream.max-total configuration. */ + if (!conn_is_back(conn) && quic_tune.fe.stream_max_total) + return quic_tune.fe.stream_max_total; + return (uint64_t)1 << 60; +} + /* Open a locally initiated stream for the connection . Set for a * bidirectional stream, else an unidirectional stream is opened. The next * available ID on the connection will be used according to the stream type. @@ -1045,6 +1057,12 @@ int qcs_attach_sc(struct qcs *qcs, struct buffer *buf, char fin) se_fl_set_error(qcs->sd); } + /* Graceful shutdown is initiated as soon as max stream is reached. */ + if (qcs->id == (qcc_max_strm_bidi_remote(qcc->conn) - 1) * 4) { + TRACE_STATE("initiate shutdown as max remote bidi stream reached", QMUX_EV_STRM_RECV, qcc->conn, qcs); + qcc_app_shutdown(qcc); + } + out: TRACE_LEAVE(QMUX_EV_STRM_RECV, qcc->conn, qcs); return 0; @@ -2361,8 +2379,6 @@ int qcc_recv_stop_sending(struct qcc *qcc, uint64_t id, uint64_t err) return 1; } -#define QUIC_MAX_STREAMS_MAX_ID (1ULL<<60) - /* Signal the closing of remote stream with id . Flow-control for new * streams may be allocated for the peer if needed. */ @@ -2373,18 +2389,10 @@ static int qcc_release_remote_stream(struct qcc *qcc, uint64_t id) TRACE_ENTER(QMUX_EV_QCS_END, qcc->conn); if (quic_stream_is_bidi(id)) { - /* RFC 9000 4.6. Controlling Concurrency - * - * If a max_streams transport parameter or a MAX_STREAMS frame is - * received with a value greater than 260, this would allow a maximum - * stream ID that cannot be expressed as a variable-length integer; see - * Section 16. If either is received, the connection MUST be closed - * immediately with a connection error of type TRANSPORT_PARAMETER_ERROR - * if the offending value was received in a transport parameter or of - * type FRAME_ENCODING_ERROR if it was received in a frame; see Section - * 10.2. - */ - if (qcc->lfctl.ms_bidi == QUIC_MAX_STREAMS_MAX_ID) { + const uint64_t max = qcc_max_strm_bidi_remote(qcc->conn); + /* The peer must not have been authorized to open a stream outside of this range. */ + BUG_ON(qcc->lfctl.ms_bidi > max); + if (qcc->lfctl.ms_bidi == max) { TRACE_DATA("maximum streams value reached", QMUX_EV_QCC_SEND, qcc->conn); goto out; } @@ -2394,7 +2402,7 @@ static int qcc_release_remote_stream(struct qcc *qcc, uint64_t id) * the initial window or reaching the stream ID limit. */ if (qcc->lfctl.cl_bidi_r > qcc->lfctl.ms_bidi_init / 2 || - qcc->lfctl.cl_bidi_r + qcc->lfctl.ms_bidi == QUIC_MAX_STREAMS_MAX_ID) { + qcc->lfctl.cl_bidi_r + qcc->lfctl.ms_bidi == max) { TRACE_DATA("increase max stream limit with MAX_STREAMS_BIDI", QMUX_EV_QCC_SEND, qcc->conn); frm = qc_frm_alloc(QUIC_FT_MAX_STREAMS_BIDI); if (!frm) { diff --git a/src/quic_tp.c b/src/quic_tp.c index a56d6cb75..ea27efcc7 100644 --- a/src/quic_tp.c +++ b/src/quic_tp.c @@ -53,8 +53,10 @@ void quic_transport_params_init(struct quic_transport_params *p, int server) const uint64_t stream_rx_bufsz = qmux_stream_rx_bufsz(); const uint stream_rxbuf = server ? quic_tune.fe.stream_rxbuf : quic_tune.be.stream_rxbuf; - const int max_streams_bidi = server ? - quic_tune.fe.stream_max_concurrent : quic_tune.be.stream_max_concurrent; + /* On FE side, check if stream.max-total is set and inferior to stream.max-concurrent */ + const int max_streams_bidi = server && quic_tune.fe.stream_max_total ? + MIN(quic_tune.fe.stream_max_concurrent, quic_tune.fe.stream_max_total) : + server ? quic_tune.fe.stream_max_concurrent : quic_tune.be.stream_max_concurrent; /* TODO value used to conform with HTTP/3, should be derived from app_ops */ const int max_streams_uni = 3;