]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: h3: prevent new streams on GOAWAY reception
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Wed, 15 Apr 2026 14:44:59 +0000 (16:44 +0200)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Fri, 17 Apr 2026 11:28:17 +0000 (13:28 +0200)
Implement the reception of a HTTP/3 GOAWAY frame. This is performed via
the new function h3_parse_goaway_frm(). The advertised ID is stored in
new <id_shut_r> h3c member. It serves to ensure that a bigger ID is not
advertised when receiving multiple GOAWAY frames.

GOAWAY frame reception is only really useful on the backend side for
haproxy. When this occurs, h3c is now flagged with H3_CF_GOAWAY_RECV.
Also, QCC is also updated with new flag QC_CF_CONN_SHUT. This flag
indicates that no new stream may be opened on the connection. Callback
avail_streams() is thus edited to report 0 in this case.

include/haproxy/mux_quic-t.h
include/haproxy/mux_quic.h
src/h3.c
src/mux_quic.c

index a8c2f5c83d3fa276731c376d49c757bcf8be869f..b384624f7de5babd0024579becf7a09abc148528 100644 (file)
@@ -259,7 +259,7 @@ struct qcc_app_ops {
 #define QC_CF_ERRL_DONE 0x00000002 /* local error properly handled, connection can be released */
 #define QC_CF_IS_BACK   0x00000004 /* backend side */
 #define QC_CF_CONN_FULL 0x00000008 /* no stream buffers available on connection */
-/* unused 0x00000010 */
+#define QC_CF_CONN_SHUT 0x00000010 /* peer has initiate app layer shutdown - no new stream should be opened locally */
 #define QC_CF_ERR_CONN  0x00000020 /* fatal error reported by transport layer */
 #define QC_CF_WAIT_HS   0x00000040 /* MUX init before QUIC handshake completed (0-RTT) */
 
index 0ab083018e51720c421c0160c8166bf29b291f68..05d1b43347c9bc1ffa3c7af0cb59bb0515dc0a3c 100644 (file)
@@ -43,6 +43,7 @@ int qcc_stream_can_send(const struct qcs *qcs);
 void qcc_reset_stream(struct qcs *qcs, int err);
 void qcc_send_stream(struct qcs *qcs, int urg, int count);
 void qcc_abort_stream_read(struct qcs *qcs);
+void qcc_update_shut_id(struct qcc *qcc, uint64_t val);
 int qcc_recv(struct qcc *qcc, uint64_t id, uint64_t len, uint64_t offset,
              char fin, char *data);
 int qcc_recv_max_data(struct qcc *qcc, uint64_t max);
index 77f8bb91ef3ccebfbbc963a3aa48d7d2197082ad..8a836b7db38ac19053a77eff9051d18da99b053b 100644 (file)
--- a/src/h3.c
+++ b/src/h3.c
@@ -123,6 +123,7 @@ INITCALL1(STG_REGISTER, trace_register_source, TRACE_SOURCE);
 #define H3_CF_UNI_QPACK_DEC_SET 0x00000008  /* Remote QPACK decoder stream opened */
 #define H3_CF_UNI_QPACK_ENC_SET 0x00000010  /* Remote QPACK encoder stream opened */
 #define H3_CF_GOAWAY_SENT       0x00000020  /* GOAWAY sent on local control stream */
+#define H3_CF_GOAWAY_RECV       0x00000040  /* GOAWAY received from the peer */
 
 /* Default settings */
 static uint64_t h3_settings_qpack_max_table_capacity = 0;
@@ -141,6 +142,7 @@ struct h3c {
        uint64_t max_field_section_size;
 
        uint64_t id_shut_l; /* GOAWAY ID locally emitted */
+       uint64_t id_shut_r; /* GOAWAY ID emitted by the peer */
 
        struct buffer_wait buf_wait; /* wait list for buffer allocations */
        /* Stats counters */
@@ -1681,6 +1683,43 @@ static ssize_t h3_parse_settings_frm(struct h3c *h3c, const struct buffer *buf,
        return ret;
 }
 
+static ssize_t h3_parse_goaway_frm(struct h3c *h3c, const struct buffer *buf,
+                                   size_t len)
+{
+       struct buffer b;
+       uint64_t id;
+       size_t ret = 0;
+
+       TRACE_ENTER(H3_EV_RX_FRAME, h3c->qcc->conn);
+
+       b = b_make(b_orig(buf), b_size(buf), b_head_ofs(buf), len);
+       if (!b_quic_dec_int(&id, &b, &ret)) {
+               h3c->err = H3_ERR_FRAME_ERROR;
+               qcc_report_glitch(h3c->qcc, 1);
+               return -1;
+       }
+
+       if ((h3c->flags & H3_CF_GOAWAY_RECV) && id > h3c->id_shut_r) {
+               h3c->err = H3_ERR_ID_ERROR;
+               qcc_report_glitch(h3c->qcc, 1);
+               return -1;
+       }
+
+       h3c->flags |= H3_CF_GOAWAY_RECV;
+       h3c->id_shut_r = id;
+
+       /* RFC 9114 5.2. Connection Shutdown
+        *
+        * Endpoints MUST NOT initiate new requests or promise new pushes on the
+        * connection after receipt of a GOAWAY frame from the peer. Clients MAY
+        * establish a new connection to send additional requests.
+        */
+       h3c->qcc->flags |= QC_CF_CONN_SHUT;
+
+       TRACE_LEAVE(H3_EV_RX_FRAME, h3c->qcc->conn);
+       return ret;
+}
+
 /* Transcode HTTP/3 payload received in buffer <b> to HTX data for stream
  * <qcs>. If <fin> is set, it indicates that no more data will arrive after.
  *
@@ -1883,10 +1922,17 @@ static ssize_t h3_rcv_buf(struct qcs *qcs, struct buffer *b, int fin)
                                h3s->st_req = H3S_ST_REQ_TRAILERS;
                        }
                        break;
+               case H3_FT_GOAWAY:
+                       ret = h3_parse_goaway_frm(qcs->qcc->ctx, b, flen);
+                       if (ret < 0) {
+                               TRACE_ERROR("error on SETTINGS parsing", H3_EV_RX_FRAME, qcs->qcc->conn, qcs);
+                               qcc_set_error(qcs->qcc, h3c->err, 1);
+                               goto err;
+                       }
+                       break;
                case H3_FT_CANCEL_PUSH:
                case H3_FT_PUSH_PROMISE:
                case H3_FT_MAX_PUSH_ID:
-               case H3_FT_GOAWAY:
                        /* Not supported */
                        ret = flen;
                        break;
@@ -3201,6 +3247,7 @@ static int h3_init(struct qcc *qcc)
        h3c->err = 0;
        h3c->flags = 0;
        h3c->id_shut_l = 0;
+       h3c->id_shut_r = 0;
 
        qcc->ctx = h3c;
        h3c->prx_counters = qc_counters(qcc->conn->target, &h3_stats_module);
index 0bd9327f08628e3b1bafa4f693da7c0eb2c024b4..6066a7b16036401c9ffcf0417bd656f2ede10618 100644 (file)
@@ -3214,6 +3214,10 @@ static int qmux_avail_streams(struct connection *conn)
        struct qcc *qcc = conn->ctx;
        int ret, max_reuse = 0;
 
+       /* Shutdown initiated by the peer - in HTTP/3 this corresponds to a GOAWAY frame received. */
+       if (qcc->flags & QC_CF_CONN_SHUT)
+               return 0;
+
        ret = qcc_fctl_avail_streams(qcc, 1);
 
        if (srv->max_reuse >= 0) {