ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
quic_conn.cpp
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 pc_quic_conn.cpp
6 * @brief Stateful QUIC v1 server connection engine (see pc_quic_conn.h).
7 */
8
10
11#if PC_ENABLE_HTTP3
12
13#include "crypto/aead/aes128gcm.h" // PC_AES128GCM_TAG_LEN
17#include <string.h>
18
19// A single STREAM frame's payload cannot exceed one datagram, so it can never overflow a stream's
20// reassembly buffer - which is why that clamp in handle_stream carries a coverage exclusion. Both
21// sizes are overridable macros (quic_conn.h), so pin the relationship here rather than let a jumbo
22// PC_QUIC_MAX_DATAGRAM or a shrunken PC_QUIC_STREAM_RX silently make the excluded path reachable.
23static_assert(PC_QUIC_MAX_DATAGRAM < PC_QUIC_STREAM_RX,
24 "PC_QUIC_STREAM_RX must exceed PC_QUIC_MAX_DATAGRAM: one STREAM frame's payload is bounded by the "
25 "datagram, and handle_stream relies on that to deliver it without clamping");
26
27// The frame payload build_frames fills is one datagram; every builder it calls (ACK, CRYPTO header,
28// HANDSHAKE_DONE, STREAM header) must fit with room to spare, which is why their failure guards
29// carry coverage exclusions. RFC 9000 sec 14.1 already requires at least 1200 octets for Initial
30// packets, so pin that floor: a smaller datagram would make those guards reachable.
31static_assert(PC_QUIC_MAX_DATAGRAM >= 1200,
32 "PC_QUIC_MAX_DATAGRAM must be at least the RFC 9000 sec 14.1 minimum of 1200 octets, which is also "
33 "what lets build_frames assume every frame builder has room");
34
35namespace
36{
37// The open (decrypt) keys for an encryption level: Initial keys come from the DCID, Handshake and
38// 1-RTT keys from the TLS handshake. Returns NULL if that level's keys are not available yet.
39QuicPacketKeys *open_keys(QuicConn *qc, int level)
40{
41 if (level == QuicEnc::QUIC_ENC_INITIAL)
42 {
43 return &qc->initial.client;
44 }
45 return pc_quic_tls_keys(&qc->tls, level, /*is_server=*/false);
46}
47QuicPacketKeys *seal_keys(QuicConn *qc, int level)
48{
49 if (level == QuicEnc::QUIC_ENC_INITIAL)
50 {
51 return &qc->initial.server;
52 }
53 return pc_quic_tls_keys(&qc->tls, level, /*is_server=*/true);
54}
55
56// Find a stream slot by id, or allocate one; NULL if the table is full.
57QuicStream *stream_get(QuicConn *qc, uint64_t id, bool create)
58{
59 QuicStream *free_slot = nullptr;
60 for (size_t i = 0; i < PC_QUIC_MAX_STREAMS; i++)
61 {
62 if (qc->streams[i].id == id && qc->streams[i].id != UINT64_MAX)
63 {
64 return &qc->streams[i];
65 }
66 if (!free_slot && qc->streams[i].id == UINT64_MAX)
67 {
68 free_slot = &qc->streams[i];
69 }
70 }
71 if (!create || !free_slot) // GCOVR_EXCL_LINE create is true at both call sites (handle_stream and
72 {
73 return nullptr; // pc_quic_conn_stream_send), so the lookup-only arm is never taken
74 }
75 memset(free_slot, 0, sizeof(*free_slot));
76 free_slot->id = id;
77 return free_slot;
78}
79} // namespace
80
81void pc_quic_conn_init(QuicConn *qc, const QuicTlsConfig *cfg, const uint8_t *odcid, uint8_t odcid_len,
82 const uint8_t *peer_scid, uint8_t peer_scid_len, const uint8_t *our_scid, uint8_t our_scid_len,
83 const QuicConnCallbacks *cb)
84{
85 memset(qc, 0, sizeof(*qc));
86 memcpy(qc->odcid, odcid, odcid_len);
87 qc->odcid_len = odcid_len;
88 memcpy(qc->dcid, peer_scid, peer_scid_len);
89 qc->dcid_len = peer_scid_len;
90 memcpy(qc->scid, our_scid, our_scid_len);
91 qc->scid_len = our_scid_len;
92 if (cb)
93 {
94 qc->cb = *cb;
95 }
96
97 pc_quic_derive_initial_secrets(odcid, odcid_len, &qc->initial);
98
99 for (int i = 0; i < 3; i++)
100 {
101 qc->space[i].largest_acked = -1;
102 qc->space[i].last_ae_pn = -1;
103 }
104 for (size_t i = 0; i < PC_QUIC_MAX_STREAMS; i++)
105 {
106 qc->streams[i].id = UINT64_MAX;
107 }
108
109 // The server's transport parameters carry the connection IDs the handshake must echo.
110 QuicTlsConfig c = *cfg;
111 c.params.has_original_dcid = true;
112 memcpy(c.params.original_dcid, odcid, odcid_len);
113 c.params.original_dcid_len = odcid_len;
114 c.params.has_initial_scid = true;
115 memcpy(c.params.initial_scid, our_scid, our_scid_len);
116 c.params.initial_scid_len = our_scid_len;
117 pc_quic_tls_server_init(&qc->tls, &c);
118}
119
120// --- Frame handling --------------------------------------------------------------------------
121namespace
122{
123// Queue a transport CONNECTION_CLOSE for a fatal error at @p level; the first error wins (RFC 9000 sec
124// 10.2.3). Sending at the level the error was seen on guarantees the peer holds keys to read it.
125void queue_close(QuicConn *qc, uint64_t error_code, uint64_t frame_type, int level)
126{
127 if (qc->close_queued || qc->closed)
128 {
129 return;
130 }
131 qc->close_queued = true;
132 qc->close_error = error_code;
133 qc->close_frame_type = frame_type;
134 qc->close_level = (uint8_t)level;
135}
136
137void handle_crypto(QuicConn *qc, int level, const QuicFrame *f)
138{
139 QuicPnSpace *s = &qc->space[level];
140 uint64_t want = s->crypto_rx_off;
141 if (f->crypto.offset > want)
142 {
143 return; // out-of-order beyond our window; the peer will retransmit
144 }
145 if (f->crypto.offset + f->crypto.length <= want)
146 {
147 return; // wholly duplicate
148 }
149 size_t skip = (size_t)(want - f->crypto.offset);
150 const uint8_t *nd = f->crypto.data + skip;
151 size_t nl = (size_t)(f->crypto.length - skip);
152 if (s->crypto_rx_have + nl > sizeof(s->crypto_rx))
153 {
154 nl = sizeof(s->crypto_rx) - s->crypto_rx_have; // clamp to the reassembly window
155 }
156 memcpy(s->crypto_rx + s->crypto_rx_have, nd, nl);
157 s->crypto_rx_have += nl;
158 s->crypto_rx_off += nl;
159
160 size_t used = pc_quic_tls_recv_crypto(&qc->tls, level, s->crypto_rx, s->crypto_rx_have);
161 if (used)
162 {
163 memmove(s->crypto_rx, s->crypto_rx + used, s->crypto_rx_have - used);
164 s->crypto_rx_have -= used;
165 }
166 // A fatal TLS error (bad Finished, unsupported handshake) becomes a QUIC CRYPTO_ERROR: report it
167 // to the client with the TLS alert in the low byte (RFC 9001 sec 4.8) instead of stalling.
168 if (qc->tls.state == QtlsState::QTLS_FAILED)
169 {
170 queue_close(qc, QuicErr::QUIC_ERR_CRYPTO_BASE + qc->tls.alert, QuicFrameType::QUIC_FT_CRYPTO, level);
171 return;
172 }
173 // Completing the handshake opens 1-RTT and lets us send HANDSHAKE_DONE. We keep the Handshake
174 // space live so the same outbound datagram still ACKs the client's Finished; HANDSHAKE_DONE (at
175 // 1-RTT) is what tells the client the handshake is confirmed (RFC 9001 sec 4.1.2).
176 if (qc->tls.state == QtlsState::QTLS_DONE && !qc->handshake_done_sent && !qc->handshake_done_queued)
177 {
178 qc->handshake_done_queued = true;
179 qc->address_validated = true;
180 if (qc->cb.on_handshake_done)
181 {
182 qc->cb.on_handshake_done(qc->cb.app, qc);
183 }
184 }
185}
186
187void handle_stream(QuicConn *qc, const QuicFrame *f)
188{
189 QuicStream *st = stream_get(qc, f->stream.id, true);
190 if (!st)
191 {
192 return;
193 }
194 uint64_t want = st->rx_off;
195 if (f->stream.offset > want)
196 {
197 return; // out-of-order beyond window
198 }
199 if (f->stream.offset + f->stream.length > want)
200 {
201 size_t skip = (size_t)(want - f->stream.offset);
202 const uint8_t *nd = f->stream.data + skip;
203 size_t nl = (size_t)(f->stream.length - skip);
204 if (nl > sizeof(st->rx)) // GCOVR_EXCL_LINE one STREAM frame's length <= datagram (<=1350) < STREAM_RX(2048)
205 {
206 nl = sizeof(st->rx); // GCOVR_EXCL_LINE so nl never exceeds the per-stream reassembly buffer
207 }
208 // Deliver in place (we hand the callback the contiguous new bytes directly).
209 st->rx_off += nl;
210 if (f->stream.fin)
211 {
212 st->rx_fin = true;
213 }
214 if (qc->cb.on_stream_data)
215 {
216 qc->cb.on_stream_data(qc->cb.app, qc, st->id, nd, nl, st->rx_fin);
217 }
218 return;
219 }
220 if (f->stream.fin && f->stream.offset + f->stream.length == want && !st->rx_fin)
221 {
222 st->rx_fin = true;
223 if (qc->cb.on_stream_data)
224 {
225 qc->cb.on_stream_data(qc->cb.app, qc, st->id, nullptr, 0, true);
226 }
227 }
228}
229
230// Process the frames in one decrypted packet. Returns false on a fatal connection error.
231bool process_frames(QuicConn *qc, int level, const uint8_t *p, size_t len, bool *ack_eliciting)
232{
233 size_t off = 0;
234 while (off < len)
235 {
236 if (p[off] == QuicFrameType::QUIC_FT_PADDING)
237 {
238 off++;
239 continue;
240 }
241 QuicFrame f;
242 size_t n = pc_quic_frame_parse(p + off, len - off, &f);
243 if (!n)
244 {
245 // Undecodable frame: a transport FRAME_ENCODING_ERROR (RFC 9000 sec 20.1). Report it.
246 queue_close(qc, QuicErr::QUIC_ERR_FRAME_ENCODING, 0, level);
247 return false;
248 }
249 off += n;
250
251 if (f.type != QuicFrameType::QUIC_FT_ACK && f.type != QuicFrameType::QUIC_FT_ACK_ECN &&
252 f.type != QuicFrameType::QUIC_FT_CONNECTION_CLOSE && f.type != QuicFrameType::QUIC_FT_CONNECTION_CLOSE_APP)
253 {
254 *ack_eliciting = true;
255 }
256
257 switch (f.type)
258 {
259 case QuicFrameType::QUIC_FT_CRYPTO:
260 handle_crypto(qc, level, &f);
261 break;
262 case QuicFrameType::QUIC_FT_ACK:
263 case QuicFrameType::QUIC_FT_ACK_ECN:
264 if ((int64_t)f.ack.largest > qc->space[level].largest_acked)
265 {
266 qc->space[level].largest_acked = (int64_t)f.ack.largest;
267 // Forward progress: reset the PTO backoff and re-evaluate the timer (RFC 9002 sec 6.2).
268 qc->pto_count = 0;
269 qc->pto_armed = false;
270 }
271 break;
272 case QuicFrameType::QUIC_FT_CONNECTION_CLOSE:
273 case QuicFrameType::QUIC_FT_CONNECTION_CLOSE_APP:
274 qc->draining = true;
275 qc->closed = true;
276 break;
277 case QuicFrameType::QUIC_FT_HANDSHAKE_DONE:
278 break; // server-only frame; ignore if a peer sends it
279 case QuicFrameType::QUIC_FT_MAX_DATA:
280 case QuicFrameType::QUIC_FT_PING:
281 break; // no per-frame state to keep for a minimal server
282 default:
283 if (f.type >= QuicFrameType::QUIC_FT_STREAM && f.type <= QuicFrameType::QUIC_FT_STREAM + 7)
284 {
285 handle_stream(qc, &f);
286 }
287 break;
288 }
289 }
290 return true;
291}
292
293// Skip an Initial packet's Token field (RFC 9000 sec 17.2.2), advancing *off. False if malformed.
294bool skip_initial_token(const uint8_t *dg, size_t len, size_t *off)
295{
296 uint64_t tok_len = 0;
297 size_t c = 0;
298 if (!pc_quic_varint_decode(dg + *off, len - *off, &tok_len, &c))
299 {
300 return false;
301 }
302 *off += c + (size_t)tok_len;
303 return *off <= len;
304}
305
306// Parse one packet's (long or short) header, filling the fields needed to locate + unprotect it.
307// Returns false on a malformed header or an unsupported type/version (drop the packet).
308bool parse_packet_header(const QuicConn *qc, const uint8_t *dg, size_t len, bool is_long, int *level, size_t *pn_offset,
309 size_t *pkt_len, uint64_t *payload_length)
310{
311 if (!is_long)
312 {
313 // Short header: DCID length is our locally chosen scid_len; the packet runs to datagram end.
314 *level = QuicEnc::QUIC_ENC_APP;
315 *pn_offset = 1 + qc->scid_len;
316 if (*pn_offset >= len)
317 {
318 return false;
319 }
320 *payload_length = len - *pn_offset;
321 *pkt_len = len;
322 return true;
323 }
324
325 QuicLongHeader h;
326 if (!pc_quic_parse_long_header(dg, len, &h))
327 {
328 return false;
329 }
330 if (h.version == 0 || h.version != QUIC_VERSION_1)
331 {
332 return false; // Version Negotiation is a client concern; unknown versions are dropped
333 }
334 if (h.type == QuicLongPacket::QUIC_LP_INITIAL)
335 {
336 *level = QuicEnc::QUIC_ENC_INITIAL;
337 }
338 else if (h.type == QuicLongPacket::QUIC_LP_HANDSHAKE)
339 {
340 *level = QuicEnc::QUIC_ENC_HANDSHAKE;
341 }
342 else
343 {
344 return false; // 0-RTT / Retry not supported
345 }
346
347 size_t off = h.hdr_len;
348 if (*level == QuicEnc::QUIC_ENC_INITIAL && !skip_initial_token(dg, len, &off))
349 {
350 return false;
351 }
352 size_t c = 0;
353 if (!pc_quic_varint_decode(dg + off, len - off, payload_length, &c))
354 {
355 return false;
356 }
357 off += c;
358 *pn_offset = off;
359 *pkt_len = *pn_offset + (size_t)*payload_length;
360 return *pkt_len <= len;
361}
362
363// Decrypt and process one packet at datagram offset; returns bytes consumed (0 to stop the datagram).
364size_t recv_packet(QuicConn *qc, const uint8_t *dg, size_t len)
365{
366 if (len < 1) // GCOVR_EXCL_LINE pc_quic_conn_recv's loop only calls this with off<len, so len-off>=1
367 {
368 return 0; // GCOVR_EXCL_LINE
369 }
370 bool is_long = pc_quic_is_long_header(dg[0]);
371
372 int level = 0;
373 size_t pn_offset = 0;
374 size_t pkt_len = 0; // total on-wire bytes of this packet
375 uint64_t payload_length = 0;
376 if (!parse_packet_header(qc, dg, len, is_long, &level, &pn_offset, &pkt_len, &payload_length))
377 {
378 return 0;
379 }
380
381 QuicPacketKeys *const keys = open_keys(qc, level);
382 if (!keys)
383 {
384 return 0; // keys for this level are not available yet
385 }
386
387 // Unprotect on a copy so a failed decrypt does not corrupt following coalesced packets. The
388 // engine runs sequentially on one task, so the scratch is a shared static (not reentrant).
389 static uint8_t work[PC_QUIC_MAX_DATAGRAM];
390 static uint8_t plain[PC_QUIC_MAX_DATAGRAM];
391 if (pkt_len > sizeof(work))
392 {
393 return 0;
394 }
395 memcpy(work, dg, pkt_len);
396 uint64_t pn = 0;
397 size_t pt = pc_quic_packet_unprotect(work, pn_offset, (size_t)payload_length, qc->space[level].largest_rx, *keys,
398 is_long, plain, &pn);
399 if (pt == (size_t)-1)
400 {
401 return is_long ? pkt_len : 0; // drop this packet, keep parsing later coalesced ones
402 }
403
404 if (!qc->space[level].have_rx || pn > qc->space[level].largest_rx)
405 {
406 qc->space[level].largest_rx = pn;
407 }
408 qc->space[level].have_rx = true;
409
410 // Receiving a Handshake packet validates the client's address (lifts anti-amplification).
411 if (level == QuicEnc::QUIC_ENC_HANDSHAKE)
412 {
413 qc->address_validated = true;
414 qc->space[QuicEnc::QUIC_ENC_INITIAL].discarded = true;
415 }
416
417 bool ack_eliciting = false;
418 process_frames(qc, level, plain, pt, &ack_eliciting);
419 if (ack_eliciting)
420 {
421 qc->space[level].ack_eliciting_rx = true;
422 }
423
424 return pkt_len;
425}
426} // namespace
427
428bool pc_quic_conn_recv(QuicConn *qc, const uint8_t *datagram, size_t len)
429{
430 if (qc->closed)
431 {
432 return false;
433 }
434 qc->recv_bytes += len;
435 size_t off = 0;
436 bool any = false;
437 while (off < len)
438 {
439 size_t n = recv_packet(qc, datagram + off, len - off);
440 if (!n)
441 {
442 break;
443 }
444 any = true;
445 off += n;
446 }
447 return any;
448}
449
450// --- Sending ---------------------------------------------------------------------------------
451namespace
452{
453// Append the owed ACK frame for space @p s (RFC 9000 sec 13.2); returns bytes written.
454size_t build_ack_frame(QuicPnSpace *s, uint8_t *buf, size_t cap)
455{
456 if (!s->ack_eliciting_rx || !s->have_rx)
457 {
458 return 0;
459 }
460 size_t n = pc_quic_build_ack(buf, cap, s->largest_rx, 0, s->largest_rx);
461 if (n) // GCOVR_EXCL_LINE build_frames hands this the whole PC_QUIC_MAX_DATAGRAM scratch (>=1200 by the
462 {
463 s->ack_eliciting_rx = false; // static_assert above), and one ACK frame is at most ~20 bytes: it always fits
464 }
465 return n;
466}
467
468// Append the CRYPTO flight for INITIAL/HANDSHAKE (ServerHello / EE..Finished); returns bytes written,
469// sets *ae when it emits an ack-eliciting CRYPTO frame.
470size_t build_crypto_frame(const QuicConn *qc, int level, QuicPnSpace *s, uint8_t *buf, size_t cap, bool *ae)
471{
472 if (level != QuicEnc::QUIC_ENC_INITIAL && level != QuicEnc::QUIC_ENC_HANDSHAKE)
473 {
474 return 0;
475 }
476 size_t flen = 0;
477 const uint8_t *flight = pc_quic_tls_flight(&qc->tls, level, &flen);
478 if (!flight || s->crypto_tx_off >= flen) // GCOVR_EXCL_LINE pc_quic_tls_flight only returns NULL for a level
479 {
480 return 0; // other than INITIAL/HANDSHAKE, which the guard above already excluded
481 }
482 size_t remain = flen - (size_t)s->crypto_tx_off;
483 // Leave room for the CRYPTO frame header (type + offset + length varints, <= 1+8+8).
484 size_t room = (cap > 20) ? (cap - 20) : 0;
485 size_t take = remain < room ? remain : room;
486 if (!take) // GCOVR_EXCL_LINE remain>0 (checked above) and cap is the datagram scratch less at most one
487 {
488 return 0; // GCOVR_EXCL_LINE ACK, so it always exceeds 20 and room is never 0
489 }
490 size_t n = pc_quic_build_crypto(buf, cap, s->crypto_tx_off, flight + s->crypto_tx_off, take);
491 if (n) // GCOVR_EXCL_LINE take was clamped to cap-20 and the CRYPTO header is at most 17 bytes: it always fits
492 {
493 s->crypto_tx_off += take;
494 *ae = true;
495 }
496 return n;
497}
498
499// Append 1-RTT extras (HANDSHAKE_DONE + stream data) at APP level; returns bytes written, sets *ae.
500size_t build_app_frames(QuicConn *qc, int level, uint8_t *buf, size_t cap, bool *ae)
501{
502 if (level != QuicEnc::QUIC_ENC_APP)
503 {
504 return 0;
505 }
506 size_t p = 0;
507 if (qc->handshake_done_queued)
508 {
509 size_t n = pc_quic_build_handshake_done(buf + p, cap - p);
510 if (n) // GCOVR_EXCL_LINE HANDSHAKE_DONE is one byte and this is the first frame written after the
511 { // ACK/CRYPTO, so the datagram-sized scratch always has room for it
512
513 p += n;
514 qc->handshake_done_queued = false;
515 qc->handshake_done_sent = true;
516 *ae = true;
517 }
518 }
519 for (size_t i = 0; i < PC_QUIC_MAX_STREAMS; i++)
520 {
521 QuicStream *st = &qc->streams[i];
522 if (st->id == UINT64_MAX)
523 {
524 continue;
525 }
526 bool more = st->tx_sent < st->tx_have;
527 bool fin_due = st->tx_fin && !st->tx_fin_sent && st->tx_sent == st->tx_have;
528 if (!more && !fin_due)
529 {
530 continue;
531 }
532 size_t room = (cap - p > 24) ? (cap - p - 24) : 0;
533 size_t remain = st->tx_have - st->tx_sent;
534 size_t take = remain < room ? remain : room;
535 bool fin = st->tx_fin && (st->tx_sent + take == st->tx_have);
536 size_t n = pc_quic_build_stream(buf + p, cap - p, st->id, st->tx_off, st->tx + st->tx_sent, take, fin);
537 if (n)
538 {
539 p += n;
540 st->tx_off += take;
541 st->tx_sent += take;
542 st->tx_fin_sent = st->tx_fin_sent || fin;
543 *ae = true;
544 }
545 }
546 return p;
547}
548
549// Build the frame payload for one encryption level into buf; returns its length (0 = nothing to send).
550// @p ae is set true if the payload carries an ack-eliciting frame (CRYPTO / STREAM / HANDSHAKE_DONE),
551// which arms loss recovery for this space.
552size_t build_frames(QuicConn *qc, int level, uint8_t *buf, size_t cap, bool *ae)
553{
554 QuicPnSpace *s = &qc->space[level];
555 size_t p = 0;
556 *ae = false;
557
558 // While closing, the only frame we send is the transport CONNECTION_CLOSE (RFC 9000 sec 10.2.3).
559 // It is not ack-eliciting (*ae stays false), so no PTO is armed for it. pc_quic_conn_send() invokes
560 // this for a single level when a close is queued, so it is emitted exactly once.
561 if (qc->close_queued && !qc->close_sent)
562 {
563 return pc_quic_build_connection_close(buf, cap, qc->close_error, qc->close_frame_type, nullptr, 0);
564 }
565
566 p += build_ack_frame(s, buf + p, cap - p); // ACK first, if we owe one
567 p += build_crypto_frame(qc, level, s, buf + p, cap - p, ae);
568 p += build_app_frames(qc, level, buf + p, cap - p, ae);
569 return p;
570}
571
572// Long-header packet type for an encryption level.
573uint8_t level_lp_type(int level)
574{
575 return level == QuicEnc::QUIC_ENC_INITIAL ? QuicLongPacket::QUIC_LP_INITIAL : QuicLongPacket::QUIC_LP_HANDSHAKE;
576}
577
578// Bytes a protected packet needs on top of its payload: AEAD tag + packet number, plus the header.
579size_t packet_overhead(const QuicConn *qc, bool is_long, uint8_t pn_len)
580{
581 size_t overhead = (size_t)PC_AES128GCM_TAG_LEN + pn_len;
582 if (is_long)
583 {
584 // type(1) + version(4) + dcid_len(1) + dcid + scid_len(1) + scid, then the Initial token
585 // varint and the Length varint (bounded generously - both are far smaller in practice).
586 overhead += 7u + qc->dcid_len + qc->scid_len + 1u + 4u;
587 }
588 else
589 {
590 overhead += 1u + qc->dcid_len; // short header: first byte + DCID, no length on the wire
591 }
592 return overhead;
593}
594
595// Build one protected packet for a level into out; returns its length (0 = nothing to send).
596size_t build_packet(QuicConn *qc, int level, uint8_t *out, size_t cap)
597{
598 QuicPnSpace *s = &qc->space[level];
599 if (s->discarded)
600 {
601 return 0;
602 }
603 QuicPacketKeys *const keys = seal_keys(qc, level);
604 if (!keys)
605 {
606 return 0;
607 }
608
609 uint64_t pn = s->next_pn;
610 uint8_t pn_len = pc_quic_pn_length(pn, s->largest_acked);
611 bool is_long = (level != QuicEnc::QUIC_ENC_APP);
612
613 // Reserve the framing BEFORE filling the payload. build_frames() advances the connection's send
614 // offsets (crypto_tx_off, and each stream's tx_off / tx_sent) as it writes, so a payload that
615 // turns out not to fit once the header, packet number and AEAD tag are added cannot simply be
616 // rejected further down: those bytes would already be counted as sent and would never go out.
617 // That is silent data loss, and it was reachable - build_frames was handed the whole
618 // PC_QUIC_MAX_DATAGRAM scratch while the packet needs header + pn + 16-byte tag on top of it,
619 // so a stream with ~1326+ bytes pending, or a Handshake CRYPTO flight carrying a large
620 // certificate, produced a payload the packet had no room for. Bound the payload instead.
621 size_t overhead = packet_overhead(qc, is_long, pn_len);
622 if (cap <= overhead)
623 {
624 return 0;
625 }
626 size_t budget = cap - overhead;
627
628 uint8_t frames[PC_QUIC_MAX_DATAGRAM];
629 // Dead with every current caller - quic_server's datagram buffer is itself
630 // PC_QUIC_MAX_DATAGRAM, so `cap - overhead` is always below `frames` - but kept as the bound
631 // on the memcpy target if a caller ever hands us a larger buffer.
632 if (budget > sizeof(frames)) // GCOVR_EXCL_BR_LINE - see above
633 {
634 budget = sizeof(frames); // GCOVR_EXCL_LINE
635 }
636 // Leave room for the PADDING the header-protection minimum may need (RFC 9001 sec 5.4.2).
637 if (budget < 4)
638 {
639 return 0;
640 }
641 budget -= 4;
642 bool ae = false;
643 size_t frame_len = build_frames(qc, level, frames, budget, &ae);
644 if (frame_len == 0)
645 {
646 return 0;
647 }
648
649 // Header protection samples 16 bytes at (packet number + 4), so the packet number and payload
650 // together must be at least 4 bytes (RFC 9001 sec 5.4.2). Pad tiny packets (e.g. a lone
651 // HANDSHAKE_DONE or ACK) with PADDING frames (zero bytes) to reach that minimum.
652 if ((size_t)pn_len + frame_len < 4)
653 {
654 size_t pad = 4 - pn_len - frame_len;
655 memset(frames + frame_len, 0, pad);
656 frame_len += pad;
657 }
658
659 size_t p = 0;
660 if (is_long)
661 {
662 // Invariant header fields, then the type-specific token (Initial only) + Length + PN.
663 size_t hn = pc_quic_build_long_header(out, cap, level_lp_type(level), QUIC_VERSION_1, qc->dcid, qc->dcid_len,
664 qc->scid, qc->scid_len, pn_len);
665 if (!hn) // GCOVR_EXCL_BR_LINE - `overhead` above already reserved more than the header
666 {
667 return 0; // GCOVR_EXCL_LINE
668 }
669 p = hn;
670 if (level == QuicEnc::QUIC_ENC_INITIAL)
671 {
672 size_t n = pc_quic_varint_encode(out + p, cap - p, 0); // empty token
673 if (!n) // GCOVR_EXCL_BR_LINE - reserved in `overhead`
674 {
675 return 0; // GCOVR_EXCL_LINE
676 }
677 p += n;
678 }
679 uint64_t length = (uint64_t)pn_len + frame_len + PC_AES128GCM_TAG_LEN;
680 size_t n = pc_quic_varint_encode(out + p, cap - p, length);
681 if (!n) // GCOVR_EXCL_BR_LINE - reserved in `overhead`
682 {
683 return 0; // GCOVR_EXCL_LINE
684 }
685 p += n;
686 }
687 else
688 {
689 // Short header: 0x40 fixed bit | pn_len-1; then the peer's DCID (no length on the wire).
690 if (1 + qc->dcid_len > cap) // GCOVR_EXCL_BR_LINE - reserved in `overhead`
691 {
692 return 0; // GCOVR_EXCL_LINE
693 }
694 out[0] = (uint8_t)(0x40 | (pn_len - 1));
695 memcpy(out + 1, qc->dcid, qc->dcid_len);
696 p = 1 + qc->dcid_len;
697 }
698
699 size_t pn_offset = p;
700 // Bound the whole packet up front (p <= cap here): the header builders checked their own writes
701 // but not the packet number + payload + tag that follow, so verify the remainder fits before
702 // writing it (avoids a size_t addition wrap in the bounds check, cpp:S3519).
703 // Redundant since the payload is now budgeted against `overhead` up front, which is exactly
704 // what makes the offsets build_frames() advanced safe to keep. Retained as defense in depth.
705 if (cap - p < (size_t)pn_len + frame_len + PC_AES128GCM_TAG_LEN) // GCOVR_EXCL_BR_LINE - see above
706 {
707 return 0; // GCOVR_EXCL_LINE
708 }
709 // Write the (unprotected) truncated packet number.
710 for (uint8_t i = 0; i < pn_len; i++)
711 {
712 out[pn_offset + i] = (uint8_t)(pn >> (8 * (pn_len - 1 - i)));
713 }
714 p += pn_len;
715
716 memcpy(out + p, frames, frame_len);
717
718 size_t total = pc_quic_packet_protect(out, cap, pn_offset, pn_len, pn, frame_len, *keys, is_long);
719 if (!total) // GCOVR_EXCL_LINE the pn_offset bound above already guarantees protect has room, and with
720 {
721 return 0; // GCOVR_EXCL_LINE valid keys the AEAD seal cannot otherwise fail
722 }
723 if (ae)
724 {
725 s->last_ae_pn = (int64_t)pn; // this space now has ack-eliciting data outstanding
726 }
727 s->next_pn++;
728 return total;
729}
730
731// Highest encryption level (INITIAL..APP) we still hold seal keys for and haven't discarded - the
732// level at which a CONNECTION_CLOSE can still be decrypted by the peer. Falls back to INITIAL.
733int pc_quic_highest_sealed_level(QuicConn *qc)
734{
735 for (int l = QuicEnc::QUIC_ENC_APP; l >= QuicEnc::QUIC_ENC_INITIAL; l--)
736 {
737 if (!qc->space[l].discarded && seal_keys(qc, l))
738 {
739 return l;
740 }
741 }
742 return QuicEnc::QUIC_ENC_INITIAL;
743}
744} // namespace
745
746size_t pc_quic_conn_send(QuicConn *qc, uint8_t *out, size_t cap)
747{
748 if (qc->closed && !qc->draining)
749 {
750 return 0;
751 }
752 // Anti-amplification (RFC 9000 sec 8.1): until the client's address is validated, send nothing
753 // once we have already put 3x the received bytes on the wire. Checked BEFORE building the packets
754 // so a blocked send never advances packet-number / CRYPTO / stream state (which would desync the
755 // flight); a build then discard was the bug. Being at-most one datagram approximate here is fine.
756 if (!qc->address_validated && qc->sent_bytes >= 3 * qc->recv_bytes)
757 {
758 return 0;
759 }
760 if (cap > PC_QUIC_MAX_DATAGRAM)
761 {
762 cap = PC_QUIC_MAX_DATAGRAM;
763 }
764
765 // A queued transport CONNECTION_CLOSE is sent once - at the highest encryption level we still hold
766 // keys for (so the peer can decrypt it) - and then the connection is closed. It replaces the normal
767 // frame build (a closing endpoint sends nothing else) and is bounded by the amplification limit above.
768 if (qc->close_queued && !qc->close_sent)
769 {
770 // Send at the level the error was seen on (the peer holds those keys); if that space has since
771 // been discarded, fall back to the highest level we still hold keys for.
772 int level = qc->close_level;
773 // GCOVR_EXCL_START close_level is a uint8_t and QuicEnc::QUIC_ENC_INITIAL is 0, so the underflow
774 // arm of the range check below can never be taken; the other three arms are all tested.
775 if (level < QuicEnc::QUIC_ENC_INITIAL || level > QuicEnc::QUIC_ENC_APP || qc->space[level].discarded ||
776 !seal_keys(qc, level))
777 {
778 level = pc_quic_highest_sealed_level(qc);
779 }
780 // GCOVR_EXCL_STOP
781 size_t n = build_packet(qc, level, out, cap);
782 if (n)
783 {
784 qc->close_sent = true;
785 qc->closed = true;
786 qc->sent_bytes += n;
787 }
788 return n;
789 }
790
791 size_t dg = 0;
792 // Coalesce Initial, then Handshake, then 1-RTT into one datagram.
793 for (int level = QuicEnc::QUIC_ENC_INITIAL; level <= QuicEnc::QUIC_ENC_APP; level++)
794 {
795 size_t n = build_packet(qc, level, out + dg, cap - dg);
796 dg += n;
797 }
798 if (dg == 0)
799 {
800 return 0;
801 }
802 qc->sent_bytes += dg;
803 return dg;
804}
805
806namespace
807{
808// PTO period with exponential backoff, capped so the shift cannot overflow (RFC 9002 sec 6.2.1).
809uint32_t pto_period(uint8_t count)
810{
811 uint32_t p = PC_QUIC_PTO_MS;
812 for (uint8_t i = 0; i < count && p < (1u << 30); i++)
813 {
814 p <<= 1;
815 }
816 return p;
817}
818// A space has unacknowledged ack-eliciting data outstanding: it sent an ack-eliciting packet the peer
819// has not yet acknowledged, and its keys are still live.
820bool space_outstanding(const QuicPnSpace *s)
821{
822 return !s->discarded && s->last_ae_pn >= 0 && s->largest_acked < s->last_ae_pn;
823}
824} // namespace
825
826void pc_quic_conn_on_timeout(QuicConn *qc, uint32_t now_ms)
827{
828 if (qc->closed)
829 {
830 return;
831 }
832 // Loss recovery (RFC 9002): retransmission is driven by a Probe Timeout, because a lost server
833 // packet is not re-triggered by the peer (a duplicate ClientHello re-delivers no CRYPTO; a lost
834 // 1-RTT response is never re-requested). Anything the peer has not acknowledged in a live space -
835 // the handshake CRYPTO flight, HANDSHAKE_DONE, or the 1-RTT response - is outstanding.
836 bool outstanding = space_outstanding(&qc->space[QuicEnc::QUIC_ENC_INITIAL]) ||
837 space_outstanding(&qc->space[QuicEnc::QUIC_ENC_HANDSHAKE]) ||
838 space_outstanding(&qc->space[QuicEnc::QUIC_ENC_APP]);
839 if (!outstanding)
840 {
841 qc->pto_armed = false; // everything acknowledged: nothing to retransmit
842 qc->pto_count = 0;
843 return;
844 }
845 if (!qc->pto_armed)
846 {
847 qc->pto_armed = true;
848 qc->pto_deadline_ms = now_ms + pto_period(qc->pto_count);
849 return;
850 }
851 if ((int32_t)(now_ms - qc->pto_deadline_ms) < 0)
852 {
853 return; // not yet (wrap-safe compare)
854 }
855
856 // PTO fired: mark the unacknowledged data in each outstanding space for retransmission so the next
857 // pc_quic_conn_send() re-sends it, then back the timer off.
858 for (int level = QuicEnc::QUIC_ENC_INITIAL; level <= QuicEnc::QUIC_ENC_HANDSHAKE; level++)
859 {
860 if (space_outstanding(&qc->space[level]))
861 {
862 qc->space[level].crypto_tx_off = 0; // re-send the CRYPTO flight for this level
863 }
864 }
865 if (space_outstanding(&qc->space[QuicEnc::QUIC_ENC_APP]))
866 {
867 // Re-send 1-RTT data. The peer dedups STREAM data by offset, so rewinding each stream to 0
868 // recovers a lost response and is a no-op for data already received.
869 if (qc->handshake_done_sent)
870 {
871 qc->handshake_done_queued = true;
872 qc->handshake_done_sent = false;
873 }
874 for (size_t i = 0; i < PC_QUIC_MAX_STREAMS; i++)
875 {
876 QuicStream *st = &qc->streams[i];
877 if (st->id == UINT64_MAX)
878 {
879 continue;
880 }
881 st->tx_off = 0;
882 st->tx_sent = 0;
883 st->tx_fin_sent = false;
884 }
885 }
886 if (qc->pto_count < 8)
887 {
888 qc->pto_count++;
889 }
890 qc->pto_deadline_ms = now_ms + pto_period(qc->pto_count);
891}
892
893size_t pc_quic_conn_stream_send(QuicConn *qc, uint64_t stream_id, const uint8_t *data, size_t len, bool fin)
894{
895 QuicStream *st = stream_get(qc, stream_id, true);
896 if (!st)
897 {
898 return 0;
899 }
900 size_t room = sizeof(st->tx) - st->tx_have;
901 size_t take = len < room ? len : room;
902 memcpy(st->tx + st->tx_have, data, take);
903 st->tx_have += take;
904 if (fin && take == len)
905 {
906 st->tx_fin = true;
907 }
908 return take;
909}
910
911void pc_quic_conn_close(QuicConn *qc, uint64_t error_code)
912{
913 // Application-initiated close: send at the highest level we still hold keys for.
914 int level = pc_quic_highest_sealed_level(qc);
915 queue_close(qc, error_code, 0, level);
916}
917
918bool pc_quic_conn_established(const QuicConn *qc)
919{
920 return qc->tls.state == QtlsState::QTLS_DONE;
921}
922
923bool pc_quic_conn_is_closed(const QuicConn *qc)
924{
925 return qc->closed || qc->draining;
926}
927
928#endif // PC_ENABLE_HTTP3
AES-128 block cipher + AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).