haproxy/include/haproxy/mux_quic-t.h
Amaury Denoyelle 805a070ab9 BUG/MINOR: mux-quic/h3: properly handle too low peer fctl initial stream
Previously, no check on peer flow-control was implemented prior to open
a local QUIC stream. This was a small problem for frontend
implementation, as in this case haproxy as a server never opens
bidirectional streams.

On frontend, the only stream opened by haproxy in this case is for
HTTP/3 control unidirectional data. If the peer uses an initial value
for max uni streams set to 0, it would violate its flow control, and the
peer will probably close the connection. Note however that RFC 9114
mandates that each peer defines minimal initial value so that at least
the control stream can be created.

This commit improves the situation of too low initial max uni streams
value. Now, on HTTP/3 layer initialization, haproxy preemptively checks
flow control limit on streams via a new function
qcc_fctl_avail_streams(). If credit is already expired due to a too
small initial value, haproxy preemptively closes the connection using
H3_ERR_GENERAL_PROTOCOL_ERROR. This behavior is better as haproxy is now
the initiator of the connection closure.

This should be backported up to 2.8.
2025-06-18 17:18:55 +02:00

307 lines
12 KiB
C

#ifndef _HAPROXY_MUX_QUIC_T_H
#define _HAPROXY_MUX_QUIC_T_H
#ifdef USE_QUIC
#ifndef USE_OPENSSL
#error "Must define USE_OPENSSL"
#endif
#include <import/ebtree-t.h>
#include <haproxy/buf-t.h>
#include <haproxy/connection-t.h>
#include <haproxy/htx-t.h>
#include <haproxy/list-t.h>
#include <haproxy/ncbuf-t.h>
#include <haproxy/quic_fctl-t.h>
#include <haproxy/quic_frame-t.h>
#include <haproxy/quic_pacing-t.h>
#include <haproxy/quic_stream-t.h>
#include <haproxy/quic_utils-t.h>
#include <haproxy/stconn-t.h>
#include <haproxy/task-t.h>
#include <haproxy/time-t.h>
/* Stream types */
enum qcs_type {
QCS_CLT_BIDI,
QCS_SRV_BIDI,
QCS_CLT_UNI,
QCS_SRV_UNI,
};
enum qcc_app_st {
QCC_APP_ST_NULL,
QCC_APP_ST_INIT,
QCC_APP_ST_SHUT,
} __attribute__((packed));
struct qcc {
struct connection *conn;
uint64_t nb_sc; /* number of attached stream connectors */
uint64_t nb_hreq; /* number of in-progress http requests */
uint32_t flags; /* QC_CF_* */
enum qcc_app_st app_st; /* application layer state */
int glitches; /* total number of glitches on this connection */
/* flow-control fields set by us enforced on our side. */
struct {
struct list frms; /* prepared frames related to flow-control */
uint64_t ms_bidi_init; /* max initial sub-ID of bidi stream allowed for the peer */
uint64_t ms_bidi; /* max sub-ID of bidi stream allowed for the peer */
uint64_t cl_bidi_r; /* total count of closed remote bidi stream since last MAX_STREAMS emission */
uint64_t ms_uni; /* max sub-ID of uni stream allowed for the peer */
uint64_t msd_bidi_l; /* initial max-stream-data on local bidi streams */
uint64_t msd_bidi_r; /* initial max-stream-data on remote bidi streams */
uint64_t msd_uni_r; /* initial max-stream-data on remote uni streams */
uint64_t md; /* current max-data allowed for the peer */
uint64_t md_init; /* initial max-data */
uint64_t offsets_recv; /* sum of offsets received */
uint64_t offsets_consume; /* sum of offsets consumed */
} lfctl;
/* flow-control fields set by the peer which we must respect. */
struct {
uint64_t ms_uni; /* max sub-ID of uni stream allowed by the peer */
uint64_t md; /* connection flow control limit updated on MAX_DATA frames reception */
uint64_t msd_bidi_l; /* initial max-stream-data from peer on local bidi streams */
uint64_t msd_bidi_r; /* initial max-stream-data from peer on remote bidi streams */
uint64_t msd_uni_l; /* initial max-stream-data from peer on local uni streams */
} rfctl;
struct {
struct quic_fctl fc; /* stream flow control applied on sending */
uint64_t buf_in_flight; /* sum of currently allocated Tx buffer sizes */
struct list frms; /* list of STREAM frames ready for sent */
struct quic_pacer pacer; /* engine used to pace emission */
int paced_sent_ctr; /* counter for when emission is interrupted due to pacing */
} tx;
uint64_t largest_bidi_r; /* largest remote bidi stream ID opened. */
uint64_t largest_uni_r; /* largest remote uni stream ID opened. */
uint64_t next_bidi_l; /* next stream ID to use for local bidi stream */
uint64_t next_uni_l; /* next stream ID to use for local uni stream */
struct eb_root streams_by_id; /* all active streams by their ID */
struct list recv_list; /* list of qcs for which demux can be resumed */
struct list send_list; /* list of qcs ready to send (STREAM, STOP_SENDING or RESET_STREAM emission) */
struct list fctl_list; /* list of sending qcs blocked on conn flow control */
struct list buf_wait_list; /* list of qcs blocked on stream desc buf */
struct list purg_list; /* list of qcs which can be purged */
struct wait_event wait_event; /* To be used if we're waiting for I/Os */
struct task *pacing_task; /* task used to wait when emission is interrupted due to pacing */
struct proxy *proxy;
/* haproxy timeout management */
struct task *task;
struct list opening_list; /* list of not already attached streams (http-request timeout) */
int timeout;
int shut_timeout;
int idle_start; /* base time for http-keep-alive timeout */
struct quic_err err; /* code for locally detected error */
const struct qcc_app_ops *app_ops;
void *ctx; /* Application layer context */
};
/* QUIC stream states
*
* On initialization a stream is put on idle state. It is opened as soon as
* data has been successfully sent or received on it.
*
* A bidirectional stream has two channels which can be closed separately. The
* local channel is closed when the STREAM frame with FIN or a RESET_STREAM has
* been emitted. The remote channel is closed as soon as all data from the peer
* has been received. The stream goes instantely to the close state once both
* channels are closed.
*
* A unidirectional stream has only one channel of communication. Thus, it does
* not use half closed states and transition directly from open to close state.
*/
enum qcs_state {
QC_SS_IDLE = 0, /* initial state */
QC_SS_OPEN, /* opened */
QC_SS_HLOC, /* half-closed local */
QC_SS_HREM, /* half-closed remote */
QC_SS_CLO, /* closed */
} __attribute__((packed));
/* STREAM receive buffer. Can handle out-of-order storage.
* Can be used as a tree node to allocate multiple entries ordered by offsets.
*/
struct qc_stream_rxbuf {
struct eb64_node off_node; /* base offset of current buffer, node for QCS rx.bufs */
struct ncbuf ncb; /* data storage with support for out of order offset */
uint64_t off_end; /* first offset directly outside of current buffer */
};
struct qcs {
struct qcc *qcc;
struct sedesc *sd;
uint32_t flags; /* QC_SF_* */
enum qcs_state st; /* QC_SS_* state */
void *ctx; /* app-ops context */
struct {
uint64_t offset; /* read offset */
uint64_t offset_max; /* maximum absolute offset received */
struct eb_root bufs; /* receive buffers tree ordered by offset */
struct buffer app_buf; /* receive buffer used by stconn layer */
uint64_t msd; /* current max-stream-data limit to enforce */
uint64_t msd_base; /* max-stream-data previous to latest update */
struct bdata_ctr data; /* data utilization counter. Note that <tot> is now used for now as accounting may be difficult with ncbuf. */
} rx;
struct {
struct quic_fctl fc; /* stream flow control applied on sending */
struct quic_frame *msd_frm; /* MAX_STREAM_DATA frame prepared */
} tx;
struct eb64_node by_id;
uint64_t id;
struct qc_stream_desc *stream;
struct list el_recv; /* element of qcc.recv_list */
struct list el_send; /* element of qcc.send_list */
struct list el_opening; /* element of qcc.opening_list */
struct list el_fctl; /* element of qcc.fctl_list */
struct list el_buf; /* element of qcc.buf_wait_list */
struct wait_event wait_event;
struct wait_event *subs;
uint64_t err; /* error code to transmit via RESET_STREAM */
int start; /* base timestamp for http-request timeout */
struct {
struct tot_time base; /* total QCS lifetime */
struct tot_time buf; /* stream to QCS send blocked on buffer */
struct tot_time fctl; /* stream to QCS send blocked on flow-control */
} timer;
};
/* Used as qcc_app_ops.close callback argument. */
enum qcc_app_ops_close_side {
QCC_APP_OPS_CLOSE_SIDE_RD, /* Read channel closed (RESET_STREAM received). */
QCC_APP_OPS_CLOSE_SIDE_WR /* Write channel closed (STOP_SENDING received). */
};
/* QUIC application layer operations */
struct qcc_app_ops {
/* Initialize <qcc> connection app context. */
int (*init)(struct qcc *qcc);
/* Finish connection initialization if prelude required. */
int (*finalize)(void *ctx);
/* Initialize <qcs> stream app context or leave it to NULL if rejected. */
int (*attach)(struct qcs *qcs, void *conn_ctx);
/* Convert received HTTP payload to HTX. Returns amount of decoded bytes from <b> or a negative error code. */
ssize_t (*rcv_buf)(struct qcs *qcs, struct buffer *b, int fin);
/* Convert HTX to HTTP payload for sending. */
size_t (*snd_buf)(struct qcs *qcs, struct buffer *b, size_t count);
/* Negotiate and commit fast-forward data from opposite MUX. */
size_t (*nego_ff)(struct qcs *qcs, size_t count);
size_t (*done_ff)(struct qcs *qcs);
/* Notify about <qcs> stream closure. */
int (*close)(struct qcs *qcs, enum qcc_app_ops_close_side side);
/* Free <qcs> stream app context. */
void (*detach)(struct qcs *qcs);
/* Perform graceful shutdown. */
void (*shutdown)(void *ctx);
/* Free connection app context. */
void (*release)(void *ctx);
/* Increment app counters on CONNECTION_CLOSE_APP reception. */
void (*inc_err_cnt)(void *ctx, int err_code);
/* Set QCC error code as suspicious activity has been detected. */
void (*report_susp)(void *ctx);
};
#endif /* USE_QUIC */
#define QC_CF_ERRL 0x00000001 /* fatal error detected locally, connection should be closed soon */
#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_ERR_CONN 0x00000020 /* fatal error reported by transport layer */
#define QC_CF_WAIT_HS 0x00000040 /* MUX init before QUIC handshake completed (0-RTT) */
/* This function is used to report flags in debugging tools. Please reflect
* below any single-bit flag addition above in the same order via the
* __APPEND_FLAG macro. The new end of the buffer is returned.
*/
static forceinline char *qcc_show_flags(char *buf, size_t len, const char *delim, uint flg)
{
#define _(f, ...) __APPEND_FLAG(buf, len, delim, flg, f, #f, __VA_ARGS__)
/* prologue */
_(0);
/* flags */
_(QC_CF_ERRL,
_(QC_CF_ERRL_DONE,
_(QC_CF_CONN_FULL,
_(QC_CF_ERR_CONN,
_(QC_CF_WAIT_HS)))));
/* epilogue */
_(~0U);
return buf;
#undef _
}
#define QC_SF_NONE 0x00000000
#define QC_SF_SIZE_KNOWN 0x00000001 /* last frame received for this stream */
#define QC_SF_FIN_STREAM 0x00000002 /* FIN bit must be set for last frame of the stream */
#define QC_SF_BLK_MROOM 0x00000004 /* app layer is blocked waiting for room in the qcs.tx.buf */
#define QC_SF_DETACH 0x00000008 /* sc is detached but there is remaining data to send */
#define QC_SF_TXBUB_OOB 0x00000010 /* stream reserved for metadata out-of-band transmission; txbuf allocation is unrestricted */
#define QC_SF_DEM_FULL 0x00000020 /* demux blocked on request channel buffer full */
#define QC_SF_READ_ABORTED 0x00000040 /* Rx closed using STOP_SENDING*/
#define QC_SF_TO_RESET 0x00000080 /* a RESET_STREAM must be sent */
#define QC_SF_HREQ_RECV 0x00000100 /* a full HTTP request has been received */
#define QC_SF_TO_STOP_SENDING 0x00000200 /* a STOP_SENDING must be sent */
#define QC_SF_UNKNOWN_PL_LENGTH 0x00000400 /* HTX EOM may be missing from the stream layer */
#define QC_SF_RECV_RESET 0x00000800 /* a RESET_STREAM was received */
/* This function is used to report flags in debugging tools. Please reflect
* below any single-bit flag addition above in the same order via the
* __APPEND_FLAG macro. The new end of the buffer is returned.
*/
static forceinline char *qcs_show_flags(char *buf, size_t len, const char *delim, uint flg)
{
#define _(f, ...) __APPEND_FLAG(buf, len, delim, flg, f, #f, __VA_ARGS__)
/* prologue */
_(0);
/* flags */
_(QC_SF_SIZE_KNOWN,
_(QC_SF_FIN_STREAM,
_(QC_SF_BLK_MROOM,
_(QC_SF_DETACH,
_(QC_SF_TXBUB_OOB,
_(QC_SF_DEM_FULL,
_(QC_SF_READ_ABORTED,
_(QC_SF_TO_RESET,
_(QC_SF_HREQ_RECV,
_(QC_SF_TO_STOP_SENDING,
_(QC_SF_UNKNOWN_PL_LENGTH,
_(QC_SF_RECV_RESET))))))))))));
/* epilogue */
_(~0U);
return buf;
#undef _
}
#endif /* _HAPROXY_MUX_QUIC_T_H */