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;