From 4120faf2898f73da15def2c4d2b008a0331f4818 Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Tue, 3 Mar 2026 09:41:26 +0100 Subject: [PATCH] MINOR: quic/h3: reorganize stream reject after MUX closure The QUIC MUX layer is closed after its transport counterpart. This may be necessary then to reject any new streams opened by the remote peer. This operation is dependent however from the application protocol. Previously, a function qc_h3_request_reject() was directly implemented in quic_conn source file for use when HTTP/3 was previously negotiated. However, this solution was not evolutive and broke layering. This patch introduces a new proper separation with a callback defined in quic_conn structure. When set, it will be used to preemptively close any new stream. QUIC MUX is responsible to set it just before its closure. No functional change. This patch is purely a refactoring with a better architecture design. Especially, H3 specific code from transport layer is now completely removed. --- include/haproxy/mux_quic-t.h | 3 ++ include/haproxy/quic_conn-t.h | 3 ++ src/h3.c | 52 ++++++++++++++++++++++++++++++++ src/mux_quic.c | 8 +++-- src/quic_conn.c | 56 +---------------------------------- src/quic_rx.c | 5 ++-- 6 files changed, 67 insertions(+), 60 deletions(-) diff --git a/include/haproxy/mux_quic-t.h b/include/haproxy/mux_quic-t.h index 28a8103a5..aa7b17706 100644 --- a/include/haproxy/mux_quic-t.h +++ b/include/haproxy/mux_quic-t.h @@ -232,6 +232,9 @@ struct qcc_app_ops { void (*inc_err_cnt)(void *ctx, int err_code); /* Set QCC error code as suspicious activity has been detected. */ void (*report_susp)(void *ctx); + + /* Free function to close a stream after MUX layer shutdown. */ + int (*strm_reject)(struct list *out, uint64_t id); }; #endif /* USE_QUIC */ diff --git a/include/haproxy/quic_conn-t.h b/include/haproxy/quic_conn-t.h index ca23260cd..3baad3fc1 100644 --- a/include/haproxy/quic_conn-t.h +++ b/include/haproxy/quic_conn-t.h @@ -409,6 +409,9 @@ struct quic_conn { unsigned int hs_expire; const struct qcc_app_ops *app_ops; + /* Callback to close any stream after MUX closure - set by the MUX itself */ + int (*strm_reject)(struct list *out, uint64_t stream_id); + /* Proxy counters */ struct quic_counters *prx_counters; diff --git a/src/h3.c b/src/h3.c index 2dafb5589..ebf1830e6 100644 --- a/src/h3.c +++ b/src/h3.c @@ -3345,6 +3345,57 @@ static void h3_trace(enum trace_level level, uint64_t mask, } } +/* Cancel a request on stream id . This is useful when the client opens a + * new stream but the MUX has already been released. A STOP_SENDING + + * RESET_STREAM frames are prepared for emission. + * + * Returns 1 on success else 0. + */ +int h3_reject(struct list *out, uint64_t id) +{ + int ret = 0; + struct quic_frame *ss, *rs; + const uint64_t app_error_code = H3_ERR_REQUEST_REJECTED; + + TRACE_ENTER(H3_EV_TX_FRAME); + + /* Do not emit rejection for unknown unidirectional stream as it is + * forbidden to close some of them (H3 control stream and QPACK + * encoder/decoder streams). + */ + if (quic_stream_is_uni(id)) { + ret = 1; + goto out; + } + + ss = qc_frm_alloc(QUIC_FT_STOP_SENDING); + if (!ss) { + TRACE_ERROR("failed to allocate quic_frame", H3_EV_TX_FRAME); + goto out; + } + + ss->stop_sending.id = id; + ss->stop_sending.app_error_code = app_error_code; + + rs = qc_frm_alloc(QUIC_FT_RESET_STREAM); + if (!rs) { + TRACE_ERROR("failed to allocate quic_frame", H3_EV_TX_FRAME); + qc_frm_free(NULL, &ss); + goto out; + } + + rs->reset_stream.id = id; + rs->reset_stream.app_error_code = app_error_code; + rs->reset_stream.final_size = 0; + + LIST_APPEND(out, &ss->list); + LIST_APPEND(out, &rs->list); + ret = 1; + out: + TRACE_LEAVE(H3_EV_TX_FRAME); + return ret; +} + /* HTTP/3 application layer operations */ const struct qcc_app_ops h3_ops = { .init = h3_init, @@ -3360,4 +3411,5 @@ const struct qcc_app_ops h3_ops = { .inc_err_cnt = h3_stats_inc_err_cnt, .report_susp = h3_report_susp, .release = h3_release, + .strm_reject = h3_reject, }; diff --git a/src/mux_quic.c b/src/mux_quic.c index 0f70d9b9b..512d0eda9 100644 --- a/src/mux_quic.c +++ b/src/mux_quic.c @@ -3377,8 +3377,12 @@ static void qcc_release(struct qcc *qcc) qcc_clear_frms(qcc); - if (qcc->app_ops && qcc->app_ops->release) - qcc->app_ops->release(qcc->ctx); + if (qcc->app_ops) { + if (qcc->app_ops->release) + qcc->app_ops->release(qcc->ctx); + if (conn->handle.qc) + conn->handle.qc->strm_reject = qcc->app_ops->strm_reject; + } TRACE_PROTO("application layer released", QMUX_EV_QCC_END, conn); pool_free(pool_head_qcc, qcc); diff --git a/src/quic_conn.c b/src/quic_conn.c index 61552e905..6143fce39 100644 --- a/src/quic_conn.c +++ b/src/quic_conn.c @@ -377,61 +377,6 @@ void quic_conn_closed_err_count_inc(struct quic_conn *qc, struct quic_frame *frm TRACE_LEAVE(QUIC_EV_CONN_CLOSE, qc); } -/* Cancel a request on connection for stream id . This is useful when - * the client opens a new stream but the MUX has already been released. A - * STOP_SENDING + RESET_STREAM frames are prepared for emission. - * - * TODO this function is closely related to H3. Its place should be in H3 layer - * instead of quic-conn but this requires an architecture adjustment. - * - * Returns 1 on success else 0. - */ -int qc_h3_request_reject(struct quic_conn *qc, uint64_t id) -{ - int ret = 0; - struct quic_frame *ss, *rs; - struct quic_enc_level *qel = qc->ael; - const uint64_t app_error_code = H3_ERR_REQUEST_REJECTED; - - TRACE_ENTER(QUIC_EV_CONN_PRSHPKT, qc); - - /* Do not emit rejection for unknown unidirectional stream as it is - * forbidden to close some of them (H3 control stream and QPACK - * encoder/decoder streams). - */ - if (quic_stream_is_uni(id)) { - ret = 1; - goto out; - } - - ss = qc_frm_alloc(QUIC_FT_STOP_SENDING); - if (!ss) { - TRACE_ERROR("failed to allocate quic_frame", QUIC_EV_CONN_PRSHPKT, qc); - goto out; - } - - ss->stop_sending.id = id; - ss->stop_sending.app_error_code = app_error_code; - - rs = qc_frm_alloc(QUIC_FT_RESET_STREAM); - if (!rs) { - TRACE_ERROR("failed to allocate quic_frame", QUIC_EV_CONN_PRSHPKT, qc); - qc_frm_free(qc, &ss); - goto out; - } - - rs->reset_stream.id = id; - rs->reset_stream.app_error_code = app_error_code; - rs->reset_stream.final_size = 0; - - LIST_APPEND(&qel->pktns->tx.frms, &ss->list); - LIST_APPEND(&qel->pktns->tx.frms, &rs->list); - ret = 1; - out: - TRACE_LEAVE(QUIC_EV_CONN_PRSHPKT, qc); - return ret; -} - /* Remove a quic-conn from its ha_thread_ctx list. If is true, * it will immediately be reinserted in the ha_thread_ctx quic_conns_clo list. */ @@ -1204,6 +1149,7 @@ struct quic_conn *qc_new_conn(void *target, qc->conn = conn; qc->qcc = NULL; qc->app_ops = NULL; + qc->strm_reject = NULL; qc->path = NULL; /* Keyupdate: required to safely call quic_tls_ku_free() from diff --git a/src/quic_rx.c b/src/quic_rx.c index d967a4e65..54a699162 100644 --- a/src/quic_rx.c +++ b/src/quic_rx.c @@ -14,7 +14,6 @@ #include -#include #include #include #include @@ -962,8 +961,8 @@ static int qc_parse_pkt_frms(struct quic_conn *qc, struct quic_rx_packet *pkt, } else { TRACE_DEVEL("No mux for new stream", QUIC_EV_CONN_PRSHPKT, qc); - if (qc->app_ops == &h3_ops) { - if (!qc_h3_request_reject(qc, strm_frm->id)) { + if (qc->strm_reject) { + if (!qc->strm_reject(&qc->ael->pktns->tx.frms, strm_frm->id)) { TRACE_ERROR("error on request rejection", QUIC_EV_CONN_PRSHPKT, qc); /* This packet will not be acknowledged */ goto err;