DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
quic_conn.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file quic_conn.h
6 * @brief Stateful QUIC v1 server connection engine (RFC 9000 / RFC 9001).
7 *
8 * One QuicConn drives a single QUIC connection: it parses each inbound UDP datagram into its
9 * coalesced packets, removes header + AEAD protection at the right encryption level (Initial keys
10 * from the client's Destination Connection ID; Handshake and 1-RTT keys from the TLS handshake it
11 * runs via quic_tls), dispatches the frames, reassembles the CRYPTO stream to advance the handshake,
12 * tracks packet numbers to generate ACKs, and coalesces the outbound Initial / Handshake / 1-RTT
13 * packets back into datagrams. Application streams are surfaced to HTTP/3 (h3_conn) through a small
14 * callback + send API, so the transport engine has no HTTP dependency.
15 *
16 * It is transport-free (no lwIP): quic_conn_recv() takes a received datagram and quic_conn_send()
17 * pulls the next datagram to transmit, so the engine is host-testable by shuttling byte buffers
18 * between a server QuicConn and a client written in the test. quic_server wires it to det_udp.
19 *
20 * Scope (a faithful minimal server): QUIC v1 only, no Retry / 0-RTT / key update / connection
21 * migration / connection-ID rotation, in-order CRYPTO and stream reassembly, and single-range ACKs.
22 * Loss recovery is a Probe Timeout (RFC 9002): quic_conn_on_timeout() retransmits the outstanding
23 * ack-eliciting flight (handshake CRYPTO and 1-RTT streams) with exponential backoff, reset on
24 * acknowledged progress. Fatal handshake / frame errors emit a transport CONNECTION_CLOSE (RFC 9000
25 * sec 19.19) instead of timing out. Small packets are padded to the header-protection minimum
26 * (RFC 9001 sec 5.4.2). Fixed storage, no heap.
27 *
28 * @author Douglas Quigg (dstroy0)
29 * @date 2026
30 */
31
32#ifndef DETERMINISTICESPASYNCWEBSERVER_QUIC_CONN_H
33#define DETERMINISTICESPASYNCWEBSERVER_QUIC_CONN_H
34
35#include "ServerConfig.h"
36
37#if DETWS_ENABLE_HTTP3
38
41#include <stddef.h>
42#include <stdint.h>
43
44#ifndef DETWS_QUIC_MAX_DATAGRAM
45#define DETWS_QUIC_MAX_DATAGRAM 1350 ///< largest UDP payload we send/accept (conservative < 1500 MTU)
46#endif
47#ifndef DETWS_QUIC_CRYPTO_RX
48#define DETWS_QUIC_CRYPTO_RX 2048 ///< per-level inbound CRYPTO reassembly window (ClientHello, Finished)
49#endif
50#ifndef DETWS_QUIC_MAX_STREAMS
51#define DETWS_QUIC_MAX_STREAMS DETWS_H3_MAX_STREAMS ///< tracked streams (request + control/QPACK)
52#endif
53#ifndef DETWS_QUIC_STREAM_RX
54#define DETWS_QUIC_STREAM_RX 2048 ///< per-stream inbound reassembly buffer
55#endif
56#ifndef DETWS_QUIC_STREAM_TX
57#define DETWS_QUIC_STREAM_TX 2048 ///< per-stream outbound buffer (drained into STREAM frames)
58#endif
59#ifndef DETWS_QUIC_PTO_MS
60#define DETWS_QUIC_PTO_MS 1000 ///< base Probe Timeout for retransmitting the handshake flight (RFC 9002)
61#endif
62
63/** @brief Per-connection stream state (client-initiated + server-initiated). */
64struct QuicStream
65{
66 uint64_t id; ///< stream id (UINT64_MAX = free slot)
67 uint64_t rx_off; ///< next in-order byte offset expected
68 uint64_t tx_off; ///< next send offset
69 bool rx_fin; ///< a FIN was received (final size known)
70 bool tx_fin; ///< a FIN should be sent after the buffered tx bytes
71 bool tx_fin_sent; ///< the FIN has been sent
72 uint8_t rx[DETWS_QUIC_STREAM_RX];
73 size_t rx_have; ///< contiguous bytes buffered in rx (from offset rx_off - rx_have)
74 uint8_t tx[DETWS_QUIC_STREAM_TX];
75 size_t tx_have; ///< bytes buffered to send
76 size_t tx_sent; ///< bytes of tx already put on the wire
77};
78
79struct QuicConn;
80
81/** @brief HTTP/3 (or test) hooks the engine drives. All nullable. */
82struct QuicConnCallbacks
83{
84 /** @brief In-order stream bytes arrived on @p stream_id (@p fin marks the final bytes). */
85 void (*on_stream_data)(void *app, QuicConn *qc, uint64_t stream_id, const uint8_t *data, size_t len, bool fin);
86 /** @brief The handshake completed (client Finished verified); 1-RTT is open. */
87 void (*on_handshake_done)(void *app, QuicConn *qc);
88 void *app; ///< opaque, passed back to the callbacks
89};
90
91/** @brief One packet-number space (Initial / Handshake / Application). */
92struct QuicPnSpace
93{
94 uint64_t next_pn; ///< next packet number to send in this space
95 int64_t largest_acked; ///< largest of our PNs the peer has acknowledged (-1 = none)
96 int64_t last_ae_pn; ///< PN of the last ack-eliciting packet we sent here (-1 = none); loss recovery
97 uint64_t largest_rx; ///< largest PN received in this space
98 bool have_rx; ///< at least one packet received
99 bool ack_eliciting_rx; ///< an ack-eliciting packet is unacknowledged (we owe an ACK)
100 bool discarded; ///< this space's keys have been dropped (nothing more sent/received)
101 uint64_t crypto_rx_off; ///< in-order CRYPTO bytes already delivered to quic_tls
102 uint8_t crypto_rx[DETWS_QUIC_CRYPTO_RX];
103 size_t crypto_rx_have; ///< contiguous CRYPTO bytes buffered at crypto_rx_off
104 uint64_t crypto_tx_off; ///< CRYPTO flight bytes already sent from this level
105};
106
107/** @brief One QUIC connection's engine state (fixed storage, no heap). */
108struct QuicConn
109{
110 uint8_t scid[QUIC_MAX_CID_LEN]; ///< our connection ID (peer's DCID toward us)
111 uint8_t scid_len;
112 uint8_t dcid[QUIC_MAX_CID_LEN]; ///< peer's connection ID (our DCID toward the peer)
113 uint8_t dcid_len;
114 uint8_t odcid[QUIC_MAX_CID_LEN]; ///< client's original DCID (Initial keys + transport param)
115 uint8_t odcid_len;
116
117 QuicInitialSecrets initial; ///< Initial keys derived from odcid
118 QuicTls tls; ///< the TLS 1.3 handshake
119
120 QuicPnSpace space[3]; ///< indexed by QUIC_ENC_*
121
122 QuicStream streams[DETWS_QUIC_MAX_STREAMS];
123
124 QuicConnCallbacks cb;
125
126 bool handshake_done_queued; ///< a HANDSHAKE_DONE frame still needs sending
127 bool handshake_done_sent;
128 bool closed; ///< a CONNECTION_CLOSE has been sent or received
129 bool draining; ///< peer closed; we only drain
130 uint64_t recv_bytes; ///< total bytes received (anti-amplification budget)
131 uint64_t sent_bytes; ///< total bytes sent before address validation
132 bool address_validated; ///< handshake-complete or received enough to lift the 3x limit
133
134 bool pto_armed; ///< a Probe Timeout is running for the outstanding handshake flight
135 uint8_t pto_count; ///< consecutive PTO expirations (exponential backoff exponent)
136 uint32_t pto_deadline_ms; ///< when the PTO fires (caller's monotonic ms; valid when pto_armed)
137
138 bool close_queued; ///< a transport CONNECTION_CLOSE is owed to the peer (fatal error hit)
139 bool close_sent; ///< the CONNECTION_CLOSE has been put on the wire
140 uint64_t close_error; ///< transport error code to report (RFC 9000 sec 20.1)
141 uint64_t close_frame_type; ///< frame type that triggered it (0 when not frame-specific)
142 uint8_t close_level; ///< encryption level to send the close at (the peer holds those keys)
143};
144
145/**
146 * @brief Initialize a server connection from the client's first Initial packet.
147 *
148 * @param cfg TLS server config (cert / key / transport params); the engine fills the
149 * original_destination_connection_id and initial_source_connection_id parameters.
150 * @param odcid the Destination Connection ID from the client's first Initial (Initial keys).
151 * @param odcid_len its length.
152 * @param peer_scid the client's Source Connection ID (our DCID toward the client).
153 * @param peer_scid_len its length.
154 * @param our_scid the connection ID we choose for ourselves (peer's DCID toward us).
155 * @param our_scid_len its length.
156 * @param cb stream / handshake callbacks (may be all NULL).
157 */
158void quic_conn_init(QuicConn *qc, const QuicTlsConfig *cfg, const uint8_t *odcid, uint8_t odcid_len,
159 const uint8_t *peer_scid, uint8_t peer_scid_len, const uint8_t *our_scid, uint8_t our_scid_len,
160 const QuicConnCallbacks *cb);
161
162/**
163 * @brief Process one received UDP datagram (one or more coalesced QUIC packets).
164 * @return true if the datagram was processed (even partially); false if it was undecryptable /
165 * malformed enough to drop entirely. Frames drive the handshake, ACK state, and stream callbacks.
166 */
167bool quic_conn_recv(QuicConn *qc, const uint8_t *datagram, size_t len);
168
169/**
170 * @brief Build the next outbound datagram (coalesced Initial / Handshake / 1-RTT packets).
171 * @return its length, or 0 when there is nothing to send right now. Call repeatedly until it
172 * returns 0. Honors the pre-validation 3x anti-amplification limit.
173 */
174size_t quic_conn_send(QuicConn *qc, uint8_t *out, size_t cap);
175
176/**
177 * @brief Drive loss recovery: if the server's handshake CRYPTO flight is outstanding (built but not
178 * yet acknowledged) and the Probe Timeout has elapsed, mark the flight for retransmission (RFC 9002)
179 * so the next quic_conn_send() re-sends it, and back the timer off exponentially. @p now_ms is the
180 * caller's monotonic millisecond clock (quic_conn stays clock-free). A no-op once the flight is
181 * acknowledged or the handshake completes. Call once per poll before quic_conn_send().
182 */
183void quic_conn_on_timeout(QuicConn *qc, uint32_t now_ms);
184
185/**
186 * @brief Queue @p len bytes (with optional @p fin) to send on @p stream_id.
187 * @return bytes accepted into the stream's send buffer (may be < len if it is full).
188 */
189size_t quic_conn_stream_send(QuicConn *qc, uint64_t stream_id, const uint8_t *data, size_t len, bool fin);
190
191/**
192 * @brief Initiate an immediate close: queue a transport CONNECTION_CLOSE (RFC 9000 sec 19.19) with
193 * @p error_code so the next quic_conn_send() reports the error to the peer instead of leaving it to
194 * time out. A no-op if the connection is already closing. The engine also calls this itself on a fatal
195 * handshake / frame error (CRYPTO_ERROR / FRAME_ENCODING_ERROR).
196 */
197void quic_conn_close(QuicConn *qc, uint64_t error_code);
198
199/** @brief True once the TLS handshake has completed (client Finished verified). */
200bool quic_conn_established(const QuicConn *qc);
201
202/** @brief True if the connection is closed or draining. */
203bool quic_conn_is_closed(const QuicConn *qc);
204
205#endif // DETWS_ENABLE_HTTP3
206#endif // DETERMINISTICESPASYNCWEBSERVER_QUIC_CONN_H
User-facing configuration for DeterministicESPAsyncWebServer.
QUIC packet protection: Initial secrets, AEAD payload protection, header protection,...
TLS 1.3 server handshake state machine for QUIC (RFC 9001 / RFC 8446).