ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
quic_conn.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - 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 connection: each inbound UDP datagram is parsed into its coalesced packets, header and AEAD
9 * protection removed at the right encryption level (Initial keys from the client's Destination
10 * Connection ID, Handshake and 1-RTT keys from the TLS handshake this runs), the frames dispatched,
11 * the CRYPTO stream reassembled to advance the handshake, packet numbers tracked to generate ACKs,
12 * and the outbound Initial / Handshake / 1-RTT packets coalesced back into datagrams. Application
13 * streams surface through the callbacks below, so the transport engine has no HTTP dependency.
14 *
15 * Transport-free (no lwIP): @ref QuicConnNs::recv takes a received datagram and @ref QuicConnNs::send
16 * pulls the next one to transmit, so the engine is host-testable by shuttling byte buffers between a
17 * server connection and a client written in the test. protocore_quic_server wires it to protocore_udp.
18 *
19 * Scope (a faithful minimal server): QUIC v1 only, no Retry / 0-RTT / key update / connection
20 * migration / connection-ID rotation, in-order CRYPTO and stream reassembly, and single-range ACKs.
21 * Loss recovery is a Probe Timeout (RFC 9002): @ref QuicConnNs::on_timeout retransmits the
22 * outstanding ack-eliciting flight with exponential backoff, reset on acknowledged progress. Fatal
23 * handshake / frame errors emit a transport CONNECTION_CLOSE (RFC 9000 sec 19.19) instead of timing
24 * out. Small packets are padded to the header-protection minimum (RFC 9001 sec 5.4.2). Fixed
25 * storage, no heap.
26 *
27 * @author Douglas Quigg (dstroy0)
28 * @date 2026
29 */
30
31#ifndef PROTOCORE_QUIC_CONN_H
32#define PROTOCORE_QUIC_CONN_H
33
34#include "protocore_config.h" // the entry point: the enable gate below, and the widths
35
36#if PROTOCORE_ENABLE_HTTP3
37
39
40#ifndef PROTOCORE_QUIC_MAX_DATAGRAM
41#endif
42#ifndef PROTOCORE_QUIC_STREAM_RX
43#define PROTOCORE_QUIC_STREAM_RX 2048 ///< largest run of new in-order stream bytes delivered in one call
44#endif
45#ifndef PROTOCORE_QUIC_PTO_MS
46#define PROTOCORE_QUIC_PTO_MS 1000 ///< base Probe Timeout for retransmitting the handshake flight (RFC 9002)
47#endif
48
49// A connection runs out of two spans, both stated in protocore_config.h, which sums each into its
50// own arena: PROTOCORE_QUIC_CONN_CTX_BORROW secure bytes for the context, because the TLS handshake
51// it holds is key material, and PROTOCORE_QUIC_CONN_BORROW plaintext bytes for what it owes each
52// stream and the CRYPTO window per packet-number space. A caller takes both once and binds them.
53// How each is carved is this module's and is never named here.
54
55/** @brief The TLS server configuration a connection is initialised against; see quic_tls.h. */
56struct QuicTlsConfig;
57
58/** @brief The two spans one connection runs out of. */
59typedef struct
60{
61 uint8_t *b; ///< PROTOCORE_QUIC_CONN_BORROW plaintext bytes; stream TX and CRYPTO RX
62} QuicConnBind;
63
64/** @brief HTTP/3 (or test) hooks the engine drives. All nullable. */
65typedef struct
66{
67 /** @brief In-order stream bytes arrived on @p stream_id (@p fin marks the final bytes). */
68 void (*on_stream_data)(void *app, uint8_t *qc, uint64_t stream_id, const uint8_t *data, size_t len, proto_bool fin);
69 /** @brief The handshake completed (client Finished verified); 1-RTT is open. */
70 void (*on_handshake_done)(void *app, uint8_t *qc);
71 void *app; ///< opaque, passed back to the callbacks
72} QuicConnCallbacks;
73
74/** @brief The client's first Initial packet, as a connection is opened from it. */
75typedef struct
76{
77 const struct QuicTlsConfig *cfg; ///< cert / key / transport params; the engine fills the two connection ids
78 const uint8_t *odcid; ///< the Destination Connection ID from that Initial (Initial keys)
79 uint8_t odcid_len; ///< its length
80 const uint8_t *peer_scid; ///< the client's Source Connection ID (our DCID toward it)
81 uint8_t peer_scid_len; ///< its length
82 const uint8_t *our_scid; ///< the connection ID we choose for ourselves (its DCID toward us)
83 uint8_t our_scid_len; ///< its length
84} QuicConnInitArgs;
85
86/** @brief One received datagram (one or more coalesced QUIC packets). */
87typedef struct
88{
89 const uint8_t *datagram; ///< the bytes as they arrived
90 size_t len; ///< how many
91} QuicConnRecvArgs;
92
93/** @brief Where the next outbound datagram lands. */
94typedef struct
95{
96 uint8_t *out; ///< the buffer
97 size_t cap; ///< its capacity
98} QuicConnSendArgs;
99
100/** @brief The caller's monotonic clock, read for loss recovery. */
101typedef struct
102{
103 uint32_t now_ms; ///< milliseconds; this engine keeps no clock of its own
104} QuicConnTimeoutArgs;
105
106/** @brief Bytes queued to send on one stream. */
107typedef struct
108{
109 uint64_t stream_id; ///< the stream
110 const uint8_t *data; ///< the bytes
111 size_t len; ///< how many
112 proto_bool fin; ///< finish the stream after these bytes
113} QuicConnStreamSendArgs;
114
115/** @brief A received Destination Connection ID, asked of a connection. */
116typedef struct
117{
118 const uint8_t *dcid; ///< the DCID the datagram carries
119 uint8_t dcid_len; ///< its length; a short header's is the server's own SCID length
120} QuicConnOwnsArgs;
121
122/** @brief The error a close reports. */
123typedef struct
124{
125 uint64_t error_code; ///< RFC 9000 sec 20.1, or the application's own space for close_app
126} QuicConnCloseArgs;
127
128/**
129 * @brief QUIC v1 server connection (RFC 9000 / RFC 9001).
130 *
131 * A caller binds the two spans once, sets the members a call takes, invokes it through ::QuicConn,
132 * and reads the outcome off the same handle.
133 *
134 * QuicConn.init(ctx_span;
135 * QuicConn.bind.b = byte_span;
136 * QuicConn.init_args.cfg = cfg;
137 * QuicConn.init(QuicConn.internal);
138 * QuicConn.recv_args.datagram = pkt;
139 * QuicConn.recv_args.len = pkt_len;
140 * QuicConn.recv(QuicConn.internal);
141 *
142 * @var QuicConnNs::bind the two spans one connection runs out of
143 * @var QuicConnNs::cb the hooks the engine drives
144 * @var QuicConnNs::init_args the client's first Initial, as a connection is opened from it
145 * @var QuicConnNs::recv_args one received datagram
146 * @var QuicConnNs::send_args where the next outbound datagram lands
147 * @var QuicConnNs::timeout_args the caller's monotonic clock
148 * @var QuicConnNs::stream_send_args bytes queued to send on one stream
149 * @var QuicConnNs::close_args the error a close reports
150 * @var QuicConnNs::ok a call's true/false outcome
151 * @var QuicConnNs::n bytes the last send or stream_send produced
152 * @var QuicConnNs::established whether the handshake has completed (client Finished verified)
153 * @var QuicConnNs::closed whether the connection is closed or draining
154 * @var QuicConnNs::init open a server connection from the client's first Initial
155 * @var QuicConnNs::callbacks install @ref QuicConnNs::cb on the bound connection
156 * @var QuicConnNs::recv process one datagram: frames drive the handshake, ACKs and streams
157 * @var QuicConnNs::send build the next outbound datagram; @ref QuicConnNs::n is 0 when idle
158 * @var QuicConnNs::on_timeout drive loss recovery; call once per poll before @ref QuicConnNs::send
159 * @var QuicConnNs::stream_send queue bytes on a stream
160 * @var QuicConnNs::owns whether this connection answers to a received DCID
161 * @var QuicConnNs::owns_args a received Destination Connection ID, asked of a connection
162 * @var QuicConnNs::close queue a transport CONNECTION_CLOSE (RFC 9000 sec 19.19)
163 * @var QuicConnNs::close_app queue the application variant (0x1d), error code in the app's space
164 * @var QuicConnNs::is_established read the handshake state into @ref QuicConnNs::established
165 * @var QuicConnNs::is_closed read the close state into @ref QuicConnNs::closed
166 *
167 * @ref QuicConnNs::send is called repeatedly until @ref QuicConnNs::n is 0, and honors the
168 * pre-validation 3x anti-amplification limit.
169 *
170 * The bound spans are the CALLER's, at addresses it knows. The caller releases them, and each pool
171 * wipes on release; this module neither takes them, holds them past the connection, nor releases
172 * them. The context span IS the connection, so two connections are two spans and never collide.
173 *
174 * No storage member: a caller binds, sets operands and reads @ref QuicConnNs::ok, and that is all
175 * the surface there is.
176 */
177typedef struct
178{
179 QuicConnBind bind;
180 QuicConnCallbacks cb;
181 QuicConnInitArgs init_args;
182 QuicConnRecvArgs recv_args;
183 QuicConnSendArgs send_args;
184 QuicConnTimeoutArgs timeout_args;
185 QuicConnStreamSendArgs stream_send_args;
186 QuicConnOwnsArgs owns_args;
187 QuicConnCloseArgs close_args;
188 proto_bool ok;
189 size_t n;
190 proto_bool established;
191 proto_bool closed;
192} QuicConnVars;
193
194/** @brief The operands and the outcome. */
195extern QuicConnVars QuicConnV;
196
197/** @brief The entries. */
198typedef struct
199{
200 void (*const init)(uint8_t *work);
201 void (*const callbacks)(uint8_t *work);
202 void (*const recv)(uint8_t *work);
203 void (*const send)(uint8_t *work);
204 void (*const on_timeout)(uint8_t *work);
205 void (*const stream_send)(uint8_t *work);
206 void (*const owns)(uint8_t *work);
207 void (*const close)(uint8_t *work);
208 void (*const close_app)(uint8_t *work);
209 void (*const is_established)(uint8_t *work);
210 void (*const is_closed)(uint8_t *work);
211} QuicConnNs;
212
213// What the table binds, defined once in the .c and taking one parameter each: everything
214// else an entry needs is an operand in QuicConnV or a region of the borrow at a fixed offset.
215void protocore_quic_conn_init(uint8_t *work);
216void protocore_quic_conn_callbacks(uint8_t *work);
217void protocore_quic_conn_recv(uint8_t *work);
218void protocore_quic_conn_send(uint8_t *work);
219void protocore_quic_conn_on_timeout(uint8_t *work);
220void protocore_quic_conn_stream_send(uint8_t *work);
221void protocore_quic_conn_owns(uint8_t *work);
222void protocore_quic_conn_close(uint8_t *work);
223void protocore_quic_conn_close_app(uint8_t *work);
224void protocore_quic_conn_is_established(uint8_t *work);
225void protocore_quic_conn_is_closed(uint8_t *work);
226
227// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
228// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
229// `QuicConn.init(work)` resolves to a named function and becomes a DIRECT call. An extern table
230// leaves the call indirect and the symbol live at every level, -O2 -flto included.
231static const QuicConnNs QuicConn __attribute__((unused)) = {
232 .init = protocore_quic_conn_init,
233 .callbacks = protocore_quic_conn_callbacks,
234 .recv = protocore_quic_conn_recv,
235 .send = protocore_quic_conn_send,
236 .on_timeout = protocore_quic_conn_on_timeout,
237 .stream_send = protocore_quic_conn_stream_send,
238 .owns = protocore_quic_conn_owns,
239 .close = protocore_quic_conn_close,
240 .close_app = protocore_quic_conn_close_app,
241 .is_established = protocore_quic_conn_is_established,
242 .is_closed = protocore_quic_conn_is_closed,
243};
244
246
247#endif // PROTOCORE_ENABLE_HTTP3
248
249#endif // PROTOCORE_QUIC_CONN_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97