From d0bd6a946b0df26f5e2e62565d7af6b6676c713f Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Tue, 21 Apr 2026 14:58:17 +0200 Subject: [PATCH] MEDIUM: mux-quic: extend shut to app proto layer Previously, shut callback was entirely implemented in QUIC mux layer. However, this operation depends on the above application protocol, as it may define its own closure procedure and error codes. This is the case notably with HTTP/3 specification. This patch defines a stream shut API between QUIC mux and application protocol layers via the new qcc_app_ops callback lclose(). The closure reason is specified via an enum argument. Application protcol can then perform the stream closure as intended. This patch is only an architecture adjustment but should not have any functional impact. Stream closure logic was moved identically from QUIC mux into h3 and h09 lclose callback. --- include/haproxy/mux_quic-t.h | 10 +++++++++- src/h3.c | 28 ++++++++++++++++++++++++++++ src/hq_interop.c | 15 +++++++++++++++ src/mux_quic.c | 25 ++++++------------------- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/include/haproxy/mux_quic-t.h b/include/haproxy/mux_quic-t.h index 02db4e8a5..6a0bfe6cb 100644 --- a/include/haproxy/mux_quic-t.h +++ b/include/haproxy/mux_quic-t.h @@ -214,6 +214,12 @@ enum qcc_app_ops_close_side { QCC_APP_OPS_CLOSE_SIDE_WR /* Write channel closed (STOP_SENDING received). */ }; +enum qcc_app_ops_lclose_mode { + QCC_APP_OPS_LCLO_MODE_NORMAL, + QCC_APP_OPS_LCLO_MODE_ABORT, + QCC_APP_OPS_LCLO_MODE_KILL_CONN, +}; + /* QUIC application layer operations */ struct qcc_app_ops { const char *alpn; @@ -236,8 +242,10 @@ struct qcc_app_ops { size_t (*nego_ff)(struct qcs *qcs, size_t count); size_t (*done_ff)(struct qcs *qcs); - /* Notify about stream closure. */ + /* Notify about stream remote closure. */ int (*close)(struct qcs *qcs, enum qcc_app_ops_close_side side); + /* Notify about stream upper layer closure. */ + void (*lclose)(struct qcs *qcs, enum qcc_app_ops_lclose_mode mode); /* Free stream app context. */ void (*detach)(struct qcs *qcs); diff --git a/src/h3.c b/src/h3.c index a899370ce..8d633bfc7 100644 --- a/src/h3.c +++ b/src/h3.c @@ -3095,6 +3095,33 @@ static int h3_close(struct qcs *qcs, enum qcc_app_ops_close_side side) return 0; } +static void h3_lclose(struct qcs *qcs, enum qcc_app_ops_lclose_mode mode) +{ + TRACE_ENTER(H3_EV_H3S_END, qcs->qcc->conn, qcs); + + switch (mode) { + case QCC_APP_OPS_LCLO_MODE_NORMAL: + /* Close stream with FIN. This can only be performed if at + * least HEADERS frame was emitted, or else some clients close + * the connection with H3_FRAME_UNEXPECTED. + */ + if (qcs->tx.fc.off_soft) { + qcs->flags |= QC_SF_FIN_STREAM; + qcc_send_stream(qcs, 0, 0); + } + else { + qcc_reset_stream(qcs, 0, se_tevt_type_shutw); + } + break; + + default: + qcc_reset_stream(qcs, 0, 0); + break; + } + + TRACE_LEAVE(H3_EV_H3S_END, qcs->qcc->conn, qcs); +} + /* Allocates HTTP/3 stream context relative to . If the operation cannot * be performed, an error is returned and context is unchanged. * @@ -3500,6 +3527,7 @@ const struct qcc_app_ops h3_ops = { .nego_ff = h3_nego_ff, .done_ff = h3_done_ff, .close = h3_close, + .lclose = h3_lclose, .detach = h3_detach, .shutdown = h3_shutdown, .inc_err_cnt = h3_stats_inc_err_cnt, diff --git a/src/hq_interop.c b/src/hq_interop.c index 8268e62a7..35a0e3358 100644 --- a/src/hq_interop.c +++ b/src/hq_interop.c @@ -323,6 +323,20 @@ static int hq_interop_attach(struct qcs *qcs, void *conn_ctx) return 0; } +static void hq_interop_lclose(struct qcs *qcs, enum qcc_app_ops_lclose_mode mode) +{ + switch (mode) { + case QCC_APP_OPS_LCLO_MODE_NORMAL: + qcs->flags |= QC_SF_FIN_STREAM; + qcc_send_stream(qcs, 0, 0); + break; + + default: + qcc_reset_stream(qcs, 0, 0); + break; + } +} + const struct qcc_app_ops hq_interop_ops = { .alpn = "hq-interop", @@ -331,4 +345,5 @@ const struct qcc_app_ops hq_interop_ops = { .nego_ff = hq_interop_nego_ff, .done_ff = hq_interop_done_ff, .attach = hq_interop_attach, + .lclose = hq_interop_lclose, }; diff --git a/src/mux_quic.c b/src/mux_quic.c index 1bf13dc0f..4208029aa 100644 --- a/src/mux_quic.c +++ b/src/mux_quic.c @@ -4591,25 +4591,12 @@ static void qmux_strm_shut(struct stconn *sc, unsigned int mode, struct se_abort /* Early closure reported if QC_SF_FIN_STREAM not yet set. */ if (!qcs_is_close_local(qcs) && !(qcs->flags & (QC_SF_FIN_STREAM|QC_SF_TO_RESET))) { - - /* Close stream with FIN if length unknown and some data are - * ready to be/already transmitted. - * TODO select closure method on app proto layer - */ - if (qcs->flags & QC_SF_UNKNOWN_PL_LENGTH && - qcs->tx.fc.off_soft) { - if (!(qcc->flags & (QC_CF_ERR_CONN|QC_CF_ERRL))) { - TRACE_STATE("set FIN STREAM", - QMUX_EV_STRM_SHUT, qcc->conn, qcs); - qcs->flags |= QC_SF_FIN_STREAM; - qcc_send_stream(qcs, 0, 0); - } - } - else { - /* RESET_STREAM necessary. */ - qcc_reset_stream(qcs, 0, 0); - } - + if (qcs->flags & QC_SF_UNKNOWN_PL_LENGTH) + qcc->app_ops->lclose(qcs, QCC_APP_OPS_LCLO_MODE_NORMAL); + else if (se_fl_test(qcs->sd, SE_FL_KILL_CONN)) + qcc->app_ops->lclose(qcs, QCC_APP_OPS_LCLO_MODE_KILL_CONN); + else + qcc->app_ops->lclose(qcs, QCC_APP_OPS_LCLO_MODE_ABORT); tasklet_wakeup(qcc->wait_event.tasklet); }