net.quic #
net.quic — build dependencies
Tracking issue: vlang/v#27675.
TLS 1.3 approach
net.quic implements a QUIC-scoped TLS 1.3 handshake (RFC 8446) in pure V, rather than patching vendored mbedTLS (which has no QUIC support in any released version — see the issue for the full rationale). X.509 certificate parsing and chain validation are delegated to mbedTLS's already-bound C functions (mbedtls_x509_crt_parse, mbedtls_x509_crt_verify, mbedtls_pk_parse_key, mbedtls_pk_verify_ext/mbedtls_pk_verify) — the same thing net.http's HTTP/1.1 and HTTP/2 backends already do. No mbedTLS source patch is required for this.
OpenSSL dependency: hard, not opt-out
TLS 1.3 key exchange for net.quic needs P-256 ECDH (for the secp256r1 key_share group), which didn't exist anywhere in V before this. It's added as a new OpenSSL binding (vlib/crypto/ecdsa/ecdsa.c.v), following the same -lcrypto linkage crypto.ecdsa already uses for ECDSA sign/verify.
This was a candidate for a -d no_openssl_quic opt-out flag (falling back to Ed25519-only cert chain support), but is not needed: Windows CI (windows_ci_gcc.yml) already builds and runs vlib/crypto/ecdsa/ecdsa_test.v against OpenSSL today, with explicit OpenSSL diagnostics steps beforehand. So the exact dependency net.quic needs is already proven to build and pass on Linux, macOS, and Windows. Decision: P-256 ECDH is a hard dependency of net.quic. No opt-out build flag, no reduced-interop fallback mode.
CertificateVerify signature verification (ECDSA and RSA-PSS) and certificate chain-of-trust validation (including RSA-PKCS1v1.5-signed certificates, still common among real-world CAs — net.quic advertises this via the signature_algorithms_cert extension, RFC 8446 §4.2.3) are both handled through mbedTLS's already-vendored, already-bound C functions (mbedtls_pk_verify_ext, mbedtls_x509_crt_verify) — no OpenSSL dependency for either. (An earlier draft of this file added a separate vlib/crypto/rsa_pss/ OpenSSL module for RSA-PSS specifically; it was removed as unused dead code once the mbedTLS path above was confirmed to cover the same need.)
mbedTLS X.509-only usage (no mbedtls_ssl_context)
net.quic calls mbedtls_x509_crt_parse/_verify and mbedtls_pk_parse_key/ mbedtls_pk_verify directly, without ever constructing an mbedtls_ssl_context — a usage pattern the existing net.mbedtls.SSLConn path never exercises (it always builds a full SSL context/config). This is confirmed to work: see vlib/net/mbedtls/x509_standalone_test.v, which parses and verifies a certificate with no mbedtls_ssl_context in scope, relying only on the module's existing init() (v_mbedtls_threading_setup(), already called automatically on import net.mbedtls regardless of whether an SSLConn is ever constructed).
Constants #
const min_initial_datagram_size = 1200
min_initial_datagram_size is RFC 9000 §14.1's minimum UDP datagram size for any datagram carrying an Initial packet -- an anti-amplification measure (a server must not be usable to amplify traffic toward a spoofed victim address by more than a small factor before the client's address has been validated).
const max_datagram_size = u64(1200)
kInitialWindow (RFC 9002 §7.2): min(10max_datagram_size, max(2max_datagram_size, 14720)) with max_datagram_size pinned to 1200 (matching the safe-minimum PMTU Phase 4 already assumes for Initial packet padding) = min(12000, max(2400, 14720)) = min(12000, 14720) = 12000.
const initial_window = u64(12000)
const minimum_window = u64(2 * max_datagram_size)
kMinimumWindow (RFC 9002 §7.2).
const loss_reduction_factor = 0.5
kLossReductionFactor (RFC 9002 §7.2).
const max_crypto_stream_buffered_bytes = u64(65536)
max_crypto_stream_buffered_bytes bounds how much data (consumed + out-of-order pending) a single CryptoStreamReassembler will ever accept at one encryption level, keyed off the highest byte offset any admitted frame may claim. CRYPTO frames are NOT subject to QUIC's ordinary stream flow control (RFC 9000 §7.5), so nothing else in this module caps this -- without a bound here, a malicious or buggy peer could send arbitrarily far-future CRYPTO offsets and exhaust memory before the handshake ever completes or fails. 64 KiB is generously larger than any realistic TLS 1.3 handshake flight (ClientHello/ServerHello/EncryptedExtensions/ Certificate chain/CertificateVerify/Finished all comfortably fit within low tens of KB even for a sizeable certificate chain).
const max_crypto_stream_pending_fragments = 64
max_crypto_stream_pending_fragments bounds how many DISTINCT out-of-order fragments may accumulate in one CryptoStreamReassembler's pending list at once -- a separate concern from max_crypto_stream_buffered_bytes (which bounds the offset RANGE a fragment may claim, not how many small non-overlapping fragments can exist within that range). 64 is generous headroom for genuine network reordering across a single handshake's CRYPTO stream.
const default_ack_delay_exponent = u64(3)
default_ack_delay_exponent is RFC 9000 §18.2's default value for the ack_delay_exponent transport parameter, used when a peer has not yet sent (or does not override) it.
const h3_frame_data = u64(0x00)
const h3_frame_headers = u64(0x01)
const h3_frame_cancel_push = u64(0x03)
const h3_frame_settings = u64(0x04)
const h3_frame_push_promise = u64(0x05)
const h3_frame_goaway = u64(0x07)
const h3_frame_max_push_id = u64(0x0d)
const h3_control_stream_type = u64(0x00)
h3_control_stream_type is the Stream Type value that marks a control stream (RFC 9114 §6.2.1).
const h3_push_stream_type = u64(0x01)
h3_push_stream_type is the Stream Type value that marks a push stream (RFC 9114 §6.2.2); it is immediately followed by the push ID it fulfills.
const quic_v1 = u32(00x00000001)
const quic_v1_max_cid_len = 20
quic_v1_max_cid_len is RFC 9000 §17.2's own connection-ID length limit for this version: "In QUIC version 1, this value MUST NOT exceed 20 bytes." Enforced on both the encode and parse paths for long headers -- the wire format's own length field allows up to 255 bytes (a single byte), which is NOT the same thing as this version's own protocol limit; a peer sending 21-255 is spec-violating even though the wire format itself has no problem representing it.
const initial_salt = [u8(0x38), 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a,
0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a]!
RFC 9001 §5.2 — the QUIC v1 Initial packet protection salt. Used as the HKDF-Extract salt when deriving Initial secrets from a connection's client-chosen Destination Connection ID. This value is public and fixed per QUIC version; it provides domain separation between QUIC versions, not secrecy.
const max_key_updates_accepted = 1000
max_key_updates_accepted bounds how many PEER-INITIATED key updates one KeyUpdateState will honor over the connection's life. A compliant peer paces updates out (RFC 9001 §6.1: waits for an acknowledgment of a packet sent in the current phase; §6.5: then roughly 3x the probe timeout) -- but that norm can't be enforced by a receiver, only assumed of a compliant peer, and real time-based pacing needs RTT/PTO estimation this module doesn't have yet (Phase 7). This is a coarse, time-independent stand-in that still stops a peer from forcing unbounded HKDF/AES-key-schedule re-derivation by cycling the key phase bit across many packets.
const packet_threshold = u64(3)
kPacketThreshold (RFC 9002 §6.1.1).
const time_threshold_numerator = i64(9)
kTimeThreshold = 9/8 (RFC 9002 §6.1.2), applied as an integer multiply-then-divide rather than a float to avoid floating-point drift in a value that gates a security-relevant timer.
const time_threshold_denominator = i64(8)
const persistent_congestion_threshold = i64(3)
kPersistentCongestionThreshold (RFC 9002 §7.6.2), in units of PTO.
const max_packet_number = (u64(1) << 62) - 1
max_packet_number is RFC 9000 §12.3's own stated limit: "if any packet number is exhausted, ... MUST close the connection" -- packet numbers are 62-bit unsigned values, so 2^62-1 is the largest one a sender may ever use; a sender reaching this limit must stop sending in this space (typically by closing the connection or migrating), not keep counting.
const qpack_max_dynamic_table_capacity_bytes = 64 * 1024 * 1024
qpack_max_dynamic_table_capacity_bytes is this implementation's own upper bound on a QPACK dynamic table capacity. RFC 9204 names no specific number here, but §7.4's rationale ("implementation needs to ensure large values... do not create security weaknesses") applies to capacity itself exactly as much as it does to individual field values -- 64 MiB is far beyond any realistic HTTP header table need.
const qpack_max_prefixed_int = u64(0x30x3fffffffffffffff) // 2^62 - 1
qpack_max_prefixed_int is this implementation's own decode limit for a QPACK prefixed integer (RFC 9204 §4.1.1 requires support up to and including 62 bits; §7.4 requires SOME implementation limit be chosen so that oversized values cannot create security weaknesses). Matches this project's own QUIC varint range (varint.v's max_varint) rather than picking an unrelated arbitrary bound.
const qpack_max_string_literal_len = 1 << 20
qpack_max_string_literal_len bounds a single decoded QPACK string literal's on-wire (encoded) length (RFC 9204 §7.4). 1 MiB is far larger than any real HTTP field value while still being a small, fixed cost for a malicious peer to force allocated.
const qpack_settings_max_table_capacity_id = u64(0x01)
qpack_settings_max_table_capacity_id is SETTINGS_QPACK_MAX_TABLE_CAPACITY (RFC 9204 §5, §8.1). It is the equivalent of HTTP/2's SETTINGS_HEADER_TABLE_SIZE.
const qpack_settings_blocked_streams_id = u64(0x07)
qpack_settings_blocked_streams_id is SETTINGS_QPACK_BLOCKED_STREAMS (RFC 9204 §5, §8.1).
const qpack_static_table = [
QpackStaticEntry{':authority', ''},
QpackStaticEntry{':path', '/'},
QpackStaticEntry{'age', '0'},
QpackStaticEntry{'content-disposition', ''},
QpackStaticEntry{'content-length', '0'},
QpackStaticEntry{'cookie', ''},
QpackStaticEntry{'date', ''},
QpackStaticEntry{'etag', ''},
QpackStaticEntry{'if-modified-since', ''},
QpackStaticEntry{'if-none-match', ''},
QpackStaticEntry{'last-modified', ''},
QpackStaticEntry{'link', ''},
QpackStaticEntry{'location', ''},
QpackStaticEntry{'referer', ''},
QpackStaticEntry{'set-cookie', ''},
QpackStaticEntry{':method', 'CONNECT'},
QpackStaticEntry{':method', 'DELETE'},
QpackStaticEntry{':method', 'GET'},
QpackStaticEntry{':method', 'HEAD'},
QpackStaticEntry{':method', 'OPTIONS'},
QpackStaticEntry{':method', 'POST'},
QpackStaticEntry{':method', 'PUT'},
QpackStaticEntry{':scheme', 'http'},
QpackStaticEntry{':scheme', 'https'},
QpackStaticEntry{':status', '103'},
QpackStaticEntry{':status', '200'},
QpackStaticEntry{':status', '304'},
QpackStaticEntry{':status', '404'},
QpackStaticEntry{':status', '503'},
QpackStaticEntry{'accept', '*/*'},
QpackStaticEntry{'accept', 'application/dns-message'},
QpackStaticEntry{'accept-encoding', 'gzip, deflate, br'},
QpackStaticEntry{'accept-ranges', 'bytes'},
QpackStaticEntry{'access-control-allow-headers', 'cache-control'},
QpackStaticEntry{'access-control-allow-headers', 'content-type'},
QpackStaticEntry{'access-control-allow-origin', '*'},
QpackStaticEntry{'cache-control', 'max-age=0'},
QpackStaticEntry{'cache-control', 'max-age=2592000'},
QpackStaticEntry{'cache-control', 'max-age=604800'},
QpackStaticEntry{'cache-control', 'no-cache'},
QpackStaticEntry{'cache-control', 'no-store'},
QpackStaticEntry{'cache-control', 'public, max-age=31536000'},
QpackStaticEntry{'content-encoding', 'br'},
QpackStaticEntry{'content-encoding', 'gzip'},
QpackStaticEntry{'content-type', 'application/dns-message'},
QpackStaticEntry{'content-type', 'application/javascript'},
QpackStaticEntry{'content-type', 'application/json'},
QpackStaticEntry{'content-type', 'application/x-www-form-urlencoded'},
QpackStaticEntry{'content-type', 'image/gif'},
QpackStaticEntry{'content-type', 'image/jpeg'},
QpackStaticEntry{'content-type', 'image/png'},
QpackStaticEntry{'content-type', 'text/css'},
QpackStaticEntry{'content-type', 'text/html; charset=utf-8'},
QpackStaticEntry{'content-type', 'text/plain'},
QpackStaticEntry{'content-type', 'text/plain;charset=utf-8'},
QpackStaticEntry{'range', 'bytes=0-'},
QpackStaticEntry{'strict-transport-security', 'max-age=31536000'},
QpackStaticEntry{'strict-transport-security', 'max-age=31536000; includesubdomains'},
QpackStaticEntry{'strict-transport-security', 'max-age=31536000; includesubdomains; preload'},
QpackStaticEntry{'vary', 'accept-encoding'},
QpackStaticEntry{'vary', 'origin'},
QpackStaticEntry{'x-content-type-options', 'nosniff'},
QpackStaticEntry{'x-xss-protection', '1; mode=block'},
QpackStaticEntry{':status', '100'},
QpackStaticEntry{':status', '204'},
QpackStaticEntry{':status', '206'},
QpackStaticEntry{':status', '302'},
QpackStaticEntry{':status', '400'},
QpackStaticEntry{':status', '403'},
QpackStaticEntry{':status', '421'},
QpackStaticEntry{':status', '425'},
QpackStaticEntry{':status', '500'},
QpackStaticEntry{'accept-language', ''},
QpackStaticEntry{'access-control-allow-credentials', 'FALSE'},
QpackStaticEntry{'access-control-allow-credentials', 'TRUE'},
QpackStaticEntry{'access-control-allow-headers', '*'},
QpackStaticEntry{'access-control-allow-methods', 'get'},
QpackStaticEntry{'access-control-allow-methods', 'get, post, options'},
QpackStaticEntry{'access-control-allow-methods', 'options'},
QpackStaticEntry{'access-control-expose-headers', 'content-length'},
QpackStaticEntry{'access-control-request-headers', 'content-type'},
QpackStaticEntry{'access-control-request-method', 'get'},
QpackStaticEntry{'access-control-request-method', 'post'},
QpackStaticEntry{'alt-svc', 'clear'},
QpackStaticEntry{'authorization', ''},
QpackStaticEntry{'content-security-policy', 'script-src 'none'; object-src 'none'; base-uri 'none''},
QpackStaticEntry{'early-data', '1'},
QpackStaticEntry{'expect-ct', ''},
QpackStaticEntry{'forwarded', ''},
QpackStaticEntry{'if-range', ''},
QpackStaticEntry{'origin', ''},
QpackStaticEntry{'purpose', 'prefetch'},
QpackStaticEntry{'server', ''},
QpackStaticEntry{'timing-allow-origin', '*'},
QpackStaticEntry{'upgrade-insecure-requests', '1'},
QpackStaticEntry{'user-agent', ''},
QpackStaticEntry{'x-forwarded-for', ''},
QpackStaticEntry{'x-frame-options', 'deny'},
QpackStaticEntry{'x-frame-options', 'sameorigin'},
]!
qpack_static_table is the 99-entry table from RFC 9204 Appendix A, transcribed directly from the fetched RFC text (not recalled from HPACK's differently-ordered, differently-sized 61-entry table by assumption -- QPACK's table is index-0-based and was independently regenerated from 2018 traffic analysis, so it shares only some rows with HPACK's, not all, and never at the same index).
const qpack_encoder_stream_type = u64(0x02)
qpack_encoder_stream_type is the Stream Type value that marks a QPACK encoder stream (RFC 9204 §4.2, §8.2). It shares HTTP/3's unidirectional Stream Type varint space (the same one h3_stream_type.v classifies 0x00/0x01 in) but is defined by RFC 9204, not RFC 9114 itself -- kept in its own file rather than added to h3_stream_type.v's classifier, mirroring how RFC 9204 §5's settings are recognized as "not reserved" by h3_frame.v without h3_frame.v itself interpreting them.
const qpack_decoder_stream_type = u64(0x03)
qpack_decoder_stream_type is the Stream Type value that marks a QPACK decoder stream (RFC 9204 §4.2, §8.2).
const retry_integrity_tag_len = 16
const initial_rtt = time.Duration(333 * time.millisecond)
kInitialRtt (RFC 9002 §5.3): the RTT estimate used before any real sample exists -- seeds smoothed_rtt so PTO computation (loss_detection.v) has a sane value even before a single packet has been acknowledged.
const granularity = time.Duration(1 * time.millisecond)
kGranularity (RFC 9002 §5.3): the system timer granularity assumed throughout loss detection -- both the time-threshold loss delay and the PTO calculation are floored at this value so an unrealistically small rttvar/smoothed_rtt can never produce a timer that fires more often than the assumed clock resolution.
const max_stream_buffered_bytes = u64(1) << 20
max_stream_buffered_bytes bounds how many bytes of BUFFERED-BUT-NOT-YET- CONSUMED data (the contiguous received window plus out-of-order pending fragments) a single StreamReassembler will ever hold at once -- not the stream's total size. discard() lets a caller (the future application-read path, Phase 9) free consumed bytes, sliding this window forward as the stream progresses, so a stream of any total size can be handled as long as the UNCONSUMED portion at any one moment stays under this ceiling. 1 MiB is generous for that window while still bounded, for the same DoS reason crypto_stream.v's own limit exists -- a peer sending arbitrarily far-future STREAM offsets could otherwise exhaust memory. Real flow control (flow_control.v) additionally caps outstanding unconsumed bytes via the advertised max_stream_data limit long before this ceiling would ever matter in practice -- this is a hard backstop, not the primary defense.
const max_stream_pending_fragments = 64
max_stream_pending_fragments mirrors max_crypto_stream_pending_fragments's own reasoning: bounds how many DISTINCT out-of-order fragments can accumulate, independent of the byte-range cap above.
const max_varint = u64(0x30x3FFFFFFFFFFFFFFF) // 2^62 - 1
QUIC variable-length integer encoding (RFC 9000 §16).
This is NOT the same encoding as vlib/encoding/leb128 — the two are bit-incompatible and must never be interchanged. LEB128 uses a continuation bit in every byte; QUIC's varint instead packs a 2-bit length class into the top bits of the FIRST byte:
0b00xxxxxx -> 1 byte, 6-bit value 0b01xxxxxx xxxxxxxx -> 2 bytes, 14-bit value 0b10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx -> 4 bytes, 30-bit value 0b11xxxxxx (+ 7 more bytes) -> 8 bytes, 62-bit value
The maximum representable value is 2^62 - 1.
fn build_client_hello #
fn build_client_hello(p ClientHelloParams) ![]u8
build_client_hello constructs a complete TLS 1.3 ClientHello handshake message (RFC 8446 §4.1.2), framed via encode_handshake_message. Sends exactly eight extensions: server_name, supported_versions, supported_groups, signature_algorithms, signature_algorithms_cert, alpn, key_share, and quic_transport_parameters (RFC 9001 §8.2) — order doesn't matter per RFC 8446 §4.2 ("extensions MAY appear in any order") except that pre_shared_key would have to be last, and v1 never sends one (no 0-RTT/resumption, Phase 14).
fn certificate_verify_signed_content #
fn certificate_verify_signed_content(role CertificateVerifyRole, transcript_hash []u8) []u8
certificate_verify_signed_content builds RFC 8446 §4.4.3's exact signed-content construction: 64 octets of 0x20, the role-specific context string, a single 0x00 separator byte, then Transcript-Hash(Handshake Context, Certificate). This 64-byte pad exists specifically to defeat a prior-TLS-version attack that obtained signatures over a chosen 32-byte prefix (RFC 8446 §4.4.3) — it is not arbitrary padding a future cleanup could shrink.
v1 only ever needs the server variant (client CertificateVerify is never sent — CertificateRequest is rejected outright, per PROGRESS.md); the client variant is included for API completeness with the RFC's own two-sided definition, at negligible cost, and to keep this function correct if Phase 13's server-role client-cert-auth support is ever added without needing to revisit this file.
fn classify_h3_unidirectional_stream_type #
fn classify_h3_unidirectional_stream_type(raw_type u64) H3UnidirectionalStreamKind
classify_h3_unidirectional_stream_type maps a raw Stream Type varint value to its H3UnidirectionalStreamKind per RFC 9114 §6.2/§6.2.1/§6.2.2/ §6.2.3. Exported separately from the header parser so callers (and tests) can classify a value without needing a full byte buffer.
fn classify_qpack_stream_type #
fn classify_qpack_stream_type(raw_type u64) ?QpackStreamKind
classify_qpack_stream_type maps a raw unidirectional Stream Type varint to a QpackStreamKind, if it is one of QPACK's own two types. Returns none for anything else (RFC 9114's own types, grease, or a genuinely unrecognized extension) -- a caller should try this classifier before falling back to classify_h3_unidirectional_stream_type, since 0x02/0x03 would otherwise be reported .unknown there (correct as far as RFC 9114 itself is concerned, but incomplete once QPACK is implemented).
fn compute_finished_verify_data #
fn compute_finished_verify_data(base_secret []u8, transcript_hash []u8) ![]u8
compute_finished_verify_data implements RFC 8446 §4.4.4's Finished message computation:
finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length) verify_data = HMAC(finished_key, Transcript-Hash(...))
base_secret is whichever side's traffic secret applies — the client's Finished uses client_handshake_traffic_secret, the server's uses server_handshake_traffic_secret (Phase 2b's HandshakeSecrets fields); this function is side-agnostic, the caller picks. transcript_hash must already cover exactly the messages up to but NOT including this Finished message itself (for the server's Finished: ClientHello... CertificateVerify*; for the client's Finished: ClientHello...server Finished).
fn compute_retry_integrity_tag #
fn compute_retry_integrity_tag(original_dcid []u8, retry_packet_without_tag []u8) ![]u8
compute_retry_integrity_tag computes the expected 16-byte Retry Integrity Tag (RFC 9001 §5.8) given the ORIGINAL destination connection ID the client used in the Initial packet that provoked this Retry (original_dcid -- NOT the Retry packet's own dcid field, a distinct value the caller must retain from before ever sending its first Initial packet) and the Retry packet's own bytes with the trailing 16-byte tag excluded.
fn decode_base #
fn decode_base(sign bool, delta_base u64, req_insert_count u64) !u64
decode_base resolves the Base from its Sign bit and Delta Base value (RFC 9204 §4.5.1.2). Rejects the case the RFC calls out explicitly: Sign bit 1 with Delta Base >= Required Insert Count would make Base negative, which MUST be treated as invalid (and, not incidentally, is exactly what would underflow the unsigned subtraction below).
fn decode_field_section_prefix #
fn decode_field_section_prefix(buf []u8, total_inserts u64, max_table_capacity u64) !(QpackFieldSectionPrefix, int)
decode_field_section_prefix decodes the two-integer prefix at the start of buf (RFC 9204 §4.5.1). Unlike this module's frame-level decoders, this returns a hard error (not none) on truncation: by the time this runs, buf is already a complete, length-delimited HEADERS frame payload (h3_frame.v's H3FrameDecoder only ever hands over a frame once its declared Length is fully buffered) -- so a short prefix here means the encoding itself is malformed, not merely "not arrived yet".
fn decode_h3_frame_payload #
fn decode_h3_frame_payload(frame_type u64, payload []u8) !H3Frame
decode_h3_frame_payload decodes payload (already known to be exactly frame_type's declared Length, per §7.1/§10.8 -- callers such as H3FrameDecoder are responsible for that slicing) into a concrete H3Frame. Returns an error, carrying an H3ErrorCode via error_with_code, for:- an H2-carryover reserved frame type (§7.2.8) -- H3_FRAME_UNEXPECTED;
- a SETTINGS frame whose payload is not an exact sequence of (identifier, value) varint pairs (§7.1's generic "terminates before the end of the identified fields" rule) -- H3_FRAME_ERROR;- a SETTINGS frame containing a duplicate identifier -- H3_SETTINGS_ERROR. RFC 9114 §7.2.4 states this as a MAY ("A receiver MAY treat the presence of duplicate setting identifiers as a connection error"), not a MUST; this implementation chooses to enforce it, matching the RFC's own stated rationale elsewhere in the document for preferring strict validation over permissiveness (§4.1.2: "deliberately strict because being permissive can expose implementations to [...] vulnerabilities"). Documented here as a deliberate implementation choice, not presented as a literal MUST;- a SETTINGS frame containing one of the 5 reserved HTTP/2-carryover setting identifiers (§7.2.4.1/§11.2.2 Table 3: 0x00, 0x02, 0x03, 0x04, 0x05) -- H3_SETTINGS_ERROR. Every other frame type's payload is a fixed small number of varints (CANCEL_PUSH/GOAWAY/MAX_PUSH_ID: one; PUSH_PROMISE: one varint + the remaining bytes) -- trailing or missing bytes there are likewise H3_FRAME_ERROR per §7.1's generic rule.
fn decode_packet_number #
fn decode_packet_number(truncated u64, pn_len int, largest_pn ?u64) !u64
decode_packet_number reconstructs the full packet number from its on-the-wire truncated form, given the largest packet number successfully processed so far in this space (RFC 9000 Appendix A.3, DecodePacketNumber). truncated must already be the numeric value of the pn_len-byte field (i.e. the header-protection mask must already have been removed and the bytes parsed as a big-endian integer of that length).
fn decode_preferred_address #
fn decode_preferred_address(buf []u8) !PreferredAddress
decode_preferred_address parses the fixed-format Figure 22 layout: a 25-byte fixed prefix (4-byte IPv4 address, 2-byte IPv4 port, 16-byte IPv6 address, 2-byte IPv6 port, 1-byte connection ID length), followed by the connection ID itself and a fixed 16-byte stateless reset token.
fn decode_prefixed_int #
fn decode_prefixed_int(buf []u8, prefix_bits int) !(u64, int)
decode_prefixed_int decodes an RFC 7541 §5.1 prefixed integer (reused unmodified by RFC 9204 §4.1.1) whose low prefix_bits bits of buf[0] carry the initial value, with any continuation bytes following. The caller owns whatever flag/pattern bits occupy the high 8-prefix_bits bits of that same first byte -- this function only ever reads the low bits and continuation bytes. Returns the decoded value and the number of bytes consumed.
fn decode_prefixed_string #
fn decode_prefixed_string(buf []u8, length_prefix_bits int) !(string, bool, int)
decode_prefixed_string decodes an RFC 7541 §5.2 string literal (reused, generalized to a mid-byte start, by RFC 9204 §4.1.2): a Huffman flag bit immediately above the length's own length_prefix_bits-bit prefix integer, followed by that many bytes of (possibly Huffman-coded) string data. length_prefix_bits is the number written in each wire figure's "Length (N+)" annotation (e.g. 7 for a byte-aligned value, 5 or 3 for a name sharing its first byte with preceding flag bits) -- the caller already knows this from which representation it is decoding, the same division of labor as decode_prefixed_int's prefix_bits. Returns the decoded string, whether it was Huffman-coded, and bytes consumed.
fn decode_qpack_decoder_instruction #
fn decode_qpack_decoder_instruction(buf []u8) !QpackDecodedDecoderInstruction
decode_qpack_decoder_instruction decodes ONE instruction starting at buf[0] (RFC 9204 Figures 9-11: 1...=Section-Ack, 01..=Stream- Cancellation, 00..=Insert-Count-Increment). Returns has_instruction: false (not an error) when buf does not yet hold a complete instruction -- same resumable-parsing contract as decode_qpack_encoder_instruction, including the same obligation to propagate (not swallow) a genuine malformed-data error rather than treating it as "not yet complete": this decoder-stream sibling had the identical swallow-everything bug (a ? return type with no error channel at all), fixed here for the same reason even though no production caller yet exists for it -- the same class of bug, not just the one instance a reviewer named.
fn decode_qpack_encoder_instruction #
fn decode_qpack_encoder_instruction(buf []u8) !QpackDecodedEncoderInstruction
decode_qpack_encoder_instruction decodes ONE instruction starting at buf[0], distinguishing the 4 types by their leading bit pattern (RFC 9204 Figures 5-8, checked in the order that lets each subsequent branch assume the higher bits already ruled out: 1...=Insert-with-name-ref, 01..=Insert-with-literal-name, 001.=Set-capacity, 000.=Duplicate). Returns has_instruction: false (not an error) when buf does not yet hold a complete instruction: encoder-stream data can arrive fragmented across QUIC STREAM frames the same as HTTP/3 frames do, so this mirrors H3FrameDecoder's "need more data" contract rather than treating a short read as invalid. Returns a real error -- which the caller MUST propagate, not treat as "not yet complete" -- for genuinely malformed data (an oversized integer, invalid Huffman coding): conflating the two previously let a malformed encoder-stream instruction be silently retried forever instead of raising QPACK_ENCODER_STREAM_ERROR and closing the connection (RFC 9204 §2.2.3, §3.2.2). Self-found via GPT-5.6 Luna review, Phase-R reproduced before this fix. Uses the shared qpack_err_need_more_data sentinel (qpack_primitives.v) to tell the two cases apart at every decode_prefixed_int/decode_prefixed_string call site below.
fn decode_qpack_field_line #
fn decode_qpack_field_line(buf []u8, base u64, dynamic_table &QpackDynamicTable) !QpackDecodedFieldLine
decode_qpack_field_line decodes ONE field line representation starting at buf[0] (RFC 9204 §4.5.2-§4.5.6), resolving any static or dynamic table reference. base must already be known (from decode_field_section_prefix). Same "already-complete buffer" contract as decode_field_section_prefix -- errors, does not return none, on truncation.
fn decode_ric #
fn decode_ric(encoded_insert_count u64, total_inserts u64, max_table_capacity u64) !u64
decode_ric reconstructs the Required Insert Count from its wire encoding (RFC 9204 §4.5.1.1), given the decoder's own current total insert count and configured max table capacity. Transcribed directly from the RFC's own decoder pseudocode, including its error branches: a decoder MUST treat an EncodedInsertCount value no conformant encoder could have produced as QPACK_DECOMPRESSION_FAILED.
fn decode_transport_parameters #
fn decode_transport_parameters(buf []u8) !QuicTransportParameters
decode_transport_parameters parses a full Transport Parameters sequence (RFC 9000 §18, Figure 20). Per RFC 9000 §7.4.2 ("An endpoint MUST ignore transport parameters that it does not support"), an unrecognized parameter ID is skipped, not rejected — this is also how RFC 9000 §18.1's reserved "31*N+27" grease IDs are exercised, with no special-casing needed beyond the general unknown-ID skip. ack_delay_exponent, max_udp_payload_size, max_ack_delay, and active_connection_id_limit are validated against RFC 9000 §18.2's own stated bounds.
A duplicate parameter ID is rejected outright, even though RFC 9000's prose for §18.2 does not spell this out explicitly as a MUST — this mirrors established QUIC implementation practice and this project's own "singleton wire field extracted in a loop" lesson: silently letting the last occurrence win is exactly the failure mode that class of bug warns about, for no compensating benefit (a conforming peer never sends the same parameter twice).
fn decode_varint #
fn decode_varint(buf []u8) !(u64, int)
decode_varint decodes a QUIC variable-length integer from the start of buf, returning the decoded value and the number of bytes consumed. Accepts all four legal length classes for a decoded value, even when a shorter class could have encoded the same value: RFC 9000 §16 lets an encoder choose any of the four length classes a value fits in (encoders SHOULD use the smallest, matching this file's own encode_varint, which always does), but does not require -- and real, otherwise-conforming peers are not guaranteed -- a minimal encoding on the wire. A previous version of this function rejected non-minimal encodings outright (Codex finding, vlang/v#27680 pullrequestreview-4781706846); that treated a spec-legal encoding as a parse error, wrongly failing the handshake for a compliant peer that (for whatever reason) chose a longer-than-minimal form for a value used anywhere a varint appears -- packet lengths, tokens, transport parameters, ACK ranges, and more. Truncated input is still rejected.
fn decrypt_packet_payload #
fn decrypt_packet_payload(keys QuicPacketProtectionKeys, packet_number u64, header []u8, ciphertext []u8) ![]u8
decrypt_packet_payload AEAD-decrypts and authenticates one packet's payload. On authentication failure, callers MUST silently drop the packet rather than tear down the connection: a single AEAD failure is indistinguishable from ordinary network corruption or off-path garbage UDP data, and RFC 9001's security guidance is that it must never, by itself, be escalated to a connection close. This function only reports the failure as an error; enforcing "drop, don't close" is the caller's (Phase 4+ packet-receive loop's) responsibility.
fn derive_application_secrets #
fn derive_application_secrets(handshake_secret []u8, transcript_hash_ch_sfin []u8) !ApplicationSecrets
derive_application_secrets computes the Master Secret and both application traffic secrets (RFC 8446 §7.1) from the Handshake Secret and Transcript-Hash(ClientHello...server Finished).
exporter_master_secret and resumption_master_secret are intentionally NOT computed here: v1 uses neither TLS exporters (QUIC derives its packet-protection keys directly from the traffic secrets returned here, via hkdf_expand_label with the "quic key"/"quic iv"/"quic hp" labels, never through the generic TLS exporter interface) nor session resumption (0-RTT is Phase 14, out of committed scope). Adding either output now would be untested, unused surface area.
fn derive_early_secret #
fn derive_early_secret() ![]u8
derive_early_secret returns the TLS 1.3 Early Secret (RFC 8446 §7.1). v1 has no PSK/session resumption (0-RTT is Phase 14, out of committed scope), so this always takes the spec's no-PSK branch: both the HKDF-Extract salt and IKM are absent inputs, each replaced per §7.1's rule ("If a given secret is not available, then the 0-value ... is used") with a string of Hash.length zero bytes. The salt's zero value is supplied implicitly (hkdf.extract defaults an empty salt to Hash.length zero bytes); the IKM's zero value must be passed explicitly, since HKDF-Extract does not treat an empty IKM the same as a zero IKM.
This step is still computed for real, not shortcut to a hardcoded zero Early Secret, because its OUTPUT feeds the "derived" Derive-Secret call that chains into the Handshake Secret below — skipping the computation would assume, rather than prove, that shortcut is equivalent.
fn derive_handshake_secrets #
fn derive_handshake_secrets(early_secret []u8, ecdhe_shared_secret []u8, transcript_hash_ch_sh []u8) !HandshakeSecrets
derive_handshake_secrets computes the Handshake Secret and both handshake traffic secrets (RFC 8446 §7.1) from the Early Secret, the (EC)DHE shared secret, and Transcript-Hash(ClientHello...ServerHello).
fn derive_initial_secrets #
fn derive_initial_secrets(client_dcid []u8) !InitialSecrets
derive_initial_secrets computes the QUIC v1 Initial packet protection secrets (RFC 9001 §5.2) from a connection's client-chosen Destination Connection ID.
Callers MUST key this off whatever DCID the client is CURRENTLY using on the wire for its Initial packets ("client_dst_connection_id" in RFC 9001 §5.2's own terms) -- NOT a fixed "original" value retained across the connection's whole lifetime. RFC 9001 §5.2 is explicit: "The secrets used for constructing subsequent Initial packets change when a server sends a Retry packet to use the connection ID value selected by the server." Concretely: before any Retry, client_dcid is whatever DCID the client itself randomly chose for its first Initial packet; after a Retry, the client switches its wire DCID to the Retry packet's Source Connection ID (RFC 9000 §17.2.5), and Initial secrets MUST be RE-DERIVED from that new value -- a server deriving its own Initial keys from the DCID it actually received would otherwise compute different keys than a client still using its pre-Retry secrets, making every post-Retry Initial packet undecryptable to a conforming peer. This is a previously-incorrect claim in this doc comment itself (Codex finding, vlang/v#27680 pullrequestreview-4781706846), verified against the RFC 9001 §5.2 text directly, not just re-derived from memory.
Do NOT confuse this with the SEPARATE, unrelated original_destination_connection_id transport parameter (RFC 9000 §7.3) -- that is purely an anti-tampering AUTHENTICATION value the server echoes back for the client to verify, carrying no bearing on which secrets protect which packets. This function itself is DCID-agnostic (it derives from whatever client_dcid it's given); it is the caller's responsibility (Phase 9's QuicConn) to track the CURRENT wire DCID and call this again whenever a Retry changes it.
fn derive_packet_protection_keys #
fn derive_packet_protection_keys(secret []u8) !QuicPacketProtectionKeys
derive_packet_protection_keys computes quic_key/quic_iv/quic_hp (RFC 9001 §5.1) from one encryption level's one-directional traffic secret (an Initial secret, a handshake traffic secret, or an application traffic secret — this function is level-agnostic, since HKDF-Expand-Label's "quic key"/"quic iv"/"quic hp" labels are identical at every level).
fn derive_secret #
fn derive_secret(secret []u8, label string, transcript_hash []u8) ![]u8
derive_secret implements RFC 8446 §7.1's Derive-Secret(Secret, Label, Messages):
Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
The caller supplies the already-computed Transcript-Hash(Messages) directly. Accumulating raw handshake message bytes into a running hash — and, per RFC 8446 §4.4.1, covering only the handshake message bytes themselves with no QUIC/TLS record-layer framing — is Phase 2c's job (ClientHello...Finished), not this function's.
fn derive_updated_packet_protection_keys #
fn derive_updated_packet_protection_keys(next_secret []u8, current_hp []u8) !QuicPacketProtectionKeys
derive_updated_packet_protection_keys computes the key/IV for a NEW generation produced by a 1-RTT key update (RFC 9001 §6.1's next_secret), while carrying current_hp forward UNCHANGED. RFC 9001 §6 is explicit that a key update rotates only the packet-protection key and IV -- the header-protection key is derived once, at the start of the encryption level, and never updates within it (using key_update.v's own derive_updated_secret to also produce a fresh hp would desync this side's header-protection key from what a compliant peer keeps, since the peer never rotates its own).
fn derive_updated_secret #
fn derive_updated_secret(current_secret []u8) ![]u8
derive_updated_secret computes the NEXT traffic secret from the current one (RFC 9001 §6.1). The same derivation applies independently to a connection's client_secret and server_secret -- each side's traffic secret chain advances on its own schedule, which is exactly why this function takes a bare secret rather than anything connection-scoped.
The output is always exactly sha256.size bytes -- RFC 9001 §6.1 defines this as the negotiated TLS hash length, NOT a function of the input's own length. v1 is pinned to TLS_AES_128_GCM_SHA256 (see tls13_client_hello.v), so that length is always sha256.size, matching every other traffic-secret derivation in this module (initial_secrets.v, tls13_keyschedule.v). Trusting current_secret.len instead would let a wrong-length input silently produce a wrong-length "updated" secret that no compliant peer would ever derive, rather than surfacing the mismatch as an error.
fn dial #
fn dial(params DialParams, now u64) !(&QuicConn, QuicDatagram)
dial starts a new client connection attempt: picks this endpoint's own connection IDs, derives Initial secrets, builds the ClientHello, and returns the connection object plus the first outgoing datagram (a padded Initial packet) the caller must send.
fn effective_idle_timeout #
fn effective_idle_timeout(local_max_idle_timeout_ms u64, peer_max_idle_timeout_ms u64) ?time.Duration
effective_idle_timeout resolves RFC 9000 §10.1's min-of-non-zero rule. none return means the connection has no idle timeout whatsoever.
fn empty_transcript_hash #
fn empty_transcript_hash() []u8
empty_transcript_hash returns Transcript-Hash(""), used for RFC 8446 §7.1's two "derived" chaining steps (Early Secret -> Handshake Secret, Handshake Secret -> Master Secret). Both are defined over an EMPTY Messages input, not over any real handshake message — this is the spec's fixed value for that case, not a placeholder standing in for one.
fn encode_ack_frame #
fn encode_ack_frame(ranges []AckRange, ack_delay u64, ecn_counts ?EcnCounts) ![]u8
encode_ack_frame serializes an ACK frame from its already-resolved ranges (largest-first, non-overlapping, matching AckFrame.ranges' shape), deriving the wire's Largest Acknowledged / First ACK Range / Gap / ACK Range Length encoding.
fn encode_base #
fn encode_base(base u64, req_insert_count u64) (bool, u64)
encode_base computes the Sign bit and Delta Base value for a given Base and Required Insert Count (RFC 9204 §4.5.1.2, the encoder's side of the same relationship decode_base reverses).
fn encode_cancel_push_frame #
fn encode_cancel_push_frame(push_id u64) ![]u8
encode_cancel_push_frame encodes a CANCEL_PUSH frame (§7.2.3).
fn encode_connection_close_frame #
fn encode_connection_close_frame(is_application_error bool, error_code u64, frame_type u64, reason string) ![]u8
encode_connection_close_frame serializes a CONNECTION_CLOSE frame. frame_type is ignored (the Frame Type field is OMITTED from the wire entirely, not encoded as a zero value) when is_application_error is true, matching the application-level variant's wire shape (RFC 9000 §19.19, second form) -- a decoder parsing this back sees no such field on the wire either, which is why parse_frame's own ConnectionCloseFrame.frame_type defaults to 0 for that variant, rather than reading a zero varint that was never sent.
fn encode_crypto_frame #
fn encode_crypto_frame(offset u64, data []u8) ![]u8
encode_crypto_frame serializes a CRYPTO frame.
fn encode_data_blocked_frame #
fn encode_data_blocked_frame(maximum_data u64) ![]u8
encode_data_blocked_frame serializes a DATA_BLOCKED frame.
fn encode_data_frame #
fn encode_data_frame(data []u8) ![]u8
encode_data_frame encodes a DATA frame (§7.2.1).
fn encode_field_section_prefix #
fn encode_field_section_prefix(req_insert_count u64, base u64, max_table_capacity u64) ![]u8
encode_field_section_prefix encodes the Required Insert Count + Base prefix for an encoded field section (RFC 9204 §4.5.1). Errors if req_insert_count is nonzero and max_table_capacity is too small to represent it (see encode_ric).
fn encode_goaway_frame #
fn encode_goaway_frame(id u64) ![]u8
encode_goaway_frame encodes a GOAWAY frame (§7.2.6). See GoawayFrame's doc comment for the direction-dependent meaning of id.
fn encode_h3_control_stream_header #
fn encode_h3_control_stream_header() ![]u8
encode_h3_control_stream_header returns the single-varint header a control stream must send as its first bytes (RFC 9114 §6.2.1).
fn encode_h3_push_stream_header #
fn encode_h3_push_stream_header(push_id u64) ![]u8
encode_h3_push_stream_header returns the Stream Type + Push ID header a push stream must send as its first bytes (RFC 9114 §6.2.2, Figure 2). push_id must already satisfy RFC 9000 §16's varint range (2^62-1); encode_varint enforces that and returns an error otherwise.
fn encode_handshake_message #
fn encode_handshake_message(typ HandshakeType, body []u8) ![]u8
encode_handshake_message wraps body in the standard TLS 1.3 handshake message header (RFC 8446 §4): a 1-byte HandshakeType followed by a 3-byte big-endian length. This framing is identical to the synthetic message_hash record Phase 2b's synthetic_client_hello1_hash builds by hand for the HelloRetryRequest transcript rule; that call site is not migrated to use this function, since encode_handshake_message requires a real HandshakeType and message_hash's synthetic record is deliberately NOT a real handshake message being framed for transmission.
fn encode_headers_frame #
fn encode_headers_frame(encoded_field_section []u8) ![]u8
encode_headers_frame encodes a HEADERS frame (§7.2.2) around an already-QPACK-encoded field section (QPACK encoding is Phase 11's responsibility, not this function's).
fn encode_indexed_dynamic_post_base #
fn encode_indexed_dynamic_post_base(post_index u64) []u8
encode_indexed_dynamic_post_base encodes an Indexed Field Line with Post-Base Index (RFC 9204 §4.5.3, Figure 14: 0001 + 4-bit prefix index).
fn encode_indexed_dynamic_relative #
fn encode_indexed_dynamic_relative(rel_index u64) []u8
encode_indexed_dynamic_relative encodes an Indexed Field Line referencing the dynamic table by Base-relative index (RFC 9204 §4.5.2, Figure 13: 1 0 + 6-bit prefix index).
fn encode_indexed_static #
fn encode_indexed_static(index u64) []u8
encode_indexed_static encodes an Indexed Field Line referencing the static table (RFC 9204 §4.5.2, Figure 13: 1 1 + 6-bit prefix index).
fn encode_literal_with_literal_name #
fn encode_literal_with_literal_name(never_index bool, name string, value string) []u8
encode_literal_with_literal_name encodes a Literal Field Line with Literal Name (RFC 9204 §4.5.6, Figure 17: 001 + N + H + 3-bit prefix name length/string + value string literal).
fn encode_literal_with_name_ref #
fn encode_literal_with_name_ref(is_static bool, never_index bool, name_index u64, value string) []u8
encode_literal_with_name_ref encodes a Literal Field Line with Name Reference (RFC 9204 §4.5.4, Figure 15: 01 + N + T + 4-bit prefix name index + value string literal). name_index must already be resolved to the correct context (static index, or Base-relative dynamic index).
fn encode_literal_with_post_base_name_ref #
fn encode_literal_with_post_base_name_ref(never_index bool, post_index u64, value string) []u8
encode_literal_with_post_base_name_ref encodes a Literal Field Line with Post-Base Name Reference (RFC 9204 §4.5.5, Figure 16: 0000 + N + 3-bit prefix post-Base name index + value string literal).
fn encode_long_header #
fn encode_long_header(h QuicLongHeader, reserved_bits u8, pn_length_bits u8) ![]u8
encode_long_header serializes a long header's unprotected portion. The packet-number-length bits (first byte, low 2 bits) and the reserved bits (first byte, bits 2-3) are passed in explicitly because they belong to the header-protected region — the caller (Phase 3's packet writer) is responsible for having already decided the packet number's encoded length before calling this, since length (a varint covering packet-number + payload bytes) must be sized to include it. This two-pass dependency (packet number length must be known before the Length field can be finalized, but the Length field precedes the packet number on the wire) is the caller's responsibility to sequence correctly, not something this function can resolve on its own.
fn encode_max_data_frame #
fn encode_max_data_frame(maximum_data u64) ![]u8
encode_max_data_frame serializes a MAX_DATA frame.
fn encode_max_push_id_frame #
fn encode_max_push_id_frame(push_id u64) ![]u8
encode_max_push_id_frame encodes a MAX_PUSH_ID frame (§7.2.7).
fn encode_max_stream_data_frame #
fn encode_max_stream_data_frame(stream_id u64, maximum_stream_data u64) ![]u8
encode_max_stream_data_frame serializes a MAX_STREAM_DATA frame.
fn encode_max_streams_frame #
fn encode_max_streams_frame(direction StreamDirection, maximum_streams u64) ![]u8
encode_max_streams_frame serializes a MAX_STREAMS frame.
fn encode_packet_number #
fn encode_packet_number(full_pn u64, largest_acked ?u64) !([]u8, int)
encode_packet_number picks the smallest encoding (1, 2, 3, or 4 bytes) for full_pn such that it can be unambiguously reconstructed given largest_acked (the largest packet number acknowledged so far in this space, or none if nothing has been acknowledged yet). Returns the truncated bytes and the number of bytes used.
Per RFC 9000 §17.1, the sender MUST use a packet number encoding that can represent more than twice as large a range as the difference between the packet number being sent and the largest acknowledged packet.
Rejects full_pn > max_packet_number (RFC 9000 §12.3) rather than silently truncating it -- an un-checked caller could otherwise reach here with an exhausted counter and get back a corrupted, wrapped-around encoding (e.g. 2^62 truncates to a 4-byte field of all zeros) instead of the caller finding out its connection needs to stop sending. Also guards the (num_unacked + 1) * 2 sizing arithmetic below, which would itself overflow for num_unacked values near u64 max.
fn encode_preferred_address #
fn encode_preferred_address(pa PreferredAddress) ![]u8
encode_preferred_address serializes the fixed-format Figure 22 layout. RFC 9000 §18.2: "A server MUST NOT include a zero-length connection ID in this transport parameter" — enforced here so a malformed PreferredAddress can never be silently encoded.
fn encode_prefixed_int #
fn encode_prefixed_int(mut out []u8, value u64, prefix_bits int, high_bits u8)
encode_prefixed_int appends an RFC 7541 §5.1 prefixed integer to out, using a prefix of prefix_bits bits whose high bits are pre-set via high_bits (the caller's pattern/flag bits, already shifted into position -- mirrors h2_hpack_write_int's signature).
fn encode_prefixed_string #
fn encode_prefixed_string(mut out []u8, s string, length_prefix_bits int, high_bits u8)
encode_prefixed_string appends an RFC 7541 §5.2 string literal to out (RFC 9204 §4.1.2), choosing Huffman coding when it is shorter than the raw bytes, using an length_prefix_bits-bit length prefix (see decode_prefixed_string for what that width means). high_bits carries any preceding flag/pattern bits the caller needs set in the same first byte, NOT including the Huffman flag itself (this function sets that bit on its own, at position length_prefix_bits).
fn encode_qpack_duplicate #
fn encode_qpack_duplicate(rel_index u64) []u8
encode_qpack_duplicate encodes a Duplicate instruction (RFC 9204 §4.3.4, Figure 8: 000 + 5-bit prefix relative index).
fn encode_qpack_insert_count_increment #
fn encode_qpack_insert_count_increment(increment u64) []u8
encode_qpack_insert_count_increment encodes an Insert Count Increment instruction (RFC 9204 §4.4.3, Figure 11: 00 + 6-bit prefix increment).
fn encode_qpack_insert_with_literal_name #
fn encode_qpack_insert_with_literal_name(name string, value string) []u8
encode_qpack_insert_with_literal_name encodes an Insert With Literal Name instruction (RFC 9204 §4.3.3, Figure 7: 01 + name as a 6-bit prefix string literal + value as an 8-bit prefix string literal).
fn encode_qpack_insert_with_name_ref #
fn encode_qpack_insert_with_name_ref(is_static bool, name_index u64, value string) []u8
encode_qpack_insert_with_name_ref encodes an Insert With Name Reference instruction (RFC 9204 §4.3.2, Figure 6: 1 + T + 6-bit prefix name index + value string literal). name_index must already be resolved to the correct context (static index, or encoder-instruction-relative dynamic index) by the caller.
fn encode_qpack_section_ack #
fn encode_qpack_section_ack(stream_id u64) []u8
encode_qpack_section_ack encodes a Section Acknowledgment instruction (RFC 9204 §4.4.1, Figure 9: 1 + 7-bit prefix stream ID).
fn encode_qpack_set_dynamic_table_capacity #
fn encode_qpack_set_dynamic_table_capacity(capacity u64) []u8
encode_qpack_set_dynamic_table_capacity encodes a Set Dynamic Table Capacity instruction (RFC 9204 §4.3.1, Figure 5: 001 + 5-bit prefix).
fn encode_qpack_stream_cancellation #
fn encode_qpack_stream_cancellation(stream_id u64) []u8
encode_qpack_stream_cancellation encodes a Stream Cancellation instruction (RFC 9204 §4.4.2, Figure 10: 01 + 6-bit prefix stream ID).
fn encode_reset_stream_frame #
fn encode_reset_stream_frame(stream_id u64, error_code u64, final_size u64) ![]u8
encode_reset_stream_frame serializes a RESET_STREAM frame.
fn encode_ric #
fn encode_ric(req_insert_count u64, max_table_capacity u64) !u64
encode_ric transforms a Required Insert Count into its wire encoding (RFC 9204 §4.5.1.1), transcribed directly from the RFC's own pseudocode. Errors if max_table_capacity is too small to ever hold an entry (< 32 bytes, RFC 9204's own per-entry overhead) -- mirrors decode_ric's own full_range == 0 guard just below, which this function used to lack, dividing by zero instead (self-found via Luna review).
fn encode_settings_frame #
fn encode_settings_frame(settings []H3Setting) ![]u8
encode_settings_frame encodes a SETTINGS frame (§7.2.4) from an ordered list of settings. Does not itself enforce the duplicate-identifier or reserved-identifier rules decode_h3_settings_payload checks on receipt -- a sender controls its own output and is trusted not to construct an invalid frame; validating here too would only be useful for catching an internal bug, not a wire-format concern.
fn encode_short_header #
fn encode_short_header(dcid []u8, spin_bit bool, reserved_bits u8, key_phase bool, pn_length_bits u8) ![]u8
encode_short_header serializes a short header's unprotected portion (RFC 9000 §17.3.1), everything up to (but not including) the packet number field -- the caller appends the packet-number bytes separately and applies header protection afterward, exactly mirroring encode_long_header's own two-step convention (see its doc comment for why the packet-number length must be decided before this is called). Unlike a long header, there is no DCID length prefix on the wire (see QuicShortHeader's own doc comment) -- dcid is written verbatim.
fn encode_stop_sending_frame #
fn encode_stop_sending_frame(stream_id u64, error_code u64) ![]u8
encode_stop_sending_frame serializes a STOP_SENDING frame.
fn encode_stream_data_blocked_frame #
fn encode_stream_data_blocked_frame(stream_id u64, maximum_stream_data u64) ![]u8
encode_stream_data_blocked_frame serializes a STREAM_DATA_BLOCKED frame.
fn encode_stream_frame #
fn encode_stream_frame(stream_id u64, offset u64, data []u8, fin bool, include_length bool) ![]u8
encode_stream_frame serializes a STREAM frame. The OFF bit is included automatically whenever offset != 0 (never needed for 0, and always correct to include when nonzero); include_length, however, is a genuine caller decision -- omitting the LEN field means this frame MUST be the last one in its packet (RFC 9000 §19.8), a packet-layout choice stream.v/flow_control.v's caller makes, not something inferable from the frame's own fields alone.
fn encode_streams_blocked_frame #
fn encode_streams_blocked_frame(direction StreamDirection, maximum_streams u64) ![]u8
encode_streams_blocked_frame serializes a STREAMS_BLOCKED frame.
fn encode_transport_parameters #
fn encode_transport_parameters(p QuicTransportParameters) ![]u8
encode_transport_parameters serializes every present field as an (ID, Length, Value) tuple (RFC 9000 §18, Figure 21). Absent (none) Optional fields and a false disable_active_migration are simply omitted — both omission and explicit-default-value encoding are spec-equivalent on decode, so there is no need to special-case "equals the default" here.
fn encode_varint #
fn encode_varint(value u64) ![]u8
encode_varint encodes value as a QUIC variable-length integer, using the smallest length class that fits (RFC 9000 §16 requires minimal encoding).
fn encrypt_packet_payload #
fn encrypt_packet_payload(keys QuicPacketProtectionKeys, packet_number u64, header []u8, payload []u8) ![]u8
encrypt_packet_payload AEAD-encrypts one packet's payload (RFC 9001 §5.3). header must be the ENTIRE unprotected header, up to and including the plaintext packet number bytes — it is the AEAD associated data in full, not just a prefix of it. packet_number must already be the full, reconstructed value (see packet_protection_nonce).
fn find_extension #
fn find_extension(extensions []TlsExtension, typ u16) ?TlsExtension
find_extension returns the first extension in extensions matching typ, or none if no such extension is present.
fn first_stream_id_of #
fn first_stream_id_of(initiator StreamInitiator, direction StreamDirection) u64
first_stream_id_of returns the lowest (first-allocated) stream ID for a given (initiator, direction) category -- 0, 1, 2, 3 respectively for client-bidi, server-bidi, client-uni, server-uni. Successive streams in the same category are always exactly +4 from the previous one (RFC 9000 §2.1).
fn fits_within_pmtu #
fn fits_within_pmtu(datagram_len int) bool
fits_within_pmtu reports whether datagram_len is a legal size for this endpoint to send, given v1's pinned (non-probing) PMTU.
fn goaway_id_is_valid_client_initiated_bidi_stream_id #
fn goaway_id_is_valid_client_initiated_bidi_stream_id(id u64) bool
goaway_id_is_valid_client_initiated_bidi_stream_id reports whether id is legal as the stream-ID-flavored form of a GOAWAY frame (server-to-client direction): RFC 9000 §2.1 encodes a client-initiated bidirectional stream ID as id%4==0. RFC 9114 §7.2.6: "A client MUST treat receipt of a GOAWAY frame containing a stream ID of any other type as a connection error of type H3_ID_ERROR." This is a pure, role-independent predicate; the actual connection-error action belongs to Phase 12's wiring, once a real connection can be role-aware about which GOAWAY variant it expects.
fn handle_version_negotiation #
fn handle_version_negotiation(vn QuicVersionNegotiation, original_dcid []u8, original_scid []u8, already_processed_other_packet bool) !
handle_version_negotiation inspects a parsed Version Negotiation packet against quic_v1, the only version this module implements or ever offers. VN packets are UNAUTHENTICATED (sent before any keys exist), so RFC 9000 §6.2 draws a sharp line between discarding one and treating it as terminal -- a caller must not conflate them into one "VN always aborts" policy. ANY ONE of the following makes this function discard silently (return success, caller continues the existing connection attempt as if the packet had never arrived) rather than fail the connection attempt:
already_processed_other_packetis true: RFC 9000 §6.2, first sentence -- "A client MUST discard any Version Negotiation packet if it has received and successfully processed any other packet, including an earlier Version Negotiation packet." Once ANY other packet (Initial, Handshake, or an earlier VN) has been successfully processed, the connection attempt is already past the point where a VN packet could be legitimate -- tracking this fact is connection- lifecycle state the caller (Phase 9 QuicConn) must supply, since this function is a stateless verification primitive.- The VN packet's connection IDs don't echo this client's own Initial (RFC 9000 §17.2.1: "The server MUST include the value from the Source Connection ID field of the packet it receives in the Destination Connection ID field. The value for Source Connection ID MUST be copied from the Destination Connection ID of the received packet... Echoing BOTH connection IDs gives clients some assurance that the server received the packet and that the Version Negotiation packet was not generated by an entity that did not observe the Initial packet."). BOTH directions are checked: the VN packet's DCID must equal this client's own SCID, AND the VN packet's SCID must equal this client's own DCID -- mirroring retry.v's analogous two-sided echo validation for Retry packets.- The server's offered-version list INCLUDES v1: RFC 9000 §6.2, second sentence -- "A client MUST discard a Version Negotiation packet that lists the QUIC version selected by the client." This is the exact shape a spoofed, off-path-injected VN packet would have (an attacker with no visibility into the real server's actual (non-)response cannot know whether v1 will be listed, but a client that abandons its attempt on ANY VN packet turns this into a trivial connection- kill primitive for such an attacker).
If none of the above hold and the list does NOT include v1: the normal, genuinely terminal case -- this client has no other version to fall back to, so the connection attempt fails cleanly. This is the only outcome that returns an error.
fn handshake_type_from_u8 #
fn handshake_type_from_u8(b u8) !HandshakeType
handshake_type_from_u8 validates a wire byte against the known TLS 1.3 HandshakeType set (RFC 8446 §B.3), rather than blindly casting it to HandshakeType — an arbitrary u8 does not correspond to a valid variant for most of the 0..255 range, and a cast alone would not catch that.
fn hkdf_expand_label #
fn hkdf_expand_label(secret []u8, label string, context []u8, length int) ![]u8
hkdf_expand_label implements TLS 1.3's HKDF-Expand-Label (RFC 8446 §7.1). It is the single derivation primitive reused throughout the QUIC-TLS key schedule (RFC 9001 §5): Initial secrets here, the full Early/Handshake/ Master secret chain in Phase 2b, and per-level quic_key/quic_iv/quic_hp in Phase 3. secret must already be a PRK (an HKDF-Extract or a prior HKDF-Expand-Label output) — this function only ever expands, never extracts. QUIC never populates the Context field; every call site in this module passes an empty context, but the parameter exists for API fidelity with RFC 8446 and Phase 2b's Derive-Secret, which does use it (as a transcript hash).
HkdfLabel wire format (RFC 8446 §7.1):
uint16 length opaque label<7..255> = "tls13 " + label opaque context<0..255> = context
fn initial_receive_limit_for_stream #
fn initial_receive_limit_for_stream(id StreamId, role QuicRole, own_params QuicTransportParameters) u64
initial_receive_limit_for_stream returns the initial flow-control limit THIS endpoint has advertised to the PEER for how much the PEER may send on id, given OUR OWN transport parameters (the ones we sent). Mirror image of initial_send_limit_for_stream, using our own parameters directly (no inversion needed here -- they're already from our own perspective).
fn initial_send_limit_for_stream #
fn initial_send_limit_for_stream(id StreamId, role QuicRole, peer_params QuicTransportParameters) u64
initial_send_limit_for_stream returns the initial flow-control limit THIS endpoint may send on id, given the PEER's own advertised transport parameters (RFC 9000 §4.1). The naming is peer-relative and easy to get backwards: initial_max_stream_data_bidi_local in the PEER's parameters describes streams THEY consider local (streams THEY initiate) -- which are REMOTE-initiated from where we're sitting. Conversely their _bidi_remote describes streams WE initiate. This function resolves that inversion once, in one place, rather than leaving every call site to get the direction right on its own.
Concretely, for role=client: on a client-initiated bidi stream (ours to send on), the limit is the SERVER's initial_max_stream_data_bidi_remote (the server's own "how much may streams opened by my peer send me" value). On a server-initiated bidi stream, it's the server's initial_max_stream_data_bidi_local (the server's own "how much may my peer send me on streams I opened" value).
fn is_h3_frame_valid_on_stream #
fn is_h3_frame_valid_on_stream(frame H3Frame, role H3StreamRole) bool
is_h3_frame_valid_on_stream reports whether frame's type is permitted on a stream playing role, per RFC 9114 §7 Table 1. H3RawFrame (both grease, §7.2.8, and genuinely unrecognized extension types, §9) is valid everywhere: Table 1's own "Reserved" row is Yes/Yes/Yes, and §9 states implementations "MUST ignore unknown or unsupported values in all extensible protocol elements" -- read directly from §9's text, not assumed by analogy with the grease case alone.
This function does NOT enforce the SETTINGS-must-be-first-and-only-once rule (that needs per-stream sequencing state across multiple frames, not just this one frame's type) -- see H3ControlStreamState for that.
fn is_h3_reserved_codepoint #
fn is_h3_reserved_codepoint(v u64) bool
is_h3_reserved_codepoint reports whether v is one of the reserved "grease" values 0x1f*N+0x21 (N = 0, 1, 2, ...) shared by the frame type, stream type, SETTINGS identifier, and error code numeric spaces. These values MUST NOT be assigned meaning by any implementation and MUST NOT be treated as a protocol violation on receipt -- callers use this to route an otherwise-unrecognized codepoint to "ignore silently" rather than "reject as unknown/invalid".
fn is_persistent_congestion #
fn is_persistent_congestion(lost_packets []SentPacketInfo, pto time.Duration, max_ack_delay time.Duration, first_rtt_sample_time ?u64) bool
is_persistent_congestion determines RFC 9002 §7.6.2's persistent congestion collapse condition from a SINGLE detect_and_remove_lost_packets batch: two ack-eliciting lost packets (a) for which a prior RTT sample already existed when the EARLIER of the two was sent, (b) spanning at least kPersistentCongestionThreshold persistent-congestion-durations apart, with (c) every packet number in between also present in this same lost batch (nothing sent between them survived as acked or still-outstanding). max_ack_delay is the peer's raw max_ack_delay transport parameter, passed through UNCHANGED regardless of packet number space -- RFC 9002 §7.6.1 is explicit that this differs from the PTO formula: "Unlike the PTO computation in Section 6.2, this duration includes the max_ack_delay irrespective of the packet number spaces in which losses are established." Do NOT zero it for Initial/Handshake the way pto_time_and_space correctly does for the PTO formula -- that rule does not apply here.
Scope note: this checks contiguity only WITHIN one detection pass rather than stitching together multiple separate loss-detection calls. A genuine persistent-congestion episode -- an extended stall with no acks at all -- is realistically detected by exactly one PTO-triggered time-threshold pass covering the whole stalled range (nothing intervenes to fragment it, since nothing is being acked), so this matches the real-world trigger pattern. A contrived scenario spanning multiple separate detect-lost calls with no intervening acks is not merged in v1; failing to collapse in that narrow case is the conservative direction to err in (never wrongly collapsing the window), not an incorrect one.
fn new_connection_close_tracker #
fn new_connection_close_tracker() ConnectionCloseTracker
new_connection_close_tracker returns a tracker in the initial active state.
fn new_crypto_stream_reassembler #
fn new_crypto_stream_reassembler() &CryptoStreamReassembler
new_crypto_stream_reassembler allocates an empty reassembler for one encryption level's CRYPTO stream (see this file's module-level doc comment for the one-per-level requirement).
fn new_ecn_state #
fn new_ecn_state() EcnState
new_ecn_state returns a zero-valued EcnState with no counts recorded yet.
fn new_flow_control_window #
fn new_flow_control_window(initial_limit u64) FlowControlWindow
new_flow_control_window constructs a send-side flow-control window starting at initial_limit with nothing yet consumed.
fn new_h3_control_stream_state #
fn new_h3_control_stream_state() H3ControlStreamState
new_h3_control_stream_state returns a fresh tracker for a control stream that has not yet received (or sent) any frames.
fn new_h3_frame_decoder #
fn new_h3_frame_decoder() &H3FrameDecoder
new_h3_frame_decoder allocates an empty incremental frame decoder.
fn new_handshake_completion_state #
fn new_handshake_completion_state() &HandshakeCompletionState
new_handshake_completion_state creates a fresh tracker with none of the completion/confirmation checkpoints reached yet.
fn new_idle_timeout_state #
fn new_idle_timeout_state() IdleTimeoutState
new_idle_timeout_state returns a timer that has not yet been reset by any packet.
fn new_key_update_state #
fn new_key_update_state(initial_secret []u8) !&KeyUpdateState
new_key_update_state seeds tracking with the FIRST 1-RTT traffic secret (Phase 2's application traffic secret, derived once at handshake completion) and phase 0 -- RFC 9001 §6: "the Key Phase bit... is initially set to 0 for the first set of 1-RTT packets". Clones initial_secret rather than retaining the caller's own slice -- V array assignment shares backing storage, so retaining it uncloned would let a caller's later mutation of their own copy silently corrupt this state's secret out from under it.
fn new_newreno_congestion_control #
fn new_newreno_congestion_control() NewRenoCongestionControl
new_newreno_congestion_control constructs a fresh NewReno controller at RFC 9002 §7.2's initial state: congestion_window seeded to kInitialWindow, ssthresh unbounded.
fn new_packet_number_spaces #
fn new_packet_number_spaces() &QuicPacketNumberSpaces
new_packet_number_spaces creates the three independent per-space states (Initial, Handshake, Application Data) a connection needs, each starting from its own zero value -- no shared state between them.
fn new_qpack_decoder #
fn new_qpack_decoder(max_table_capacity u64) QpackDecoder
new_qpack_decoder returns a decoder configured with this endpoint's own SETTINGS_QPACK_MAX_TABLE_CAPACITY value (RFC 9204 §5) -- the value THIS side sends to its peer, bounding what the peer's encoder may ever set the mirrored dynamic table's capacity to (§3.2.3).
fn new_qpack_encoder #
fn new_qpack_encoder() QpackEncoder
new_qpack_encoder returns an encoder with an empty, zero-capacity dynamic table, as at the start of a connection (RFC 9204 §3.2.2: initial capacity is zero).
fn new_qpack_stream_registry #
fn new_qpack_stream_registry() QpackStreamRegistry
new_qpack_stream_registry returns an empty registry, as at the start of a connection before either QPACK stream has been seen.
fn new_quic_loss_detection_timer #
fn new_quic_loss_detection_timer() &QuicLossDetectionTimer
new_quic_loss_detection_timer constructs empty per-space loss-detection state with a fresh RttEstimator, matching RFC 9002 Appendix A.4's Init.
fn new_quic_stream #
fn new_quic_stream(id StreamId, role QuicRole) &QuicStream
new_quic_stream constructs a QuicStream for id, populating exactly the halves this endpoint (given role) actually has for that ID's category.
fn new_quic_stream_set #
fn new_quic_stream_set(role QuicRole) &QuicStreamSet
new_quic_stream_set constructs an empty QuicStreamSet for the given endpoint role.
fn new_receive_window #
fn new_receive_window(initial_limit u64) ReceiveWindow
new_receive_window constructs a receive-side flow-control window, initially advertising initial_limit to the peer.
fn new_rtt_estimator #
fn new_rtt_estimator() RttEstimator
new_rtt_estimator constructs an RttEstimator pre-seeded with RFC 9002 §5.3's pre-sample values (kInitialRtt / kInitialRtt/2), before any real sample has been taken.
fn new_stateless_reset_tracker #
fn new_stateless_reset_tracker() StatelessResetTracker
new_stateless_reset_tracker returns a tracker with no tokens recorded yet.
fn new_stream_reassembler #
fn new_stream_reassembler() &StreamReassembler
new_stream_reassembler constructs an empty reassembler starting at stream offset 0.
fn pad_initial_payload #
fn pad_initial_payload(payload []u8, header_len int, aead_tag_len int) []u8
pad_initial_payload appends PADDING frame bytes (wire value 0x00, RFC 9000 §19.1) to a not-yet-protected Initial packet's payload so that the FINAL protected packet -- header_len bytes of already-built header + this (possibly padded) payload + aead_tag_len bytes of AEAD tag -- reaches at least min_initial_datagram_size (1200) bytes. A no-op if the packet would already reach that size unpadded.
header_len MUST be the full on-wire header length INCLUDING the packet number field -- i.e. encode_long_header(...).len + pn_length, not just the encoded long header by itself (see initial_exchange_test.v for the exact call shape). The packet number is encoded separately from the long header proper but still occupies bytes of the final protected packet this function is sizing against; omitting it under-counts total by 1-4 bytes and silently under-pads below the RFC 9000 §14.1 floor.
This is RFC 9000 §14.1's PRIMARY padding mechanism -- "adding PADDING frames to the Initial packet" -- and the one every real implementation (quiche, ngtcp2, quinn) actually uses: the padding lands INSIDE the packet's own Length-delimited boundary and is authenticated by AEAD, unlike raw bytes appended to the datagram after protection (RFC 9000 §12.2 requires every long-header packet's own Length field to cover its full extent; anything past it is a SEPARATE coalesced packet or, if it doesn't parse as one, discardable garbage -- not part of THIS packet). Callers must add this padding, recompute the header's own Length field to include it, and re-encode the header BEFORE calling protect_packet -- see initial_exchange_test.v for the full sequence (padding first changes the payload length that flows into length and hence the AEAD-protected packet, so it cannot be applied to an already-protected packet the way a prior version of this padding scheme did).
fn parse_certificate #
fn parse_certificate(body []u8) !ParsedCertificate
parse_certificate parses a Certificate handshake message BODY (RFC 8446 §4.4.2). v1 is client-only and never requests client-cert auth (CertificateRequest is rejected outright per PROGRESS.md), so this function only ever parses a SERVER's Certificate message — which RFC 8446 §4.4.2.4 states "MUST always be non-empty" ("If the server supplies an empty Certificate message, the client MUST abort the handshake with a decode_error alert"), enforced unconditionally here rather than deferred to a caller that would need to know which role sent it.
fn parse_certificate_verify #
fn parse_certificate_verify(body []u8) !ParsedCertificateVerify
parse_certificate_verify parses a CertificateVerify handshake message BODY (RFC 8446 §4.4.3). Validates algorithm against the fixed set v1 itself offered in its own signature_algorithms extension (tls13_client_hello.v's sig_scheme_* constants) — RFC 8446 §4.4.3: "the signature algorithm MUST be one offered in the client's signature_algorithms extension." Since v1's offered set is a fixed, hardcoded list rather than something that varies per connection, this check needs no caller-supplied state, unlike the checks tls13_server_hello.v defers to the state machine.
fn parse_encrypted_extensions #
fn parse_encrypted_extensions(body []u8) ![]TlsExtension
parse_encrypted_extensions parses an EncryptedExtensions handshake message BODY (RFC 8446 §4.3.1: just a length-prefixed extension list, nothing else), then rejects any extension outside encrypted_extensions_allowed -- RFC 8446 §4.2: "Implementations MUST NOT send extension responses if the remote endpoint did not send the corresponding extension requests... Upon receiving such an extension, an endpoint MUST abort the handshake with an 'unsupported_extension' alert." Previously only early_data was checked (a Codex finding, vlang/v#27680 pullrequestreview-4783410111, pointed out that other EE-illegal extensions like key_share/supported_versions passed through unrejected); early_data keeps its own explicit message for a clearer diagnostic, but both routes now carry the same unsupported_extension QUIC CONNECTION_CLOSE code via error_with_code, not a generic error() a caller would otherwise remap to decode_error.
fn parse_extension_list #
fn parse_extension_list(buf []u8) ![]TlsExtension
parse_extension_list walks a full extension list's inner bytes (the concatenated extensions themselves — callers strip whatever length prefix wrapped the whole list first) and returns every entry. RFC 8446 §4.2: "There MUST NOT be more than one extension of the same type in a given extension block" — enforced here, mirroring transport_parameters.v's duplicate-ID rejection for the analogous QUIC-level TLV sequence.
fn parse_frame #
fn parse_frame(buf []u8) !(QuicFrame, int)
parse_frame parses exactly one frame from the start of buf, returning the frame and the number of bytes consumed. A run of consecutive PADDING bytes is consumed as a single PaddingFrame (see its own doc comment).
fn parse_frames #
fn parse_frames(buf []u8) ![]QuicFrame
parse_frames parses every frame filling buf (a packet's already AEAD-decrypted payload), in order, until the buffer is fully consumed.
fn parse_h3_unidirectional_stream_header #
fn parse_h3_unidirectional_stream_header(buf []u8) ?H3UnidirectionalStreamHeader
parse_h3_unidirectional_stream_header attempts to decode the Stream Type (and, for a push stream, the Push ID that immediately follows it -- RFC 9114 §6.2.2 Figure 2) from the start of buf. Returns none, not an error, when buf does not yet hold enough bytes to complete the header: RFC 9114 places no requirement on how these header bytes are split across QUIC STREAM frames, so a caller reading a fresh unidirectional stream incrementally must be able to retry once more data has arrived rather than treat a short read as a protocol violation (mirrors this file's h3_frame.v incremental frame decoder). On success, the returned header's consumed field is the number of bytes it occupied at the START of buf.
fn parse_handshake_message #
fn parse_handshake_message(buf []u8) !(HandshakeMessage, int)
parse_handshake_message reads one TLS 1.3 handshake message from the front of buf and returns it along with the number of bytes consumed. buf may contain trailing bytes belonging to a LATER message — QUIC delivers handshake messages via CRYPTO frames that can span or share packet boundaries arbitrarily (Phase 4's crypto_stream.v job to reassemble into a contiguous byte stream); this function only peels off exactly one message and reports how much of buf it consumed, so a caller can loop.
fn parse_long_header #
fn parse_long_header(buf []u8) !(QuicLongHeader, int)
parse_long_header parses a long header's unprotected portion (packet type through the Length field for Initial/0-RTT/Handshake, or through the retry token for Retry — see parse_retry_header for the Retry Integrity Tag that follows). Returns the parsed header and the number of bytes consumed, so the caller can locate where header-protected packet number bytes begin (or, for Retry, where the fixed-size integrity tag begins).
Reserved bits (first_byte bits 2-3) are NOT validated here — they are protected by header protection and can only be checked meaningfully after that is removed (Phase 3's responsibility).
fn parse_retry_packet #
fn parse_retry_packet(buf []u8, original_dcid []u8, original_scid []u8) !QuicRetryPacket
Field semantics (RFC 9000 §17.2.5.1, confirmed against the primary text -- easy to get backwards since both DCID and SCID are, as always, from the PACKET SENDER'S (the server's) point of view, not the client's): the server populates the Retry's DESTINATION Connection ID with the connection ID the CLIENT included as its own SOURCE Connection ID in the Initial that provoked this Retry -- an echo, not a new value. The server's NEWLY CHOSEN connection ID -- the one the client MUST switch to as the Destination Connection ID of its retried Initial -- is the Retry's SOURCE Connection ID (scid below), never dcid.
original_dcid is the Destination Connection ID the client used on its own original Initial packet (the value Initial secrets were derived from) -- required to enforce RFC 9000 §17.2.5.1's anti-loop check: "A client MUST discard a Retry packet that contains a Source Connection ID field that is identical to the Destination Connection ID field of its Initial packet." A Retry whose SCID merely echoes back the client's own prior DCID carries no real server-chosen replacement and cannot meaningfully be switched to.
original_scid is the Source Connection ID the client used on that same original Initial packet -- required to validate the ECHO half of the same RFC 9000 §17.2.5.1 sentence quoted above ("the server populates the Destination Connection ID with the connection ID the client included as its own Source Connection ID"): the Retry's own DCID must equal it. This is a DISTINCT check from the anti-loop one below -- the publicly known Retry Integrity Key authenticates that whoever sent this observed SOME Initial packet, not that THIS Retry is addressed to THIS client's own connection attempt, so an attacker who also observed or correctly guessed the client's original DCID could still forge a Retry with a mismatched DCID without this check.
fn parse_server_hello #
fn parse_server_hello(body []u8) !ServerHelloMessage
parse_server_hello parses a ServerHello handshake message body (RFC 8446 §4.1.3), returning either a ParsedServerHello or, when random matches the magic HelloRetryRequest value, a ParsedHelloRetryRequest -- the two share a wire type but are validated against distinct mandatory fields and allowed- extension sets (see server_hello_allowed/hello_retry_request_allowed above). Rejects a non-1.3 selected_version, a non-empty legacy_session_id_echo, a missing/malformed key_share, and any extension not on the applicable allowlist -- cipher_suite itself is parsed and returned but not validated here; the caller (process_server_hello) checks it against the single suite this client offers.
fn parse_short_header #
fn parse_short_header(buf []u8, dcid_len int) !(QuicShortHeader, int)
parse_short_header parses a short header given the expected DCID length (which the caller must supply from its own connection state — see the struct doc comment above for why this can't be inferred from the packet). Returns the header and bytes consumed (always 1 + dcid_len; the packet number that follows has no explicit length prefix and extends however many bytes the (still-protected) low 2 bits of the first byte indicate, which is only knowable after header protection removal).
fn parse_version_negotiation #
fn parse_version_negotiation(buf []u8) !QuicVersionNegotiation
parse_version_negotiation parses a Version Negotiation packet (RFC 9000 §17.2.1) -- distinguished from every other long-header packet by its version field being 0, and carrying no packet number, Length field, or encrypted payload.
fn peek_header_form #
fn peek_header_form(buf []u8) !HeaderForm
peek_header_form reports whether buf starts a long-header or short-header packet. This bit is never protected — it's always readable directly.
fn protect_header #
fn protect_header(mut packet []u8, pn_offset int, pn_length int, hp_key []u8, form HeaderForm) !
protect_header applies header protection to packet in place, given an already-chosen pn_length — the sender always knows this, since it picked the packet number's encoded length itself before calling this (see encode_packet_number). packet must already contain the full unprotected header AND the AEAD-encrypted payload: per RFC 9001 §5.4.1/§5.3, the sample is taken from the CIPHERTEXT, so this function must run strictly AFTER AEAD encryption — reversing that order is the single most common QUIC packet-protection implementation bug. pn_offset is the index into packet where the (currently still plaintext) packet number bytes begin.
fn protect_packet #
fn protect_packet(header []u8, form HeaderForm, packet_number u64, pn_length int, payload []u8, keys QuicPacketProtectionKeys) ![]u8
protect_packet assembles one fully protected QUIC packet from its unprotected header, full packet number, and plaintext payload, applying RFC 9001 §5's protection steps in the ONLY correct order: AEAD-encrypt the payload FIRST, THEN sample the resulting ciphertext to derive the header protection mask, THEN apply that mask to the header. Reversing this order (protecting the header before the payload exists, or sampling plaintext instead of ciphertext) is the single most common QUIC packet-protection implementation bug — callers should use this function rather than sequencing encrypt_packet_payload/protect_header themselves.
header is the complete unprotected header bytes, ending with the plaintext packet number encoded in exactly pn_length bytes (see encode_packet_number). form selects which low bits of the first byte header protection covers.
fn qpack_blocked_streams_from_settings #
fn qpack_blocked_streams_from_settings(settings []H3Setting) u64
qpack_blocked_streams_from_settings extracts SETTINGS_QPACK_BLOCKED_STREAMS from an already-decoded HTTP/3 SETTINGS frame's parameter list, defaulting to 0 (RFC 9204 §5) when absent.
fn qpack_entry_size #
fn qpack_entry_size(name string, value string) int
qpack_entry_size is the size RFC 9204 §3.2.1 assigns a dynamic table entry for accounting purposes: name length + value length + 32 bytes of fixed overhead, measured on the UN-Huffman-encoded name/value.
fn qpack_is_sensitive #
fn qpack_is_sensitive(name string) bool
qpack_is_sensitive mirrors net.http's h2_is_sensitive (same security posture applied consistently across both compression schemes in this codebase, not a fresh unreviewed policy): RFC 9204 §7.1.3 names Cookie and Authorization as fields an encoder might choose never to index.
fn qpack_max_entries #
fn qpack_max_entries(max_table_capacity u64) u64
qpack_max_entries is MaxEntries from RFC 9204 §4.5.1.1: the largest number of entries the dynamic table could hold at the decoder's configured maximum capacity (NOT the table's current, possibly smaller, capacity -- the RFC is explicit that this uses "the maximum capacity of the dynamic table as specified by the decoder", a value fixed by SETTINGS for the life of the connection, independent of subsequent Set Dynamic Table Capacity instructions).
fn qpack_max_table_capacity_from_settings #
fn qpack_max_table_capacity_from_settings(settings []H3Setting) u64
qpack_max_table_capacity_from_settings extracts SETTINGS_QPACK_MAX_TABLE_CAPACITY from an already-decoded HTTP/3 SETTINGS frame's parameter list, defaulting to 0 (RFC 9204 §5) when absent. Pure function over h3_frame.v's existing H3Setting list -- this file adds no new frame parsing, only QPACK's own interpretation of values decode_h3_settings_payload already validated and stored generically.
fn qpack_static_find #
fn qpack_static_find(name string, value string) ?int
qpack_static_find returns the index of a static table entry whose name and value both match exactly, if one exists. Used by an encoder deciding whether a field line can be a fully indexed reference.
fn qpack_static_find_name #
fn qpack_static_find_name(name string) ?int
qpack_static_find_name returns the index of the first static table entry whose name matches, if one exists. Used by an encoder falling back to a literal field line with a static name reference.
fn qpack_static_lookup #
fn qpack_static_lookup(index int) !QpackStaticEntry
qpack_static_lookup returns the static table entry at index (RFC 9204 §3.1). An out-of-range index is a decode-time error the caller must map to QPACK_DECOMPRESSION_FAILED (field line representation) or QPACK_ENCODER_STREAM_ERROR (encoder instruction), per which stream the reference appeared on -- this function only reports "invalid", not which error code applies, since that depends on context this file doesn't have.
fn scaled_ack_delay_micros #
fn scaled_ack_delay_micros(raw_ack_delay u64, ack_delay_exponent u64) u64
scaled_ack_delay_micros converts an AckFrame's raw ack_delay into microseconds using the peer's negotiated ack_delay_exponent (RFC 9000 §19.3: ACK Delay is the peer's estimate, in ack_delay_exponent-scaled units, of the time between receiving the largest-acknowledged packet and sending this ACK).
fn split_coalesced_datagram #
fn split_coalesced_datagram(datagram []u8) ![]CoalescedPacket
split_coalesced_datagram splits one received UDP datagram into its constituent QUIC packets, in wire order. Each returned packet's bytes is exactly that one packet's span, sliced (not copied) from datagram.
fn synthetic_client_hello1_hash #
fn synthetic_client_hello1_hash(client_hello1 []u8) []u8
synthetic_client_hello1_hash implements RFC 8446 §4.4.1's HelloRetryRequest transcript rule: after a HelloRetryRequest, every Transcript-Hash computed for the rest of the handshake replaces the literal ClientHello1 bytes with a synthetic "message_hash" handshake message wrapping Hash(ClientHello1), instead of hashing ClientHello1 itself twice over (once standalone in an earlier transcript, once as a prefix of a later one):
Transcript-Hash(ClientHello1, HelloRetryRequest, ... Mn) = Hash(message_hash || /* Handshake type / 00 00 Hash.length || / Handshake message length (bytes) */ Hash(ClientHello1) || HelloRetryRequest || ... || Mn)
This function returns that synthetic record's bytes (NOT yet hashed). The caller — Phase 2c's transcript accumulator, once a HelloRetryRequest is seen — feeds these bytes into the running hash IN PLACE OF the real ClientHello1 bytes, then continues normally with HelloRetryRequest and every later message appended as-is.
fn tls_alert_to_quic_error #
fn tls_alert_to_quic_error(alert TlsAlert) u64
tls_alert_to_quic_error implements RFC 9001 §4.8: "If TLS produces an alert, QUIC MUST convert it into a QUIC CONNECTION_CLOSE error. The alert description ... is added to 0x100 to produce a QUIC error code." This pure-V implementation never runs an actual TLS alert-producing library, so this handshake's own failure paths pick the RFC 8446 §6 alert a compliant peer would have raised for the equivalent condition, and this function does the same 0x100 translation a real TLS-stack-to-QUIC bridge would.
fn unprotect_header #
fn unprotect_header(mut packet []u8, pn_offset int, hp_key []u8, form HeaderForm) !int
unprotect_header removes header protection from packet in place and returns the packet number's true encoded length. Unlike protect_header, the caller does NOT know pn_length up front — it is itself part of the protected first byte — so this function must unmask the first byte FIRST to learn pn_length, then unmask exactly that many packet-number bytes. Doing this in the opposite order (unmasking a guessed number of packet-number bytes before the first byte has revealed the real count) would corrupt bytes belonging to the payload rather than the packet number.
Once unmasked, the first byte's Reserved Bits are checked: RFC 9000 §17.2/§17.3.1 requires a sender to always transmit them as zero, and requires a receiver to treat a non-zero value here as a PROTOCOL_VIOLATION connection error, NOT a silently dropped packet (unlike an AEAD authentication failure, see decrypt_packet_payload's doc comment) -- mapping this error to that specific close behavior is the caller's (Phase 8+ connection-lifecycle) responsibility, same division as this module's other errors.
fn unprotect_packet #
fn unprotect_packet(mut packet []u8, pn_offset int, form HeaderForm, keys QuicPacketProtectionKeys, largest_pn ?u64) !UnprotectedPacket
unprotect_packet reverses protect_packet on a received packet: removes header protection FIRST — which is the only way to learn the packet's real packet-number length and value at all — THEN AEAD-decrypts the payload using that recovered packet number and the now-unprotected header as associated data. This is the mirror image of protect_packet's ordering, not the same steps run backwards; see unprotect_header's own doc comment for why header-then-payload is mandatory on this side specifically.
packet is mutated in place (header protection removal happens destructively). pn_offset is where the (still-protected) packet number field begins — the caller gets this from parsing the always-visible header fields (parse_long_header/parse_short_header) that precede it. largest_pn is the largest packet number already processed in this packet's number space (none if this is the first packet processed in that space) — see decode_packet_number.
On AEAD authentication failure, the caller MUST drop the packet silently rather than close the connection (see decrypt_packet_payload's doc comment) — this function only surfaces the error.
fn varint_len #
fn varint_len(value u64) !int
varint_len returns the number of bytes encode_varint would use to encode value, without actually encoding it.
fn verify_finished #
fn verify_finished(base_secret []u8, transcript_hash []u8, peer_verify_data []u8) !bool
verify_finished checks a peer-supplied Finished message's verify_data against the expected value computed from our own key schedule and transcript state, using a constant-time comparison (crypto.hmac.equal) — verify_data is exactly the kind of peer-supplied authenticator where a naive == would leak timing information about how much of it matched.
fn verify_retry_integrity_tag #
fn verify_retry_integrity_tag(original_dcid []u8, packet []u8, already_processed_other_packet bool) !bool
verify_retry_integrity_tag reports whether a Retry packet should be accepted: FALSE means discard it (never process it further -- do NOT call parse_retry_packet on a packet this function rejects), TRUE means its Integrity Tag validated and it is the client's first accepted Retry or Initial for this connection attempt. Discarding is never a connection error: Retry packets aren't authenticated by the connection's own key material, and both discard conditions below exist specifically to deny an off-path attacker a way to abort or replay into a legitimate handshake in progress, so tearing the connection down over either would hand that attacker exactly the outcome the RFC is protecting against.
already_processed_other_packet is the caller-tracked connection- lifecycle state (owned by whichever later phase owns QuicConn, Phase 9, not by this stateless primitive) for BOTH halves of RFC 9000 §17.2.5.2's combined rule: "A client MUST accept and process at most one Retry packet for each connection attempt. After the client has received and processed an Initial or Retry packet from the server, it MUST discard any subsequent Retry packets that it receives." A caller sets this true once it has successfully processed EITHER its first Initial from the server OR its first accepted Retry (this function's own TRUE return already IS that moment for the Retry half) -- checked FIRST, before spending a AES-GCM verification on a packet that must be discarded regardless of its tag.
A Retry with an invalid tag MUST also be discarded (RFC 9000 §17.2.5.2) -- this is the ORIGINAL check this function performed before the state parameter was added; unchanged in behavior or meaning.
fn verify_server_certificate_chain #
fn verify_server_certificate_chain(parsed ParsedCertificate, ca_bundle_pem string, hostname string) !&VerifiedCertificateChain
verify_server_certificate_chain builds an mbedTLS certificate chain from a parsed Certificate message's certificate_list (leaf-first, per RFC 8446 §4.4.2) and validates it against ca_bundle_pem (one or more trusted CA certificates in PEM format — the caller's trust anchor, mirroring this codebase's existing net.mbedtls.SSLConnectConfig.verify contract, since there is no OS trust-store lookup anywhere in this codebase for any TLS client) AND that hostname (the SNI name this client actually sent) matches the leaf certificate's SAN/CN — see net.mbedtls.verify_certificate_chain's own doc comment for the mechanism. Without this, any otherwise-trusted certificate for an unrelated host would be accepted (hostname impersonation), since chain-of-trust alone says nothing about which host the certificate is actually FOR.
fn CertificateVerifyRole.from #
fn CertificateVerifyRole.from[W](input W) !CertificateVerifyRole
fn ClientHandshakeState.from #
fn ClientHandshakeState.from[W](input W) !ClientHandshakeState
fn ConnectionCloseState.from #
fn ConnectionCloseState.from[W](input W) !ConnectionCloseState
fn ConnectionState.from #
fn ConnectionState.from[W](input W) !ConnectionState
fn H3ErrorCode.from #
fn H3ErrorCode.from[W](input W) !H3ErrorCode
fn H3StreamRole.from #
fn H3StreamRole.from[W](input W) !H3StreamRole
fn H3UnidirectionalStreamKind.from #
fn H3UnidirectionalStreamKind.from[W](input W) !H3UnidirectionalStreamKind
fn HandshakeType.from #
fn HandshakeType.from[W](input W) !HandshakeType
fn HeaderForm.from #
fn HeaderForm.from[W](input W) !HeaderForm
fn LongPacketType.from #
fn LongPacketType.from[W](input W) !LongPacketType
fn QpackErrorCode.from #
fn QpackErrorCode.from[W](input W) !QpackErrorCode
fn QpackStreamKind.from #
fn QpackStreamKind.from[W](input W) !QpackStreamKind
fn QuicEventKind.from #
fn QuicEventKind.from[W](input W) !QuicEventKind
fn QuicPacketNumberSpace.from #
fn QuicPacketNumberSpace.from[W](input W) !QuicPacketNumberSpace
fn QuicRole.from #
fn QuicRole.from[W](input W) !QuicRole
fn RecvStreamState.from #
fn RecvStreamState.from[W](input W) !RecvStreamState
fn SendStreamState.from #
fn SendStreamState.from[W](input W) !SendStreamState
fn StreamDirection.from #
fn StreamDirection.from[W](input W) !StreamDirection
fn StreamInitiator.from #
fn StreamInitiator.from[W](input W) !StreamInitiator
fn Tls13ClientHandshake.start #
fn Tls13ClientHandshake.start(p ClientHandshakeParams) !(&Tls13ClientHandshake, []u8)
Tls13ClientHandshake.start generates this client's ephemeral ECDHE keypair, builds ClientHello (RFC 8446 §4.1.2), and returns both the new handshake object (state .wait_server_hello) and the ClientHello bytes to send. p.transport_parameters is validated by build_client_hello itself (rejects any server-only parameter) before anything is allocated.
fn TlsAlert.from #
fn TlsAlert.from[W](input W) !TlsAlert
type H3Frame #
type H3Frame = CancelPushFrame
| DataFrame
| GoawayFrame
| H3RawFrame
| HeadersFrame
| MaxPushIdFrame
| PushPromiseFrame
| SettingsFrame
H3Frame is any decoded HTTP/3 frame.
type QpackDecoderInstruction #
type QpackDecoderInstruction = QpackInsertCountIncrement
| QpackSectionAck
| QpackStreamCancellation
QpackDecoderInstruction is any one of the 3 decoder-stream instructions.
type QpackEncoderInstruction #
type QpackEncoderInstruction = QpackDuplicate
| QpackInsertWithLiteralName
| QpackInsertWithNameRef
| QpackSetDynamicTableCapacity
QpackEncoderInstruction is any one of the 4 encoder-stream instructions.
type QuicFrame #
type QuicFrame = AckFrame
| ConnectionCloseFrame
| CryptoFrame
| DataBlockedFrame
| HandshakeDoneFrame
| MaxDataFrame
| MaxStreamDataFrame
| MaxStreamsFrame
| PaddingFrame
| PingFrame
| ResetStreamFrame
| StopSendingFrame
| StreamDataBlockedFrame
| StreamFrame
| StreamsBlockedFrame
type ServerHelloMessage #
type ServerHelloMessage = ParsedHelloRetryRequest | ParsedServerHello
enum CertificateVerifyRole #
enum CertificateVerifyRole {
server
client
}
enum ClientHandshakeState #
enum ClientHandshakeState {
wait_server_hello
wait_encrypted_extensions
wait_certificate
wait_certificate_verify
wait_finished
connected
}
ClientHandshakeState tracks which handshake message this client is waiting to receive next (RFC 8446 §4.1.4's message flow, minus the PSK/0-RTT-only states v1 never reaches). wait_certificate covers BOTH a Certificate and a CertificateRequest arriving next -- RFC 8446 permits either, and process_certificate_or_request distinguishes them by message type rather than the state machine splitting into two states for a message type v1 always rejects anyway.
enum ConnectionCloseState #
enum ConnectionCloseState {
active
closing
draining
}
RFC 9000 §10.2 — Immediate Close. Once either endpoint decides to close the connection, it enters one of two mutually-exclusive states:
- "closing": THIS endpoint sent (or is about to send) its own CONNECTION_CLOSE and is waiting out the closing period. It MAY still send -- specifically, a rate-limited retransmission of that same CONNECTION_CLOSE in response to further incoming packets (§10.2.1), to guard against the peer never having received the first one, without allowing an unbounded ping-pong.- "draining": THIS endpoint received a CONNECTION_CLOSE FROM the peer (or otherwise learned the connection is being closed) and MUST NOT send ANYTHING at all, not even its own CONNECTION_CLOSE -- §10.2.2 is unambiguous that draining is fully silent.
An endpoint already in "closing" that then RECEIVES a CONNECTION_CLOSE from the peer moves straight to "draining" (§10.2.2): the peer has already acknowledged the connection is ending, so this endpoint's own pending retransmit is moot and continuing to send would violate the silence requirement for no benefit.
enum ConnectionState #
enum ConnectionState {
handshaking
established
closing
draining
closed
}
enum H3ErrorCode #
enum H3ErrorCode {
no_error = 0x0100
general_protocol_error = 0x0101
internal_error = 0x0102
stream_creation_error = 0x0103
closed_critical_stream = 0x0104
frame_unexpected = 0x0105
frame_error = 0x0106
excessive_load = 0x0107
id_error = 0x0108
settings_error = 0x0109
missing_settings = 0x010a
request_rejected = 0x010b
request_cancelled = 0x010c
request_incomplete = 0x010d
message_error = 0x010e
connect_error = 0x010f
version_fallback = 0x0110
}
H3ErrorCode is the RFC 9114 §8.1 "HTTP/3 Error Codes" registry, used as the QUIC application-protocol error code when abruptly terminating an HTTP/3 stream, aborting a stream read, or closing an HTTP/3 connection. Exact wire values transcribed directly from §8.1 / Table 4 (§11.2.3), not recalled -- every value here was read from the fetched RFC text at .claude/skills/code-review/rfc-texts/rfc9114.txt.
fn (H3ErrorCode) code #
fn (e H3ErrorCode) code() u64
code returns the wire value of e as a u64, ready to carry as a QUIC application-protocol error code (RFC 9000 §20.2) in a CONNECTION_CLOSE or RESET_STREAM/STOP_SENDING frame.
enum H3StreamRole #
enum H3StreamRole {
control
request
push
}
H3StreamRole is which of HTTP/3's three stream purposes (Table 1's three columns) a stream is currently playing. Determining a stream's role in the first place (reading its unidirectional Stream Type header via h3_stream_type.v, or recognizing it as a client-initiated bidirectional QUIC stream) is a Phase 12 wiring concern; this file only needs to be TOLD the role once known.
enum H3UnidirectionalStreamKind #
enum H3UnidirectionalStreamKind {
control
push
reserved
unknown
}
H3UnidirectionalStreamKind classifies a decoded Stream Type value. reserved and unknown are deliberately distinct even though RFC 9114 treats both as "ignore, do not error" (§6.2/§6.2.3): reserved values exist SPECIFICALLY to be grease, while unknown covers a value this implementation genuinely does not recognize (e.g. a future extension's QPACK-encoder/-decoder stream types, defined by RFC 9204 rather than RFC 9114 itself, or any other extension) -- keeping them apart lets a caller log/count them differently without treating either as an error.
enum HandshakeType #
enum HandshakeType {
client_hello = 1
server_hello = 2
new_session_ticket = 4
end_of_early_data = 5
encrypted_extensions = 8
certificate = 11
certificate_request = 13
certificate_verify = 15
finished = 20
key_update = 24
message_hash = 254
}
RFC 8446 §B.3 — TLS 1.3 handshake message types. Excludes the TLS-1.2-era RESERVED values (hello_request, hello_verify_request, hello_retry_request as its own wire type — HelloRetryRequest is a ServerHello variant distinguished by its random field, not a separate msg_type — server_key_exchange, server_hello_done, client_key_exchange, certificate_url, certificate_status, supplemental_data): those can never legitimately appear on a TLS 1.3 wire, so a peer sending one is malformed input, correctly rejected by parse_handshake_message's "unknown type" path rather than accepted as a recognized-but-unused variant.
enum HeaderForm #
enum HeaderForm {
short
long
}
enum LongPacketType #
enum LongPacketType {
initial
zero_rtt
handshake
retry
}
enum QpackErrorCode #
enum QpackErrorCode {
decompression_failed = 0x0200
encoder_stream_error = 0x0201
decoder_stream_error = 0x0202
}
QpackErrorCode is the RFC 9204 §6/§8.3 "HTTP/3 Error Codes" additions for QPACK, used as the QUIC application-protocol error code when QPACK decoding or an encoder/decoder-stream instruction fails. Exact wire values transcribed directly from §6 / Table 3 (§8.3), not recalled -- mirrors h3_error.v's H3ErrorCode shape exactly (sibling checked before writing this).
fn (QpackErrorCode) code #
fn (e QpackErrorCode) code() u64
code returns the wire value of e as a u64, ready to carry as a QUIC application-protocol error code (RFC 9000 §20.2) in a CONNECTION_CLOSE frame.
enum QpackStreamKind #
enum QpackStreamKind {
encoder
decoder
}
QpackStreamKind distinguishes the two QPACK stream types.
enum QuicEventKind #
enum QuicEventKind {
handshake_confirmed
connection_closed
}
enum QuicPacketNumberSpace #
enum QuicPacketNumberSpace {
initial
handshake
application_data
}
enum QuicRole #
enum QuicRole {
client
server
}
QuicRole distinguishes which side of the connection THIS endpoint is -- needed because "is this stream mine to have opened" and "am I allowed to send on this uni stream" depend on who's asking, not just the ID itself. v1 only ever runs as .client (server support is Phase 13, out of committed scope) -- this enum exists now so stream.v doesn't need reshaping when that phase lands, matching QuicConn's own planned role-field design.
enum RecvStreamState #
enum RecvStreamState {
recv
size_known
data_recvd
data_read
reset_recvd
reset_read
}
RFC 9000 §3.2 — Receive Stream States. size_known/data_recvd/ reset_recvd are driven by frame ARRIVAL (this file's job); data_read/ reset_read are driven by the APPLICATION consuming the data/reset (a later phase's job, once an application-facing read API exists) -- mark_data_read/mark_reset_read exist as that future hook.
enum SendStreamState #
enum SendStreamState {
ready
send
data_sent
data_recvd
reset_sent
reset_recvd
}
RFC 9000 §3.1 — Send Stream States. Transitions driven by LOCAL actions (queuing data, sending FIN, sending RESET_STREAM) are modeled here directly; transitions driven by the PEER'S ACKNOWLEDGMENT (Data Sent -> Data Recvd, Reset Sent -> Reset Recvd) need ACK-processing machinery this module doesn't have yet (Phase 7) -- mark_all_data_acked/ mark_reset_acked exist as the hooks a later phase will call, not something this file drives on its own.
enum StreamDirection #
enum StreamDirection {
bidirectional
unidirectional
}
enum StreamInitiator #
enum StreamInitiator {
client
server
}
enum TlsAlert #
enum TlsAlert {
unexpected_message = 10
handshake_failure = 40
bad_certificate = 42
illegal_parameter = 47
decode_error = 50
decrypt_error = 51
// RFC 7301 §3.2 (registered separately from RFC 8446's own alert
// registry): the fatal alert a server sends when it supports none of
// the client's offered ALPN protocols. This client's own ALPN check
// in process_encrypted_extensions raises the same alert when the
// SERVER'S selection isn't one this client actually offered, or ALPN
// is missing entirely -- RFC 9001 §8.1 makes ALPN mandatory for QUIC,
// so either case means no application protocol was agreed.
no_application_protocol = 120
// RFC 8446 §4.2: "Implementations MUST NOT send extension responses if
// the remote endpoint did not send the corresponding extension
// requests... Upon receiving such an extension, an endpoint MUST abort
// the handshake with an 'unsupported_extension' alert." Used by
// parse_encrypted_extensions for any extension outside the small set
// this client can legitimately receive there (IANA TLS Alert registry
// value 110, confirmed directly, not assumed).
unsupported_extension = 110
// RFC 9001 §8.2: "endpoints that receive ClientHello or
// EncryptedExtensions messages without the quic_transport_parameters
// extension MUST close the connection with an error of type 0x016d
// (equivalent to a fatal TLS missing_extension alert)" -- 0x016d =
// 0x100 + 0x6d (109), confirmed against the IANA TLS Alert registry
// directly, not assumed.
missing_extension = 109
}
TlsAlert is the subset of RFC 8446 §6's alert descriptions this handshake actually produces -- not the full registry, matching this module's established narrow-but-real-values convention (see tls13_client_hello.v's sig_scheme_* constants).
struct AckFrame #
struct AckFrame {
pub:
largest_acknowledged u64
ack_delay u64
ranges []AckRange
ecn_counts ?EcnCounts
}
AckFrame represents an ACK frame (type 0x02, or 0x03 when it also carries ECN counts). ranges is ordered largest-first, matching the wire order; ranges are always non-overlapping with at least one unacknowledged packet number between consecutive ranges.
ack_delay is the RAW wire value (RFC 9000 §19.3): it is NOT yet scaled by the peer's ack_delay_exponent transport parameter (a connection-level value this frame-parsing layer has no access to) -- see scaled_ack_delay_micros. It also MUST be ignored entirely for RTT sampling purposes in the Initial and Handshake packet number spaces (RFC 9002 §5.3); that policy belongs to a later phase's loss-detection code (Phase 7), not here -- this struct only carries the raw value forward.
struct AckProcessingResult #
struct AckProcessingResult {
pub:
newly_acked []SentPacketInfo
lost []SentPacketInfo
persistent_congestion bool
}
AckProcessingResult reports what one on_ack_received call discovered: which previously-sent packets it newly acknowledged, which packets it caused to be declared lost, and whether that loss batch meets RFC 9002 §7.6.2's persistent-congestion condition.
struct AckRange #
struct AckRange {
pub:
smallest u64
largest u64
}
AckRange is one reconstructed, already-resolved [smallest, largest] inclusive range of acknowledged packet numbers (RFC 9000 §19.3.1) -- the wire's Gap/ACK Range Length encoding is resolved into this shape by parse_frame so callers never need to re-derive it themselves.
struct ApplicationSecrets #
struct ApplicationSecrets {
pub:
master_secret []u8
client_secret []u8 // client_application_traffic_secret_0
server_secret []u8 // server_application_traffic_secret_0
}
struct CancelPushFrame #
struct CancelPushFrame {
pub:
push_id u64
}
CancelPushFrame identifies a server push to cancel by push ID (§7.2.3).
struct CertificateEntry #
struct CertificateEntry {
pub:
cert_data []u8
extensions []TlsExtension
}
CertificateEntry is one X.509 certificate plus its per-certificate extensions (RFC 8446 §4.4.2). v1 only speaks the X509 CertificateType — RawPublicKey (RFC 7250) is never negotiated (v1's EncryptedExtensions parsing doesn't send/accept the certificate-type extensions that would select it), so cert_data is always a DER-encoded X.509 certificate.
struct ClientHandshakeParams #
struct ClientHandshakeParams {
pub:
random []u8 // exactly 32 bytes; caller supplies so callers can use a real CSPRNG while tests stay deterministic
server_name string
transport_parameters QuicTransportParameters // this client's own offered set
ca_bundle_pem string // trust anchor for the server's certificate chain
alpn_protocols []string // offered application protocols, most preferred first (RFC 7301 §3.1) -- e.g. ['h3']
}
ClientHandshakeParams is everything Tls13ClientHandshake.start needs beyond what's fixed by v1's scope decisions (single cipher suite, single named group -- see tls13_client_hello.v).
struct ClientHelloParams #
struct ClientHelloParams {
pub:
random []u8 // exactly 32 bytes; caller supplies so callers can use a real CSPRNG while tests stay deterministic
server_name string
ecdhe_public_key []u8 // Phase 1 PublicKey.uncompressed_bytes() output, 65 bytes for P-256
transport_parameters QuicTransportParameters
// Application-layer protocols this client is willing to speak, most
// preferred first (RFC 7301 §3.1) -- MANDATORY for QUIC (RFC 9001
// §8.1: there is no fallback protocol-negotiation mechanism). v1's
// only real caller offers exactly `['h3']`, but this stays a list
// (not a fixed single constant) to match RFC 7301's own wire shape
// and leave room for a future h3+h2-fallback list without another
// signature change.
alpn_protocols []string
}
ClientHelloParams is everything build_client_hello needs beyond what's fixed by v1's scope decisions (single cipher suite, single named group, a fixed signature_algorithms list).
struct CoalescedPacket #
struct CoalescedPacket {
pub:
bytes []u8
form HeaderForm
}
CoalescedPacket is one packet's raw bytes, sliced out of a (possibly multi-packet) UDP datagram, along with its header form -- callers use form to route to the right subsequent parser (parse_long_header vs. parse_short_header vs. parse_version_negotiation) without re-deriving it.
struct ConnectionCloseFrame #
struct ConnectionCloseFrame {
pub:
is_application_error bool
error_code u64
frame_type u64
reason string
}
ConnectionCloseFrame represents a CONNECTION_CLOSE frame (type 0x1c transport-level, or 0x1d application-level -- is_application_error distinguishes them). frame_type is only meaningful for the transport-level variant (the frame type that provoked the close, or 0 if unknown/not applicable); the application-level variant has no such field on the wire. Mapping these to actual connection-lifecycle behavior (closing/draining state, RFC 9000 §10.2) is Phase 8's job (connection_close.v) -- this is purely the wire decode.
struct ConnectionCloseTracker #
struct ConnectionCloseTracker {
pub mut:
state ConnectionCloseState
packets_received_while_closing u64
}
ConnectionCloseTracker tracks which of RFC 9000 §10.2's connection-close states (active/closing/draining) this endpoint is currently in.
fn (ConnectionCloseTracker) enter_closing #
fn (mut t ConnectionCloseTracker) enter_closing()
enter_closing transitions active -> closing: THIS endpoint has decided to close the connection (a locally-detected error, or the application closing it) and is about to send its own CONNECTION_CLOSE. A no-op once already closing or draining -- closing never regresses, and draining always wins over it (see enter_draining).
fn (ConnectionCloseTracker) enter_draining #
fn (mut t ConnectionCloseTracker) enter_draining()
enter_draining transitions active|closing -> draining: THIS endpoint received a CONNECTION_CLOSE from the peer. Always wins over an existing "closing" state (RFC 9000 §10.2.2) -- draining is a one-way absorbing state once reached.
fn (ConnectionCloseTracker) note_packet_received_while_closing #
fn (mut t ConnectionCloseTracker) note_packet_received_while_closing() bool
note_packet_received_while_closing reports whether, on receiving another packet while in the closing state, this endpoint may resend its own CONNECTION_CLOSE -- RFC 9000 §10.2.1 rate-limits this to avoid an unbounded ping-pong: at most one retransmission per received packet, so a peer cannot extract more retransmissions than the number of packets it itself is willing to send. Always false outside the closing state (nothing to retransmit while active, and draining must stay fully silent regardless of what arrives).
fn (ConnectionCloseTracker) may_send #
fn (t &ConnectionCloseTracker) may_send() bool
may_send reports whether ANY packet may be sent right now. Draining is fully silent (RFC 9000 §10.2.2); closing may still send the rate-limited CONNECTION_CLOSE retransmission above; active sends freely.
struct CryptoFrame #
struct CryptoFrame {
pub:
offset u64
data []u8
}
CryptoFrame represents a CRYPTO frame (type 0x06): a chunk of the TLS handshake byte stream at one encryption level, positioned at offset. Reassembling multiple (possibly out-of-order, possibly overlapping) CryptoFrames into a contiguous stream is crypto_stream.v's job, not this one's -- parse_frame only decodes a single wire frame.
struct CryptoStreamReassembler #
struct CryptoStreamReassembler {
mut:
received []u8
pending []CryptoFragment
}
fn (CryptoStreamReassembler) consumed_len #
fn (r &CryptoStreamReassembler) consumed_len() u64
consumed_len returns how many bytes, starting from stream offset 0, are currently contiguous and available via data().
fn (CryptoStreamReassembler) data #
fn (r &CryptoStreamReassembler) data() []u8
data returns the currently-contiguous prefix of the reassembled stream, as an independent COPY -- mutating the returned slice must never be able to corrupt r.received, since V's plain array assignment shares backing storage rather than copying it. Unlike a typical stream reader, CRYPTO frame offsets are never relative to a "read cursor" -- they are absolute from the start of the encryption level's handshake -- so this is a growing snapshot, not a destructive drain.
fn (CryptoStreamReassembler) add #
fn (mut r CryptoStreamReassembler) add(offset u64, data []u8) !
add ingests one CRYPTO frame's (offset, data) into the reassembler. Frames may arrive out of order; both immediately-contiguous and out-of-order fragments are accepted and, for the latter, held until the gap before them closes.
struct DataBlockedFrame #
struct DataBlockedFrame {
pub:
maximum_data u64
}
DataBlockedFrame represents a DATA_BLOCKED frame (type 0x14, RFC 9000 §19.12): informs the peer the sender wanted to send more but was blocked by the connection-level flow control limit maximum_data.
struct DataFrame #
struct DataFrame {
pub:
data []u8
}
DataFrame carries content bytes (§7.2.1). Opaque to this layer -- no interpretation of the bytes happens here.
struct DialParams #
struct DialParams {
pub:
server_name string
ca_bundle_pem string
alpn_protocols []string
transport_parameters QuicTransportParameters
}
DialParams is everything dial() needs beyond what it decides for itself (connection IDs, ClientHello random). transport_parameters is this client's own OFFERED set; dial() overrides its initial_source_connection_id with the freshly-picked scid regardless of what the caller set there.
struct EcnCounts #
struct EcnCounts {
pub:
ect0 u64
ect1 u64
ecn_ce u64
}
struct EcnState #
struct EcnState {
pub mut:
last_ect0 u64
last_ect1 u64
last_ecn_ce u64
}
EcnState tracks the most recent cumulative ECN counts this endpoint has seen reported back to it, purely for bookkeeping/diagnostic parity with what a real ECN-capable implementation would track -- no congestion-control decision may ever be driven by it.
fn (EcnState) note_ack_ecn_counts #
fn (mut s EcnState) note_ack_ecn_counts(counts EcnCounts)
note_ack_ecn_counts records one ACK frame's reported ECN counts. RFC 9000 §13.4.2.1: these are CUMULATIVE totals the peer has ever reported, not per-ACK deltas -- a real ECN-reacting implementation would compare against the previous totals to compute what changed since the last ACK; v1 has no such reaction to drive, so simply recording the latest reported totals is sufficient for its bookkeeping-only purpose.
fn (EcnState) is_validated #
fn (s &EcnState) is_validated() bool
is_validated always reports false in v1 -- see the file-level doc comment. This is the checkpoint any future congestion-control integration (Phase 9+) must consult before reacting to an ECN-CE mark (RFC 9000 §13.4.3); since it can never be true here, no such reaction is possible by construction, regardless of what note_ack_ecn_counts has recorded.
struct FlowControlWindow #
struct FlowControlWindow {
mut:
consumed u64
limit u64
}
FlowControlWindow tracks one DIRECTION's flow-control accounting for what THIS endpoint may SEND, at one scope (a whole connection, or a single stream): how many bytes have been consumed against a limit, and what that limit currently is (raised over time by the PEER's MAX_DATA/MAX_STREAM_DATA frames).
fn (FlowControlWindow) available #
fn (w &FlowControlWindow) available() u64
available returns how many more bytes this window currently permits.
fn (FlowControlWindow) consume #
fn (mut w FlowControlWindow) consume(n u64) !
consume records n more bytes as used against this window, failing if that would exceed the current limit -- callers must check available() (or catch this error) BEFORE actually sending data, never discover the violation only after the fact.
fn (FlowControlWindow) raise_limit #
fn (mut w FlowControlWindow) raise_limit(new_limit u64)
raise_limit updates the window's limit, e.g. on receiving the peer's MAX_DATA/MAX_STREAM_DATA frame. Per RFC 9000 §4.1, a limit update MUST NOT be applied if it is SMALLER than the current limit (limits are monotonically non-decreasing) -- silently ignored, not an error, since a reordered older MAX_DATA/MAX_STREAM_DATA frame arriving after a newer one is entirely normal, not a protocol violation.
struct GoawayFrame #
struct GoawayFrame {
pub:
id u64
}
GoawayFrame carries a single generic id whose meaning depends on direction: a client-initiated bidirectional QUIC stream ID when sent server-to-client, or a push ID when sent client-to-server (§7.2.6). Distinguishing the two, and validating a received stream-ID-flavored GOAWAY, needs connection role context this layer doesn't have -- see goaway_id_is_valid_client_initiated_bidi_stream_id for the one piece of that validation that IS pure/role-independent.
struct H3ControlStreamState #
struct H3ControlStreamState {
mut:
seen_first_frame bool
}
H3ControlStreamState tracks the one piece of control-stream discipline that depends on frame ORDER rather than just frame type: SETTINGS "MUST be sent as the first frame of each control stream... and MUST NOT be sent subsequently" (§6.2.1/§7.2.4). One instance is owned per control stream a connection has (its own outgoing one, and the peer's incoming one); frames arriving on a DIFFERENT stream never touch this type at all (is_h3_frame_valid_on_stream already rejects a SETTINGS frame received on a non-control stream independently of this state).
fn (H3ControlStreamState) note_frame #
fn (mut s H3ControlStreamState) note_frame(frame H3Frame) !
note_frame must be called, in stream order, for every frame decoded on this control stream -- BEFORE the caller acts on the frame's contents. Returns an error, carrying an H3ErrorCode via error_with_code, when:- this is the first frame on the stream and it is not SETTINGS (§6.2.1: "If the first frame of the control stream is any other frame type, this MUST be treated as a connection error of type H3_MISSING_SETTINGS"). A grease/unknown H3RawFrame as the first frame also triggers this -- §9: "where a known frame type is required to be in a specific location... an unknown frame type does not satisfy that requirement and SHOULD be treated as an error";- this is a SECOND (or later) SETTINGS frame on the same stream (§7.2.4: "it MUST NOT be sent subsequently... the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED").
struct H3FrameDecodeResult #
struct H3FrameDecodeResult {
pub:
has_frame bool
frame H3Frame = H3RawFrame{}
consumed int
}
H3FrameDecodeResult is the result of one H3FrameDecoder.next() call. has_frame is false whenever buf's buffered bytes do not yet contain a complete frame -- NOT an error condition, since RFC 9114 explicitly allows a frame's Type/Length/Payload to be split arbitrarily across QUIC STREAM frames (§7, "unlike QUIC frames, HTTP/3 frames can span multiple packets"). frame/consumed are meaningless when has_frame is false (defaults to a zero-value H3RawFrame / 0).
struct H3FrameDecoder #
struct H3FrameDecoder {
mut:
pending []u8
}
H3FrameDecoder incrementally decodes a sequence of HTTP/3 frames from an already-in-order byte stream (e.g. the output of a QUIC StreamReassembler.data()/read cursor). Bytes are pushed as they arrive; complete frames are popped one at a time via next().
fn (H3FrameDecoder) push #
fn (mut d H3FrameDecoder) push(data []u8)
push appends newly-received stream bytes to the decoder's internal buffer. Bytes already fully consumed by a prior next() call are never re-examined; push never itself parses anything.
fn (H3FrameDecoder) next #
fn (mut d H3FrameDecoder) next() !H3FrameDecodeResult
next attempts to decode one complete frame from the front of the currently-buffered bytes. Returns has_frame: false (not an error) if the buffered bytes do not yet contain a full frame; the caller should call push again once more data has arrived and retry. On success, the decoded frame's bytes are removed from the internal buffer, so a subsequent next() call continues from wherever this one left off. Returns an error for a genuine protocol violation (see decode_h3_frame_payload's doc comment) -- the offending bytes are left in the buffer (not consumed) when this happens, so calling next() again is harmless and simply reproduces the same error (decoding is a pure function of the buffered bytes). That said, per RFC 9114 §8, any such error is a CONNECTION error: the caller's real obligation is to close the whole HTTP/3 connection, not to keep feeding this decoder.
fn (H3FrameDecoder) pending_len #
fn (d &H3FrameDecoder) pending_len() int
pending_len returns how many not-yet-decoded bytes are currently buffered -- useful for a caller wanting to bound how much unconsumed data it will let a peer accumulate before a complete frame arrives (mirrors crypto_stream.v's max_crypto_stream_buffered_bytes rationale; no such cap is enforced BY this file itself -- see the "Known design notes" row this phase's matrix section adds on why DATA frames in particular must stay uncapped here).
struct H3RawFrame #
struct H3RawFrame {
pub:
frame_type u64
payload []u8
}
H3RawFrame preserves any frame this layer does not assign a dedicated struct to: BOTH the reserved-for-grease codepoints (§7.2.8, MUST be ignored) and any genuinely unrecognized extension frame type (§9, MUST also be ignored, per "Implementations MUST ignore unknown or unsupported values in all extensible protocol elements" -- read directly from §9, not assumed by analogy with HTTP/2). H2-carryover reserved types (h3_reserved_h2_carryover_frame_types) are NOT represented this way -- decoding one of those returns an error instead, since §7.2.8 requires a connection error on receipt, not silent tolerance.
struct H3Setting #
struct H3Setting {
pub:
identifier u64
value u64
}
H3Setting is one (identifier, value) pair from a SETTINGS frame's payload (§7.2.4, Figure 7's inner "Setting" struct).
struct H3UnidirectionalStreamHeader #
struct H3UnidirectionalStreamHeader {
pub:
kind H3UnidirectionalStreamKind
raw_type u64
push_id ?u64
consumed int
}
H3UnidirectionalStreamHeader is the decoded result of the first bytes of a unidirectional QUIC stream. push_id is only ever set when kind == .push (RFC 9114 §6.2.2, Figure 2). consumed is the number of bytes of the input buffer the header occupied, so the caller can advance past it to whatever HTTP/3 frames or push-stream data follows.
struct HandshakeCompletionState #
struct HandshakeCompletionState {
mut:
own_finished_sent bool
peer_finished_verified bool
handshake_done_received bool
sent_first_handshake_packet bool
}
RFC 9001 §4.1.2 — the TLS/QUIC handshake has two distinct completion checkpoints, and key-discard timing (RFC 9001 §4.9) depends on which one has occurred:
- "complete": this client has BOTH sent its own Finished message AND verified the server's Finished message. Neither alone is sufficient -- sending your own Finished without verifying the peer's proves nothing about the peer's identity/state, and verifying the peer's without having sent your own means the peer cannot yet trust that THIS side has completed the handshake either.- "confirmed": this client has received a HANDSHAKE_DONE frame -- a 1-RTT-only frame (RFC 9000 §19.20), so this can only happen AFTER 1-RTT keys already exist. RFC 9001 §4.9.2 requires discarding Handshake keys only once the handshake is CONFIRMED, not merely complete: "complete" alone doesn't prove the SERVER has seen this client's Finished, so a Handshake-space retransmission of it may still be needed until confirmation proves otherwise.
RFC 9001 also permits an ALTERNATE confirmation path (a client MAY treat receipt of an ACK for a 1-RTT packet it sent as confirmation, without waiting for HANDSHAKE_DONE specifically -- useful if HANDSHAKE_DONE itself is lost). Deliberately NOT implemented here: v1 always waits for HANDSHAKE_DONE, a legitimate (if slightly less loss-tolerant) subset of spec-compliant behavior.
A THIRD, separate key-discard checkpoint exists alongside these two: RFC 9001 §4.9.1 requires discarding Initial keys once this client has sent its FIRST Handshake-space packet -- independent of complete/ confirmed, and normally reached much earlier (as soon as Handshake-level keys exist and there's anything to send in that space, e.g. an ACK for the server's Handshake packets).
fn (HandshakeCompletionState) mark_own_finished_sent #
fn (mut s HandshakeCompletionState) mark_own_finished_sent()
mark_own_finished_sent records that this client has sent its own Finished message.
fn (HandshakeCompletionState) mark_peer_finished_verified #
fn (mut s HandshakeCompletionState) mark_peer_finished_verified()
mark_peer_finished_verified records that this client has verified the server's Finished message (Phase 2's process_finished having succeeded).
fn (HandshakeCompletionState) is_complete #
fn (s &HandshakeCompletionState) is_complete() bool
is_complete reports RFC 9001 §4.1.2's "handshake complete" checkpoint.
fn (HandshakeCompletionState) mark_handshake_done_received #
fn (mut s HandshakeCompletionState) mark_handshake_done_received()
mark_handshake_done_received records receipt of a HANDSHAKE_DONE frame. This function only tracks the state transition; validating that receiving one NOW is legal (e.g. rejecting it before the handshake is even complete) is the caller's frame-dispatch responsibility, not re-checked here.
fn (HandshakeCompletionState) is_confirmed #
fn (s &HandshakeCompletionState) is_confirmed() bool
is_confirmed reports RFC 9001 §4.1.2/§4.9.2's "handshake confirmed" checkpoint -- the trigger for discarding Handshake keys. Also requires is_complete() as a safety net: a HANDSHAKE_DONE frame should never legitimately arrive before completion, but confirmed implying complete is a sane invariant to enforce here regardless of caller ordering mistakes upstream.
fn (HandshakeCompletionState) mark_sent_first_handshake_packet #
fn (mut s HandshakeCompletionState) mark_sent_first_handshake_packet()
mark_sent_first_handshake_packet records that this client has sent its first Handshake-space packet.
fn (HandshakeCompletionState) should_discard_initial_keys #
fn (s &HandshakeCompletionState) should_discard_initial_keys() bool
should_discard_initial_keys reports whether RFC 9001 §4.9.1's condition for discarding Initial keys has been met. Independent of is_complete/ is_confirmed -- it typically happens well before either.
fn (HandshakeCompletionState) should_discard_handshake_keys #
fn (s &HandshakeCompletionState) should_discard_handshake_keys() bool
should_discard_handshake_keys reports whether RFC 9001 §4.9.2's condition for discarding Handshake keys has been met: the handshake is CONFIRMED, not merely complete.
struct HandshakeDoneFrame #
struct HandshakeDoneFrame {}
HandshakeDoneFrame represents a HANDSHAKE_DONE frame (type 0x1e, RFC 9000 §19.20): sent only by a server, only once, to signal handshake confirmation (RFC 9001 §4.1.2) -- carries no fields. A client MUST treat receipt of one as a connection error of type PROTOCOL_VIOLATION (RFC 9000 §19.20); parse_frame itself has no connection-role awareness to enforce that, so it is the caller's (Phase 9 QuicConn's) job, same division as every other role-dependent check this module defers (see coalesce.v's analogous note).
struct HandshakeMessage #
struct HandshakeMessage {
pub:
typ HandshakeType
body []u8
}
struct HandshakeSecrets #
struct HandshakeSecrets {
pub:
handshake_secret []u8
client_secret []u8 // client_handshake_traffic_secret
server_secret []u8 // server_handshake_traffic_secret
}
struct HeadersFrame #
struct HeadersFrame {
pub:
encoded_field_section []u8
}
HeadersFrame carries a QPACK-encoded field section (§7.2.2). QPACK decoding is Phase 11's responsibility; this layer only extracts the still-encoded bytes.
struct IdleTimeoutState #
struct IdleTimeoutState {
pub mut:
last_reset ?u64 // time.sys_mono_now()-sourced instant
}
IdleTimeoutState tracks when the idle timer last restarted -- RFC 9000 §10.1: "An endpoint restarts its idle timer when a packet from its peer is received and processed successfully" (RECEIVE side: unconditional, any packet) "[An endpoint] also restarts its idle timer when sending an ack-eliciting packet if no other ack-eliciting packets have been sent since last receiving and processing a packet" (SEND side: only ack-eliciting matters there, though restarting on every send, as this type does, is a superset -- more lenient, never less, so it still satisfies the MUST). The ack-eliciting condition in the RFC text is SEND-only; an earlier version of this type had it backwards, gating the RECEIVE side on ack-eliciting instead and never restarting on a non-ack-eliciting receive (e.g. a lone ACK frame) -- found via a maintainer "Local AI Review" on PR #28083.
fn (IdleTimeoutState) note_packet_sent #
fn (mut s IdleTimeoutState) note_packet_sent(now u64)
note_packet_sent restarts the idle timer -- ANY packet sent qualifies.
fn (IdleTimeoutState) note_packet_received #
fn (mut s IdleTimeoutState) note_packet_received(now u64)
note_packet_received restarts the idle timer -- ANY successfully processed received packet qualifies, per RFC 9000 §10.1 (see this type's own doc comment); there is no ack-eliciting condition on the receive side.
fn (IdleTimeoutState) is_idle #
fn (s &IdleTimeoutState) is_idle(timeout ?time.Duration, now u64, connection_start u64) bool
is_idle reports whether timeout has elapsed since the timer was last restarted. timeout being none (both peers disabled it) never expires. Before the very first restart (no packet sent or received yet), elapsed time is measured from connection_start.
struct InitialSecrets #
struct InitialSecrets {
pub:
client []u8
server []u8
}
InitialSecrets holds the client and server Initial secrets derived per RFC 9001 §5.2. These are inputs to Phase 3's packet-protection key derivation (quic_key/quic_iv/quic_hp via hkdf_expand_label), not keys themselves.
struct KeyResolution #
struct KeyResolution {
pub:
keys QuicPacketProtectionKeys
secret []u8
packet_phase bool
// is_new_update is true when this resolution represents a genuine new
// key update (the packet's phase differs from current, and its packet
// number is higher than anything seen in the current phase) -- AS
// OBSERVED AT RESOLVE TIME.
is_new_update bool
// is_previous_phase is true when this resolution represents a
// reordered packet from BEFORE the current phase (the packet's phase
// differs from current, and its packet number is lower than anything
// seen in the current phase) -- AS OBSERVED AT RESOLVE TIME. Mutually
// exclusive with is_new_update.
is_previous_phase bool
// generation is the ABSOLUTE key generation this resolution belongs to
// (generation 0 is the first 1-RTT secret, incrementing by exactly 1
// per accepted update), computed from s.current_generation AT RESOLVE
// TIME. Unlike the phase bit (which has period-2 parity and cannot
// tell generation N apart from generation N+2), an absolute generation
// number stays correct even if OTHER resolutions commit in between --
// it identifies a real, specific generation, not a value relative to
// "whatever is current right now". note_successful_decrypt trusts this
// field specifically because of that: comparing it against the
// CURRENT s.current_generation at commit time is always correct,
// regardless of how many other commits happened first.
generation int
}
KeyResolution is resolve_read_keys' result: which keys to attempt decrypting an incoming packet with, and how the situation looked AT RESOLVE TIME. is_new_update/is_previous_phase are informational only -- a caller may use them for logging, but note_successful_decrypt does NOT trust them for its own state transition (see that function's doc comment for why: they can go stale between resolution and commit).
struct KeyUpdateState #
struct KeyUpdateState {
mut:
current_phase bool
current_keys QuicPacketProtectionKeys
current_secret []u8
previous_keys ?QuicPacketProtectionKeys
min_pn_in_current_phase ?u64
updates_accepted int
// current_generation is an ABSOLUTE count of accepted key updates
// (generation 0 is the first 1-RTT secret). See KeyResolution.generation
// for why note_successful_decrypt compares this instead of the phase
// bit: the phase bit alone cannot tell generation N apart from
// generation N+2 (it has period-2 parity), which is exactly what let a
// stale generation-0 commit corrupt bookkeeping after a second update.
current_generation int
}
KeyUpdateState tracks ONE direction's (specifically: the direction used to READ packets FROM the peer) current and previous 1-RTT packet protection keys, plus the packet-number bookkeeping RFC 9001 §6.5 requires to tell a genuine new update apart from a reordered packet still using the previous phase.
fn (KeyUpdateState) current_phase_bit #
fn (s &KeyUpdateState) current_phase_bit() bool
current_phase_bit reports the Key Phase bit this side currently expects on an incoming 1-RTT packet that uses the CURRENT (not previous or next) generation of keys.
fn (KeyUpdateState) generation #
fn (s &KeyUpdateState) generation() int
generation reports the ABSOLUTE key generation this side has committed on the READ direction (see KeyResolution.generation's own doc comment for why this is tracked as an absolute counter rather than the period-2 phase bit). Used by the WRITE side (conn.v's sync_write_keys_to_peer_update, RFC 9001 §6.2) to know how many peer-initiated updates this connection's own send keys still need to catch up to -- the read and write generation chains are independent (§6.1), so the write side cannot derive this from its own state alone.
fn (KeyUpdateState) resolve_read_keys #
fn (s &KeyUpdateState) resolve_read_keys(packet_phase bool, packet_number u64) !KeyResolution
resolve_read_keys decides which keys to TRY decrypting an incoming 1-RTT packet with, given its key phase bit (already revealed by header protection removal) and its reconstructed packet number. Per RFC 9001 §6.5: a phase matching the current one always uses the current keys. A MISMATCHED phase is resolved by packet number, not by the mismatch alone -- lower than anything already seen in the current phase means a reordered packet from BEFORE the peer's update (use the retained previous keys, if any); higher means a genuine new update (derive and try the next keys).
This function does NOT mutate any state and does NOT itself authenticate anything -- resolving which keys to attempt is a plaintext-visible decision (the key phase bit and packet number are both unprotected once header protection is removed), never a substitute for the AEAD check that follows. The caller MUST attempt AEAD decryption with the returned keys and must call note_successful_decrypt ONLY if that decryption actually succeeds -- never on the resolution alone (RFC 9001 §6.5's warning against turning key-update handling into a decryption oracle).
fn (KeyUpdateState) note_successful_decrypt #
fn (mut s KeyUpdateState) note_successful_decrypt(resolution KeyResolution, packet_number u64) !
note_successful_decrypt commits the outcome of a resolve_read_keys resolution AFTER the caller has verified it by successfully AEAD-decrypting a real packet with it -- never before.
Deliberately does NOT trust resolution.is_new_update/is_previous_phase, and deliberately does NOT compare resolution.packet_phase against s.current_phase either -- both reflect the situation as observed AT RESOLVE TIME, and this function can be called with a STALE resolution if a caller resolves more than one packet (e.g. several packets coalesced into one datagram) before committing either. The phase bit specifically cannot be trusted for this comparison even when re-read fresh: it has period-2 parity, so after a SECOND update has committed, a genuinely old generation-0 packet's phase bit coincidentally matches the new generation-2 current phase, and comparing bits alone would mis-commit it as belonging to generation 2. resolution.generation is an ABSOLUTE counter (not subject to that parity collision) computed once at resolve time from what was genuinely current then -- comparing IT against the CURRENT s.current_generation is correct regardless of how many resolutions were computed before this one, or in what order they commit.
fn (KeyUpdateState) discard_previous_keys #
fn (mut s KeyUpdateState) discard_previous_keys()
discard_previous_keys drops the retained previous-phase keys. RFC 9001 §6.5 recommends retaining them for about 3x the probe timeout after receiving a packet in the new phase, then discarding -- that timing judgment needs RTT/PTO estimation (Phase 7), so this function only performs the mechanical discard; deciding WHEN to call it is a later phase's job.
struct LossDetectionSpaceState #
struct LossDetectionSpaceState {
pub mut:
sent_packets map[u64]SentPacketInfo
largest_acked_packet ?u64
loss_time ?u64
time_of_last_ack_eliciting_packet ?u64
}
LossDetectionSpaceState is the per-packet-number-space slice of loss detection state (RFC 9002 Appendix A.1) -- a DIFFERENT decomposition than packet_number_space.v's PacketNumberSpaceState, which exists purely for packet-number encoding/decoding. Both happen to track "largest packet number the peer has acked in this space" because RFC 9002's own pseudocode structure keeps loss detection's copy separate from whatever an implementation's packet-number codec needs -- not a duplicated bug surface, since both are simple non-regressing trackers fed from the same ACK frame at the same time by whatever future glue code (Phase 9) reads one incoming ACK once.
struct LossTimeoutResult #
struct LossTimeoutResult {
pub:
lost []SentPacketInfo
pto_fired bool
pto_space QuicPacketNumberSpace
}
LossTimeoutResult reports what firing the loss-detection timer found: EITHER a time-threshold loss batch (pto_fired == false, lost non-empty) OR a genuine PTO expiry (pto_fired == true) naming the space a probe belongs in -- RFC 9002 Appendix A.9's OnLossDetectionTimeout always re-checks loss_time_and_space FIRST, since the timer may have been armed for a loss deadline that a race already resolved differently by the time it fires. Deliberately carries NO persistent_congestion verdict: RFC 9002 §7.6.2 opens with "A sender establishes persistent congestion after the receipt of an acknowledgment" -- a time-threshold loss batch found here, with no ACK involved, must never itself trigger the persistent-congestion collapse. Only on_ack_received's own AckProcessingResult carries that verdict.
struct MaxDataFrame #
struct MaxDataFrame {
pub:
maximum_data u64
}
MaxDataFrame represents a MAX_DATA frame (type 0x10, RFC 9000 §19.9): raises the CONNECTION-level limit on how much the receiver of this frame may send in total, across all streams.
struct MaxPushIdFrame #
struct MaxPushIdFrame {
pub:
push_id u64
}
MaxPushIdFrame sets the maximum push ID a server may use (§7.2.7). A v1 client role only ever ENCODES this; decoding exists only so a client can recognize (and, at Phase 12, reject per §7.2.7 "A client MUST treat the receipt of a MAX_PUSH_ID frame as a connection error of type H3_FRAME_UNEXPECTED") a server that incorrectly sends one.
struct MaxStreamDataFrame #
struct MaxStreamDataFrame {
pub:
stream_id u64
maximum_stream_data u64
}
MaxStreamDataFrame represents a MAX_STREAM_DATA frame (type 0x11, RFC 9000 §19.10): raises the STREAM-level limit on stream_id.
struct MaxStreamsFrame #
struct MaxStreamsFrame {
pub:
direction StreamDirection
maximum_streams u64
}
MaxStreamsFrame represents a MAX_STREAMS frame (type 0x12 bidirectional, 0x13 unidirectional, RFC 9000 §19.11): raises how many concurrent streams of direction the receiver of this frame may have open.
struct NewRenoCongestionControl #
struct NewRenoCongestionControl {
pub mut:
congestion_window u64
bytes_in_flight u64
ssthresh ?u64
congestion_recovery_start_time ?u64
}
NewRenoCongestionControl is RFC 9002 Appendix B.2's congestion controller state. ssthresh is none until the first congestion event (RFC 9002 §7.2: "ssthresh is initialized to be unbounded"); is_in_slow_start() reports exactly that condition.
fn (NewRenoCongestionControl) is_in_slow_start #
fn (c &NewRenoCongestionControl) is_in_slow_start() bool
is_in_slow_start is RFC 9002 Appendix B.4's congestion_window < ssthresh check -- NOT simply "has a loss ever happened" (ssthresh == none). Those two diverge after persistent congestion: on_packets_lost's persistent-congestion branch collapses congestion_window straight to kMinimumWindow while leaving the just-computed (larger) ssthresh untouched, so cwnd can legitimately fall back BELOW an already-set ssthresh -- at that point NewReno must re-enter slow start (full growth per ack) until cwnd climbs back up to ssthresh, not stay in congestion avoidance just because a loss happened at some earlier point.
fn (NewRenoCongestionControl) on_packet_sent_cc #
fn (mut c NewRenoCongestionControl) on_packet_sent_cc(sent_bytes u64)
on_packet_sent_cc is RFC 9002 Appendix B.3's OnPacketSentCC -- the caller (a future QuicConn) calls this itself for every packet loss_detection.v's on_packet_sent recorded with in_flight == true; the two are not automatically linked (see loss_detection.v's own on_packet_sent doc comment).
fn (NewRenoCongestionControl) on_packets_acked #
fn (mut c NewRenoCongestionControl) on_packets_acked(acked_packets []SentPacketInfo)
on_packets_acked is RFC 9002 Appendix B.4's OnPacketsAcked: grows the congestion window for every newly-acked, in-flight packet that is NOT part of the currently-ongoing recovery episode -- slow start grows by the full acked byte count, congestion avoidance grows proportionally (RFC 9002's standard AIMD increase).
fn (NewRenoCongestionControl) on_packets_lost #
fn (mut c NewRenoCongestionControl) on_packets_lost(lost_packets []SentPacketInfo, persistent_congestion bool, now u64)
on_packets_lost is RFC 9002 Appendix B.6's OnPacketsLost: removes lost bytes from bytes_in_flight, reacts once via on_congestion_event (keyed off the LARGEST lost packet number's send time, per spec), and -- if persistent_congestion is true (loss_detection.v's is_persistent_congestion having already made that determination) -- collapses the window straight to kMinimumWindow and clears the recovery marker, a distinctly harsher reset than the ordinary ssthresh-halving path above, exercised only in this branch.
struct PacketNumberSpaceState #
struct PacketNumberSpaceState {
pub mut:
// next_send_pn is the packet number this side will use for its NEXT
// packet sent in this space. QUIC packet numbers start at 0 and
// increase by exactly 1 per packet sent within a space (RFC 9000
// §12.3) -- never reused, never skipped, even across packets
// coalesced into the same datagram.
next_send_pn u64
// largest_received is the largest packet number this side has
// successfully processed (header-unprotected AND AEAD-decrypted) in
// this space, or none if no packet has been processed yet -- the
// `largest_pn` decode_packet_number needs when reconstructing a
// peer's truncated packet number in this same space.
largest_received ?u64
// largest_acked_by_peer is the largest packet number, IN THIS SPACE,
// that the peer's most recent ACK frame has acknowledged, or none if
// nothing has been acked yet -- the `largest_acked` encode_packet_number
// needs when choosing how many bytes to encode a new outgoing packet
// number with.
largest_acked_by_peer ?u64
}
PacketNumberSpaceState tracks everything specific to encoding/decoding packet numbers within ONE space. A connection holds exactly three of these (see QuicPacketNumberSpaces below) -- never fewer, never shared across spaces, never reset back to a shared/global counter.
fn (PacketNumberSpaceState) next_packet_number #
fn (mut s PacketNumberSpaceState) next_packet_number() !u64
next_packet_number returns the packet number to use for the next outgoing packet in this space and advances the counter. A caller must never construct or send two packets sharing the same (space, packet_number) pair.
Rejects once the next packet number would exceed max_packet_number (RFC 9000 §12.3: this packet number space is exhausted, and the caller MUST stop sending in it) -- matching encode_packet_number's own convention, rather than silently handing out a packet number the encoder would reject one step later, or letting the counter wrap past u64::MAX into a REUSED packet number.
fn (PacketNumberSpaceState) note_received #
fn (mut s PacketNumberSpaceState) note_received(pn u64)
note_received records that packet number pn was successfully processed in this space, updating largest_received if pn is now the largest seen. Packets may legitimately arrive out of order, so this must only ever advance, never regress on a smaller, later-arriving packet number.
fn (PacketNumberSpaceState) note_peer_acked #
fn (mut s PacketNumberSpaceState) note_peer_acked(largest_in_ack u64)
note_peer_acked records that the peer's most recent ACK frame in this space acknowledged up to largest_in_ack. Same non-regression rule as note_received: a peer's own ACK frames can themselves arrive out of order, and an older ACK arriving after a newer one must not walk this value backwards.
struct PaddingFrame #
struct PaddingFrame {
pub:
length int
}
PaddingFrame represents one or more consecutive PADDING (type 0x00) bytes, collapsed into a single frame for convenience. This is purely an API choice on the parsing side -- each 0x00 byte remains independently a valid, semantically empty PADDING frame on the wire; nothing here changes wire compatibility, it only changes how a run of them is reported back to the caller.
struct ParsedCertificate #
struct ParsedCertificate {
pub:
certificate_request_context []u8
certificate_list []CertificateEntry
}
struct ParsedCertificateVerify #
struct ParsedCertificateVerify {
pub:
algorithm u16
signature []u8
}
struct ParsedHelloRetryRequest #
struct ParsedHelloRetryRequest {
pub:
cipher_suite u16
selected_version u16
// ?u16, not u16: RFC 8446 §4.1.4 lets an HRR request only a cookie
// round-trip with no key_share at all, when the client's already-
// offered share is acceptable to the server -- key_share is not
// mandatory in every HelloRetryRequest, only supported_versions is.
selected_group ?u16
cookie ?[]u8
extensions []TlsExtension
}
struct ParsedServerHello #
struct ParsedServerHello {
pub:
random []u8
cipher_suite u16
selected_version u16
key_share_group u16
key_share_key_exchange []u8
extensions []TlsExtension
}
struct PingFrame #
struct PingFrame {}
PingFrame represents a PING (type 0x01) frame: no fields, ack-eliciting.
struct PollResult #
struct PollResult {
pub mut:
events []QuicEvent
outgoing []QuicDatagram
next_timeout ?u64
}
PollResult reports everything one poll()/process_timeouts() call produced: events, datagrams the caller must now send, and when to next call process_timeouts() if nothing else arrives first (none means no timer is currently armed).
struct PreferredAddress #
struct PreferredAddress {
pub:
ipv4_address [4]u8
ipv4_port u16
ipv6_address [16]u8
ipv6_port u16
connection_id []u8
stateless_reset_token []u8 // exactly 16 bytes
}
PreferredAddress is the server-only preferred_address transport parameter's value (RFC 9000 §18.2, Figure 22). Never sent by a client; encode_transport_parameters doesn't reject a client accidentally setting it (see the doc comment there), so callers on the client side simply must not populate it.
struct PushPromiseFrame #
struct PushPromiseFrame {
pub:
push_id u64
encoded_field_section []u8
}
PushPromiseFrame carries a promised request's push ID and QPACK-encoded header section (§7.2.5). A v1 client role only ever DECODES this (see this file's module-level scope note); no encoder is provided.
struct QpackApplyInstructionResult #
struct QpackApplyInstructionResult {
pub:
applied bool
consumed int
decoder_instructions []u8
}
QpackApplyInstructionResult is the result of processing one encoder- stream instruction (RFC 9204 §4.3): whether a complete instruction was available, how many bytes it consumed, and any decoder-stream bytes this decoder wants to send in response (an Insert Count Increment, per this decoder's policy of acknowledging every insertion immediately -- see apply_encoder_instruction's doc comment for why).
struct QpackDecodeFieldSectionResult #
struct QpackDecodeFieldSectionResult {
pub:
blocked bool
lines []QpackFieldLine
decoder_instructions []u8
}
QpackDecodeFieldSectionResult is the result of decoding one encoded field section (RFC 9204 §2.2): either it was blocked (Required Insert Count not yet satisfied, RFC 9204 §2.1.2/§2.2.1 -- not an error, the caller should retry once more encoder-stream data has arrived) or it produced field lines plus any decoder-stream bytes to send (a Section Acknowledgment, RFC 9204 §4.4.1).
struct QpackDecodedDecoderInstruction #
struct QpackDecodedDecoderInstruction {
pub:
has_instruction bool
instr QpackDecoderInstruction = QpackSectionAck{}
consumed int
}
QpackDecodedDecoderInstruction is the result of attempting to decode one decoder-stream instruction -- mirrors QpackDecodedEncoderInstruction's shape and reasoning exactly (qpack_encoder_instructions.v).
struct QpackDecodedEncoderInstruction #
struct QpackDecodedEncoderInstruction {
pub:
has_instruction bool
instr QpackEncoderInstruction = QpackDuplicate{}
consumed int
}
QpackDecodedEncoderInstruction is the result of attempting to decode one encoder-stream instruction: has_instruction is false when buf does not yet hold a complete instruction (genuinely need more bytes, not an error -- see decode_qpack_encoder_instruction's doc comment). instr's default value is never meaningful when has_instruction is false; V sum types require SOME default to satisfy the struct literal.
struct QpackDecodedFieldLine #
struct QpackDecodedFieldLine {
pub:
line QpackFieldLine
referenced_index ?u64
consumed int
}
QpackDecodedFieldLine is one decoded field line plus, if it referenced the dynamic table, the absolute index of that reference -- the caller (the decoder driver) needs this to verify it does not exceed the declared Required Insert Count (RFC 9204 §2.2.3) and to track it for eventual Section Acknowledgment.
struct QpackDecoder #
struct QpackDecoder {
mut:
dynamic_table QpackDynamicTable
max_table_capacity u64
}
QpackDecoder is a QPACK decoder's state (RFC 9204 §2.2): a dynamic table mirroring the encoder's, decoded via the encoder instructions this decoder receives, plus this endpoint's own configured maximum dynamic table capacity (its own outgoing SETTINGS_QPACK_MAX_TABLE_CAPACITY, needed for the Required Insert Count wraparound math, RFC 9204 §4.5.1.1).
fn (QpackDecoder) apply_encoder_instruction #
fn (mut d QpackDecoder) apply_encoder_instruction(buf []u8) !QpackApplyInstructionResult
apply_encoder_instruction decodes and applies ONE instruction from encoder-stream bytes starting at buf[0] (RFC 9204 §4.3), mutating this decoder's mirrored dynamic table. Returns applied: false (not an error) when buf does not yet hold a complete instruction, matching this module's established resumable-parsing contract. Every error -- including a malformed instruction decode_qpack_encoder_instruction itself rejects, not only ones raised while applying it -- carries QpackErrorCode.encoder_stream_error (RFC 9204 §2.2.3, §3.2.2: every failure mode an encoder instruction can trigger on the decoder side maps to this one code). Malformed input previously fell through decode_qpack_encoder_instruction's old none-only contract and came back here as applied: false, indistinguishable from "just needs more bytes" -- silently hanging instead of raising the required connection error (self-found via GPT-5.6 Luna review, Phase-R reproduced before this fix). After every insertion or duplication, this decoder immediately emits an Insert Count Increment of 1 (RFC 9204 §2.2.2.3 leaves the exact timing to decoder policy; "emit after adding each new entry" is explicitly named as the timeliest, if not the most bandwidth-efficient, choice -- picked here for simplicity and testable correctness over coalescing).
fn (QpackDecoder) decode_field_section #
fn (mut d QpackDecoder) decode_field_section(stream_id u64, buf []u8) !QpackDecodeFieldSectionResult
decode_field_section decodes one already-complete encoded field section (e.g. a HEADERS frame's payload, RFC 9114 §7.2.2 -- already fully buffered by the time H3FrameDecoder hands it over, so this needs no resumable-parsing contract of its own; see decode_field_section_prefix and decode_qpack_field_line's doc comments for that reasoning). Returns blocked: true (not an error) if this decoder's Insert Count has not yet reached the section's declared Required Insert Count (RFC 9204 §2.1.2/§2.2.1) -- the caller should retry once more encoder-stream data has been applied. Verifies every reference stays within the declared Required Insert Count (§2.2.3) and that the declared count matches what was actually referenced (§2.2.1 -- this project enforces the "larger than expected" MAY-error case as a hard error, the same established preference for a stricter reading of an offered MAY used elsewhere in this codebase's QUIC/HTTP-3 work).
struct QpackDuplicate #
struct QpackDuplicate {
pub:
rel_index u64
}
QpackDuplicate is the Duplicate instruction (RFC 9204 §4.3.4). rel_index is a relative index in the same encoder-instruction context as QpackInsertWithNameRef.name_index.
struct QpackDynamicTable #
struct QpackDynamicTable {
mut:
entries []QpackDynamicTableEntry // FIFO; entries[0] is the oldest entry still present
dropped int // count of entries evicted so far == absolute index of entries[0], if any
capacity int
cur_size int
}
QpackDynamicTable is a QPACK dynamic table (RFC 9204 §3.2): a FIFO of entries addressed by a permanent absolute index (§3.2.4), with capacity- bounded eviction from the oldest end (§3.2.2).
The SAME struct backs both an encoder's and a decoder's table, but their insert obligations differ (RFC 9204 draws this distinction implicitly, by describing eviction from the encoder's point of view in §2.1.1/§3.2.2 while requiring the decoder to independently apply the identical instructions it receives): a decoder mirrors whatever the encoder instructs unconditionally (insert/set_capacity here), while an encoder MUST first confirm via can_insert that it would not have to evict a non-evictable entry to make room -- if can_insert returns false, the encoder must not call insert at all (fall back to a literal representation instead). can_insert and the ref-count methods (add_ref/release_ref) are therefore only ever meaningful when this table is playing the encoder role; a decoder's table never calls them.
fn (QpackDynamicTable) insert_count #
fn (t &QpackDynamicTable) insert_count() int
insert_count is the total number of entries ever inserted (RFC 9204 "Insert Count"), including ones since evicted -- the absolute index the NEXT inserted entry will receive.
fn (QpackDynamicTable) size #
fn (t &QpackDynamicTable) size() int
size is the dynamic table's current total size in bytes (RFC 9204 §3.2.1), the sum of qpack_entry_size over every entry still present.
fn (QpackDynamicTable) capacity #
fn (t &QpackDynamicTable) capacity() int
capacity is the dynamic table's current capacity in bytes (RFC 9204 §3.2.2), the upper bound size() is kept at or under via eviction.
fn (QpackDynamicTable) can_insert #
fn (t &QpackDynamicTable) can_insert(name string, value string, known_received_count int) bool
can_insert reports whether inserting name/value right now would require evicting a non-evictable entry (RFC 9204 §2.1.1: an entry with absolute index >= known_received_count, or with any outstanding reference, cannot be evicted). An encoder MUST check this before insert-ing; a decoder never needs to, since it always applies instructions its peer encoder already validated this way.
fn (QpackDynamicTable) insert #
fn (mut t QpackDynamicTable) insert(name string, value string) !int
insert adds a new entry, first evicting entries from the oldest end (unconditionally -- see this struct's module doc comment for who may safely call this without first checking can_insert) until it fits within capacity. Returns the new entry's absolute index. Errors if the entry alone is larger than the table's capacity (RFC 9204 §3.2.2: the decoder MUST treat this as QPACK_ENCODER_STREAM_ERROR -- the caller maps this generic error to that code, since the QPACK error taxonomy isn't this file's concern).
fn (QpackDynamicTable) duplicate #
fn (mut t QpackDynamicTable) duplicate(rel_index u64) !int
duplicate reinserts the entry at encoder-instruction-context relative index rel_index (RFC 9204 §4.3.4) as a new entry with a fresh absolute index, without needing its name/value re-transmitted on the wire by the caller.
fn (QpackDynamicTable) can_set_capacity #
fn (t &QpackDynamicTable) can_set_capacity(new_capacity int, known_received_count int) bool
can_set_capacity reports whether reducing capacity to new_capacity right now would require evicting a non-evictable entry (RFC 9204 §2.1.1: an entry with absolute index >= known_received_count, or with any outstanding reference, cannot be evicted) -- the same evictability rule can_insert simulates before an insertion, applied here to a capacity reduction instead. An encoder MUST check this before calling set_capacity with a smaller value; mirroring insert/can_insert, set_capacity itself does not check, so the caller controls whether a failed check means "reject" or some other recovery.
fn (QpackDynamicTable) set_capacity #
fn (mut t QpackDynamicTable) set_capacity(new_capacity int)
set_capacity applies a new dynamic table capacity (RFC 9204 §4.3.1/ §3.2.2), evicting entries from the oldest end until size fits, which can clear the table entirely at capacity 0. Whether new_capacity itself is within the peer's configured maximum (§3.2.3) is the caller's check -- this table has no notion of that external limit. Callers reducing capacity MUST check can_set_capacity first (see its doc comment) -- this evicts unconditionally, exactly like insert.
fn (QpackDynamicTable) get #
fn (t &QpackDynamicTable) get(abs_index int) !QpackDynamicTableEntry
get returns the entry at absolute index abs_index (RFC 9204 §3.2.4). Errors if it has already been evicted or has not been inserted yet -- the caller maps this to QPACK_DECOMPRESSION_FAILED or QPACK_ENCODER_STREAM_ERROR per RFC 9204 §2.2.3, depending on which stream the reference came from.
fn (QpackDynamicTable) add_ref #
fn (mut t QpackDynamicTable) add_ref(abs_index int) !
add_ref records one more outstanding unacknowledged reference to the entry at abs_index (RFC 9204 §2.1.1), blocking its eviction until a matching release_ref. Only meaningful on an encoder's own table.
fn (QpackDynamicTable) release_ref #
fn (mut t QpackDynamicTable) release_ref(abs_index int) !
release_ref removes one outstanding reference to the entry at abs_index, recorded by an earlier add_ref (RFC 9204 §2.1.1, §2.2.2.2: released by Section Acknowledgment or Stream Cancellation). A no-op if the entry has no outstanding references (never negative).
fn (QpackDynamicTable) find_exact #
fn (t &QpackDynamicTable) find_exact(name string, value string, before int) ?int
find_exact returns the absolute index of the most recently inserted entry whose name AND value both match, among entries with absolute index less than before (exclusive). Used by an encoder that wants to avoid referencing an entry the decoder may not have acknowledged yet.
fn (QpackDynamicTable) find_name #
fn (t &QpackDynamicTable) find_name(name string, before int) ?int
find_name is find_exact's name-only counterpart, for a literal field line that can still reference the dynamic table for its name.
fn (QpackDynamicTable) resolve_relative_from_insert_count #
fn (t &QpackDynamicTable) resolve_relative_from_insert_count(rel_index u64) !int
resolve_relative_from_insert_count resolves a relative index as used in ENCODER INSTRUCTIONS (RFC 9204 §3.2.5, Figure 2): 0 refers to the most recently inserted entry, shifting as more entries are inserted. Distinct from resolve_relative_from_base below -- conflating the two reference points is the most likely transcription error for this section, so they are deliberately separate functions rather than one parameterized by a "which context" flag.
rel_index arrives straight from the wire (via decode_prefixed_int, itself capped at qpack_max_prefixed_int < 2^62) and so must not be narrowed to int before it is bounds-checked: the comparisons below happen entirely in u64 space against t.insert_count()/t.dropped (always small, since they only ever grow by one real insertion at a time) before the final, by-then-safe narrow to int for indexing.
fn (QpackDynamicTable) resolve_relative_from_base #
fn (t &QpackDynamicTable) resolve_relative_from_base(rel_index u64, base u64) !int
resolve_relative_from_base resolves a relative index as used in FIELD LINE REPRESENTATIONS (RFC 9204 §3.2.5, Figure 3): 0 refers to the entry with absolute index (Base - 1), stable regardless of when the encoded field section is processed relative to encoder-stream instructions. See resolve_relative_from_insert_count's doc comment for why this is a separate function, and for why the u64 arithmetic below is bounds-checked before narrowing (both rel_index and base are wire-derived).
fn (QpackDynamicTable) resolve_post_base #
fn (t &QpackDynamicTable) resolve_post_base(post_index u64, base u64) !int
resolve_post_base resolves a post-Base index (RFC 9204 §3.2.6, Figure 4): 0 refers to the entry with absolute index equal to Base, increasing in the same direction as absolute index. Used for entries inserted while processing the same (or another) field section, referenced after Base was fixed. base + post_index cannot overflow u64: both operands are individually capped below qpack_max_prefixed_int (< 2^62) by decode_prefixed_int, so their sum is safely under 2^63.
struct QpackDynamicTableEntry #
struct QpackDynamicTableEntry {
pub mut:
name string
value string
ref_count int
}
QpackDynamicTableEntry is one entry currently held in a QPACK dynamic table (RFC 9204 §3.2). ref_count is the number of outstanding unacknowledged field-section references to this entry (§2.1.1) -- it is meaningful only on the encoder's own table; a decoder's mirrored table never consults it (see this file's module doc comment).
struct QpackEncodedFieldSection #
struct QpackEncodedFieldSection {
pub:
encoder_instructions []u8
field_section []u8
}
QpackEncodedFieldSection is the result of encoding one field section: bytes for the encoder stream (any new dynamic-table entries this section caused to be inserted) and bytes for the request stream (the field section itself, prefix included).
struct QpackEncoder #
struct QpackEncoder {
mut:
dynamic_table QpackDynamicTable
known_received_count int
peer_max_table_capacity u64
unacked []QpackUnackedSection
}
QpackEncoder is a QPACK encoder's state (RFC 9204 §2.1): a dynamic table plus everything needed to track what the decoder has and has not yet acknowledged. Its encode_field_section uses the always-safe policy RFC 9204 §2.1.2 offers as one valid strategy -- "an encoder can avoid the risk of blocking by only referencing dynamic table entries that have been acknowledged" -- rather than the riskier "reference in-flight entries" strategy Appendix C's sample algorithm leaves as a choice. This means SETTINGS_QPACK_BLOCKED_STREAMS is trivially satisfied (this encoder never risks blocking a stream at all), a deliberate scope decision documented in this phase's conformance-matrix section, not an oversight.
fn (QpackEncoder) set_capacity #
fn (mut e QpackEncoder) set_capacity(new_capacity u64, peer_max_table_capacity u64) ![]u8
set_capacity sets the dynamic table's capacity (RFC 9204 §4.3.1), recording peer_max_table_capacity (from the peer's SETTINGS_QPACK_MAX_TABLE_CAPACITY, RFC 9204 §3.2.3) for future inserts to respect too. Returns the encoder-stream bytes to send. Errors if new_capacity exceeds either the peer's configured maximum or this implementation's own limit, or if reducing to it would require evicting an entry with an outstanding reference or unacknowledged status (RFC 9204 §2.1.1) -- self-found via Luna review: this call used to evict unconditionally, silently corrupting the encoder's own view of the table relative to what it had already told the peer.
fn (QpackEncoder) note_section_acknowledged #
fn (mut e QpackEncoder) note_section_acknowledged(stream_id u64) !
note_section_acknowledged processes a Section Acknowledgment received on the decoder stream (RFC 9204 §4.4.1, §2.1.4): releases every dynamic- table reference the earliest unacknowledged field section on stream_id held, and advances Known Received Count if that section's own Required Insert Count was higher than what this encoder already believed acknowledged. Errors (QPACK_DECODER_STREAM_ERROR, per §4.4.1) if stream_id has no outstanding unacknowledged section.
fn (QpackEncoder) note_stream_cancelled #
fn (mut e QpackEncoder) note_stream_cancelled(stream_id u64)
note_stream_cancelled processes a Stream Cancellation received on the decoder stream (RFC 9204 §4.4.2, §2.2.2.2): releases every dynamic-table reference every unacknowledged field section on stream_id held (there can be more than one, e.g. trailers), WITHOUT advancing Known Received Count (the RFC is explicit: "An encoder cannot infer from this instruction that any updates to the dynamic table have been received").
fn (QpackEncoder) note_insert_count_increment #
fn (mut e QpackEncoder) note_insert_count_increment(increment u64) !
note_insert_count_increment processes an Insert Count Increment received on the decoder stream (RFC 9204 §4.4.3): advances Known Received Count by increment. Errors (QPACK_DECODER_STREAM_ERROR) on an increment of zero, or one that would push Known Received Count past the number of entries this encoder has actually inserted -- checked in u64 space before narrowing, since increment arrives straight off the wire.
fn (QpackEncoder) encode_field_section #
fn (mut e QpackEncoder) encode_field_section(stream_id u64, lines []QpackFieldLine) !QpackEncodedFieldSection
encode_field_section encodes lines for stream_id (RFC 9204 §2.1, Appendix C, adapted to the always-acknowledged-only policy described on QpackEncoder's doc comment). For each line NOT marked never_index: an exact static match is used, else an exact match among already- acknowledged dynamic entries. Otherwise (including every never_index line) a literal is emitted, using a NAME reference (static or acknowledged-dynamic -- available regardless of never_index, since only a fully indexed representation is withheld) when available, and a new dynamic-table entry is inserted for future reuse whenever can_insert allows it (never for a never_index line). Errors only if the encoded prefix's Required Insert Count cannot be represented (RFC 9204 §4.5.1.1) -- unreachable via this policy's own can_insert gate in practice, but encode_ric is pub and this propagates its contract rather than assuming its one caller can never hit it.
struct QpackFieldLine #
struct QpackFieldLine {
pub:
name string
value string
never_index bool
}
QpackFieldLine is one decoded (or to-be-encoded) HTTP field line. never_index is the 'N' bit (RFC 9204 §4.5.4, §7.1.3): when set on a decoded literal, this codebase (a client/server endpoint, never an intermediary) has no further re-encoding obligation regarding it -- see this module's matrix entry for why §7.1.3's re-encoding constraint is N/A here. It is still carried through so a caller CHOOSING to set it on an outgoing literal (e.g. for a sensitive header, see qpack_is_sensitive in qpack_encoder.v) round-trips correctly.
struct QpackFieldSectionPrefix #
struct QpackFieldSectionPrefix {
pub:
required_insert_count u64
base u64
}
QpackFieldSectionPrefix is the decoded Required Insert Count + Base pair that precedes every encoded field section (RFC 9204 §4.5.1, Figure 12).
struct QpackInsertCountIncrement #
struct QpackInsertCountIncrement {
pub:
increment u64
}
QpackInsertCountIncrement is the Insert Count Increment instruction (RFC 9204 §4.4.3): increase Known Received Count by increment.
struct QpackInsertWithLiteralName #
struct QpackInsertWithLiteralName {
pub:
name string
value string
}
QpackInsertWithLiteralName is the Insert With Literal Name instruction (RFC 9204 §4.3.3).
struct QpackInsertWithNameRef #
struct QpackInsertWithNameRef {
pub:
is_static bool
name_index u64
value string
}
QpackInsertWithNameRef is the Insert With Name Reference instruction (RFC 9204 §4.3.2). name_index is a static table index when is_static, otherwise a relative index in the ENCODER-INSTRUCTION context (RFC 9204 §3.2.5, Figure 2 -- relative to the most recently inserted entry, not to a field section's Base).
struct QpackSectionAck #
struct QpackSectionAck {
pub:
stream_id u64
}
QpackSectionAck is the Section Acknowledgment instruction (RFC 9204 §4.4.1): the decoder has finished processing the earliest unacknowledged encoded field section with dynamic-table references on stream_id.
struct QpackSetDynamicTableCapacity #
struct QpackSetDynamicTableCapacity {
pub:
capacity u64
}
QpackSetDynamicTableCapacity is the Set Dynamic Table Capacity instruction (RFC 9204 §4.3.1).
struct QpackStaticEntry #
struct QpackStaticEntry {
pub:
name string
value string
}
QpackStaticEntry is one row of the QPACK static table (RFC 9204 §3.1, Appendix A). Unlike HPACK's static table, QPACK's is indexed from 0.
struct QpackStreamCancellation #
struct QpackStreamCancellation {
pub:
stream_id u64
}
QpackStreamCancellation is the Stream Cancellation instruction (RFC 9204 §4.4.2): stream_id was reset or abandoned before all its encoded field sections were processed.
struct QpackStreamRegistry #
struct QpackStreamRegistry {
mut:
seen_encoder bool
seen_decoder bool
}
QpackStreamRegistry tracks whether a peer has already opened its (at most one) QPACK encoder stream and (at most one) QPACK decoder stream. Mirrors h3_message_state.v's H3ControlStreamState shape exactly (a from-scratch sibling comparison against that struct was done before writing this one): both are "has a single required-unique stream appeared yet" trackers fed by whatever eventually owns the real QUIC streams, needing no other connection state to be correct.
fn (QpackStreamRegistry) note_stream_opened #
fn (mut r QpackStreamRegistry) note_stream_opened(kind QpackStreamKind) !
note_stream_opened records that the peer opened a stream of QPACK kind kind. Errors, carrying H3ErrorCode.stream_creation_error, on a second stream of the same kind (RFC 9204 §4.2: "Each endpoint MUST initiate, at most, one encoder stream and, at most, one decoder stream. Receipt of a second instance of either stream type MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR" -- this is RFC 9114's own error code, not one of QPACK's three from qpack_error.v, since the violation is of HTTP/3's stream-creation model, not a QPACK decompression or instruction-stream failure). The actual connection-close action this error should trigger is Phase 12's job, once a real stream registry exists to call this from -- same "role-legality only" deferral as every analogous row in the HTTP/3 framing conformance-matrix section.
struct QuicConn #
struct QuicConn {
mut:
role QuicRole
state ConnectionState
original_dcid []u8
dcid []u8
scid []u8
peer_scid []u8
token []u8
retry_accepted bool
processed_first_server_packet bool
retry_scid ?[]u8
handshake &Tls13ClientHandshake
handshake_completion &HandshakeCompletionState
pn_spaces &QuicPacketNumberSpaces
initial_keys_client QuicPacketProtectionKeys
initial_keys_server QuicPacketProtectionKeys
initial_keys_discarded bool
handshake_keys_client ?QuicPacketProtectionKeys
handshake_keys_server ?QuicPacketProtectionKeys
handshake_keys_discarded bool
app_write_keys ?QuicPacketProtectionKeys
app_read_keys ?&KeyUpdateState
// app_write_secret/app_write_generation track this endpoint's OWN 1-RTT
// send-key generation, independently of app_read_keys' read-side
// generation chain (RFC 9001 §6.1: "each side's traffic secret chain
// advances on its own schedule" -- key_update.v's own doc comment says
// the same). RFC 9001 §6.2 requires this side's SEND keys to catch up
// to whatever generation the PEER has advanced to (see
// sync_write_keys_to_peer_update) -- the key phase bit sent on the wire
// is this generation's parity (odd generation -> phase 1), matching RFC
// 9001 §6's "toggled to signal each subsequent key update".
app_write_secret []u8
app_write_generation int
initial_crypto &CryptoStreamReassembler
initial_crypto_consumed u64
handshake_crypto &CryptoStreamReassembler
handshake_crypto_consumed u64
handshake_crypto_send_offset u64
client_hello []u8
pending_initial_crypto ?[]u8
pending_handshake_crypto ?[]u8
loss_detection &QuicLossDetectionTimer
congestion_control NewRenoCongestionControl
own_max_idle_timeout_ms u64
idle_timeout IdleTimeoutState
connection_close ConnectionCloseTracker
stateless_reset StatelessResetTracker
ecn EcnState
initial_received_pns map[u64]bool
handshake_received_pns map[u64]bool
app_received_pns map[u64]bool
// *_ack_eliciting_pending track whether ANY frame in the packets
// currently accumulated in the sibling *_received_pns map above was
// ack-eliciting (RFC 9000 §13.2.1: all frames except ACK, PADDING, and
// CONNECTION_CLOSE). drain_outgoing's auto-ACK gates on this, not just
// on *_received_pns being non-empty -- see frame_is_ack_eliciting's own
// doc comment for why the two must be tracked separately.
initial_ack_eliciting_pending bool
handshake_ack_eliciting_pending bool
app_ack_eliciting_pending bool
connection_start u64
// -- Phase 9b: steady-state 1-RTT state ---------------------------------
own_transport_parameters QuicTransportParameters
streams &QuicStreamSet
stream_send_windows map[u64]&FlowControlWindow
stream_recv_windows map[u64]&ReceiveWindow
pending_stream_write map[u64]PendingStreamWrite
pending_stream_reset map[u64]u64 // stream_id -> error_code, queued RESET_STREAM frames
conn_bytes_read u64
conn_send_window FlowControlWindow
conn_recv_window ReceiveWindow
// local_max_streams_* are the FIXED caps this endpoint advertised to the
// peer (via own_transport_parameters) for how many streams the peer may
// open to us -- v1 simplification: never dynamically raised with a
// follow-up MAX_STREAMS frame of our own (documented follow-up, not a
// Phase 9b blocker). peer_max_streams_* are the CURRENT caps the peer
// has told us (initial transport parameter, then raised by any
// MAX_STREAMS frames received) for how many streams we may open to it.
local_max_streams_bidi u64
local_max_streams_uni u64
peer_max_streams_bidi u64
peer_max_streams_uni u64
// last value we've already told the peer we're blocked at, per
// direction -- avoids re-sending a redundant STREAMS_BLOCKED for the
// same still-unraised limit on every subsequent open_stream() call.
streams_blocked_sent_bidi ?u64
streams_blocked_sent_uni ?u64
pending_streams_blocked []StreamDirection
pending_close ?PendingClose
sent_close_payload ?[]u8
closing_deadline ?u64
}
fn (QuicConn) state #
fn (c &QuicConn) state() ConnectionState
state reports which of RFC 9000's connection lifecycle states this connection is currently in.
fn (QuicConn) role #
fn (c &QuicConn) role() QuicRole
role reports which side of the connection this endpoint is. v1 is always .client (see stream.v's QuicRole doc comment).
fn (QuicConn) open_stream #
fn (mut c QuicConn) open_stream(bidi bool) !u64
open_stream opens a new LOCALLY-initiated stream (bidirectional if bidi, else unidirectional), enforcing RFC 9000 §4.6's peer-imposed MAX_STREAMS limit -- QuicStreamSet.open_local_stream itself does NOT check this (see its own doc comment), so this is where that check belongs. Queues a STREAMS_BLOCKED frame (once per still-unraised limit value, not on every call) when blocked, per RFC 9000 §19.14.
fn (QuicConn) write_stream #
fn (mut c QuicConn) write_stream(stream_id u64, data []u8, fin bool) !
write_stream queues data (and, if fin, the end of the stream) to send on stream_id. Nothing is actually sent until the next poll()/ process_timeouts() call drains it -- matching this file's own "nothing happens off the caller's own call stack" invariant. Actual flow-control admission (both this stream's own send window and the connection-level one) is checked at DRAIN time, not here, since the available budget can change between queuing and draining (a MAX_DATA/MAX_STREAM_DATA frame might arrive first).
fn (QuicConn) read_stream #
fn (mut c QuicConn) read_stream(stream_id u64) ![]u8
read_stream returns and consumes whatever new bytes have arrived on stream_id since the last read_stream() call (an empty slice if nothing new has arrived). Advances the stream's own read-side flow-control marker AND the connection-level one -- see should_advertise_more's own doc comment for why both must move together.
fn (QuicConn) close #
fn (mut c QuicConn) close(error_code u64, reason string)
close requests a graceful, application-initiated shutdown. Like write_stream, nothing happens synchronously -- the CONNECTION_CLOSE frame is built and sent on the next poll()/process_timeouts() call. A no-op once already closing/draining/closed.
fn (QuicConn) poll #
fn (mut c QuicConn) poll(incoming ?[]u8, now u64) !PollResult
poll feeds one incoming UDP datagram (or none, to just drain queued outgoing data and check timers) and returns what happened. A protocol error while processing incoming closes the connection (transitions to .closing, queues a best-effort CONNECTION_CLOSE datagram, and reports a connection_closed event) rather than propagating as a Result error -- callers see connection failures as ordinary events, matching how a caller-driven library is expected to behave, not as exceptions.
fn (QuicConn) process_timeouts #
fn (mut c QuicConn) process_timeouts(now u64) !PollResult
process_timeouts is called when a previously-returned next_timeout deadline elapses with no new incoming data. Checks idle timeout first (a genuinely idle connection has nothing left to time-out into), then drives RFC 9002's loss-detection/PTO timer.
struct QuicDatagram #
struct QuicDatagram {
pub:
bytes []u8
}
QuicDatagram is one UDP datagram this connection needs the caller to actually write to the socket.
struct QuicEvent #
struct QuicEvent {
pub:
kind QuicEventKind
error_code ?u64
reason string
}
QuicEvent reports one thing that happened during a poll()/ process_timeouts() call. error_code/reason are set only for connection_closed.
struct QuicLongHeader #
struct QuicLongHeader {
pub mut:
typ LongPacketType
version u32
dcid []u8
scid []u8
// Present only for Initial packets (RFC 9000 §17.2.2); empty otherwise.
// Retry packets carry a token too, but in a different position — see
// QuicRetryHeader.
token []u8
// `length` is the QUIC-varint-encoded byte length of (packet number +
// payload) that follows. Present for Initial/0-RTT/Handshake, absent for
// Retry (which has no packet number or length field at all).
length u64
}
QuicLongHeader represents the parsed, unprotected-portion fields of a long header packet (RFC 9000 §17.2), up to (but not including) the packet number field — the packet number's length and value are only knowable after header protection is removed, which is layered on top of this parse (Phase 3).
struct QuicLossDetectionTimer #
struct QuicLossDetectionTimer {
pub mut:
initial LossDetectionSpaceState
handshake LossDetectionSpaceState
application_data LossDetectionSpaceState
rtt RttEstimator
pto_count int
// first_rtt_sample_time records `now` from the FIRST call that ever
// produced an RTT sample (RttEstimator.has_sample's false -> true
// transition). RFC 9002 §7.6.2's persistent-congestion condition
// requires "a prior RTT sample existed" at the send time of the two
// packets bounding the congestion period -- this is what
// is_persistent_congestion checks that against.
first_rtt_sample_time ?u64
}
QuicLossDetectionTimer is the connection-wide loss detection state: three independent per-space slices (packet numbering IS independent per RFC 9000 §12.3 -- see packet_number_space.v), plus state that is deliberately NOT split by space: one shared RttEstimator (RFC 9002 §5.3 treats RTT as a connection-wide property) and one shared pto_count. The PTO TIMER itself is also connection-wide, sourced from whichever space's own deadline is earliest (pto_time_and_space) -- conflating "packet numbers are per-space" with "therefore everything must be per-space" would be its own distinct bug, flagged explicitly because it's the opposite mistake from the one packet_number_space.v warns about.
fn (QuicLossDetectionTimer) on_packet_sent #
fn (mut ld QuicLossDetectionTimer) on_packet_sent(space QuicPacketNumberSpace, packet_number u64, sent_bytes u64, is_ack_eliciting bool, in_flight bool, now u64)
on_packet_sent is RFC 9002 Appendix A.5's OnPacketSent -- must be called for EVERY packet sent in this space, not only ack-eliciting ones: a peer's ACK frame can legally reference the packet number of a packet that wasn't itself ack-eliciting (e.g. one carrying only an ACK frame of our own), and that packet number still needs a sent_packets entry to be removable by on_ack_received. Congestion-control bookkeeping (OnPacketSentCC) is deliberately NOT called from here -- that's the caller's job once it also holds a NewRenoCongestionControl (Phase 9), mirroring flow_control.v's own "keep this decoupled from owning that state" precedent.
fn (QuicLossDetectionTimer) on_ack_received #
fn (mut ld QuicLossDetectionTimer) on_ack_received(space QuicPacketNumberSpace, ack AckFrame, ack_delay_exponent u64, max_ack_delay time.Duration, handshake_confirmed bool, now u64) AckProcessingResult
on_ack_received is RFC 9002 Appendix A.6's OnAckReceived. ack_delay_exponent is the PEER's own ack_delay_exponent transport parameter (default 3, frame.v's default_ack_delay_exponent), used to scale the ACK frame's raw ack_delay field before RTT sampling. max_ack_delay is the peer's max_ack_delay transport parameter, applied per RttEstimator.update's own handshake-confirmed-gated clamp.
fn (QuicLossDetectionTimer) detect_and_remove_lost_packets #
fn (mut ld QuicLossDetectionTimer) detect_and_remove_lost_packets(space QuicPacketNumberSpace, now u64) []SentPacketInfo
detect_and_remove_lost_packets is RFC 9002 Appendix A.10's DetectAndRemoveLostPackets: a packet in this space is declared lost if EITHER the packet-threshold (largest_acked_packet is at least kPacketThreshold higher) OR the time-threshold (sent at least 9/8*max(latest_rtt,smoothed_rtt), floored at kGranularity, before now) is met -- either condition alone is sufficient, this is not an AND of both. Must only be called once largest_acked_packet is known for this space (an ACK covering something in it must have been processed first); returns empty otherwise.
fn (QuicLossDetectionTimer) loss_time_and_space #
fn (mut ld QuicLossDetectionTimer) loss_time_and_space() ?(u64, QuicPacketNumberSpace)
loss_time_and_space is RFC 9002 Appendix A.8's GetLossTimeAndSpace: whichever space's own loss_time is earliest, across all three independent spaces.
fn (QuicLossDetectionTimer) pto_time_and_space #
fn (mut ld QuicLossDetectionTimer) pto_time_and_space(handshake_confirmed bool, max_ack_delay time.Duration) (u64, QuicPacketNumberSpace)
pto_time_and_space is RFC 9002 Appendix A.8's GetPtoTimeAndSpace: when the SINGLE connection-wide PTO timer should next fire, and which space it will probe. A space with no ack-eliciting packet currently in flight contributes no candidate deadline at all (there is nothing there to probe for); the application_data space additionally contributes nothing until the handshake is confirmed (RFC 9001 -- 1-RTT keys/PTO only matter once that space is actually in use for anything the peer must respond to), at which point its own contribution also adds max_ack_delay (scaled by the same 2^pto_count backoff) since a 1-RTT ACK may legitimately be delayed by up to that much.
fn (QuicLossDetectionTimer) next_timeout #
fn (mut ld QuicLossDetectionTimer) next_timeout(handshake_confirmed bool, max_ack_delay time.Duration, bytes_in_flight u64) ?(u64, QuicPacketNumberSpace)
next_timeout is RFC 9002 Appendix A.7's SetLossDetectionTimer, scoped to v1's client-only reality: the anti-amplification-limited branch in the spec's pseudocode is server-only (it concerns how much a SERVER may send before validating the client's address) and never applies here. Returns none when the loss-detection timer should be cancelled entirely -- nothing ack-eliciting is outstanding anywhere, so there is nothing to time out.
fn (QuicLossDetectionTimer) on_loss_detection_timeout #
fn (mut ld QuicLossDetectionTimer) on_loss_detection_timeout(now u64, handshake_confirmed bool, max_ack_delay time.Duration) LossTimeoutResult
on_loss_detection_timeout is RFC 9002 Appendix A.9's OnLossDetectionTimeout: the single connection-wide loss-detection/PTO timer has fired. See LossTimeoutResult's own doc comment for which of its two outcomes this produces.
struct QuicPacketNumberSpaces #
struct QuicPacketNumberSpaces {
pub mut:
initial PacketNumberSpaceState
handshake PacketNumberSpaceState
application_data PacketNumberSpaceState
}
QuicPacketNumberSpaces holds the three independent per-space states a connection needs. Each field is its own distinct PacketNumberSpaceState value -- there is no shared mutable state between them, which is itself the enforcement mechanism for "never treat packet numbers as connection-global": there is simply no single counter to accidentally share.
struct QuicPacketProtectionKeys #
struct QuicPacketProtectionKeys {
pub:
key []u8 // quic_key -- AEAD encryption key (16 bytes)
iv []u8 // quic_iv -- AEAD nonce base (12 bytes)
hp []u8 // quic_hp -- header protection key (16 bytes)
}
QuicPacketProtectionKeys holds one encryption level's, one direction's derived key material (RFC 9001 §5.1): key/iv protect the AEAD payload, hp protects the header. A single traffic secret (e.g. one level's client_secret) yields exactly one QuicPacketProtectionKeys — client and server secrets are always different PRKs (see tls13_keyschedule.v / initial_secrets.v), so deriving from the correct side's secret is what keeps client-write and server-write keys distinct; this struct itself has no notion of "which direction" beyond whatever secret it was derived from.
struct QuicRetryPacket #
struct QuicRetryPacket {
pub:
version u32
dcid []u8 // echoes the client's own SCID from the triggering Initial -- NOT a new value; see parse_retry_packet's doc comment
scid []u8 // the server's NEW connection ID the client must switch to (RFC 9000 §17.2.5.1)
retry_token []u8
integrity_tag []u8 // retry_integrity_tag_len (16) bytes
}
QuicRetryPacket is a parsed Retry packet, before integrity verification.
struct QuicShortHeader #
struct QuicShortHeader {
pub mut:
spin_bit bool
key_phase bool
dcid []u8
}
QuicShortHeader represents a parsed short header (1-RTT packets, RFC 9000 §17.3). The DCID's length is NOT carried on the wire — the receiver must already know it from its own connection-ID-issuance bookkeeping (see QuicConn.scids, added in Phase 9); passing the wrong length here is the single most common short-header parsing bug. spin_bit and key_phase are only meaningful after header protection removal (they live in the protected low bits of the first byte), same caveat as reserved bits above.
struct QuicStream #
struct QuicStream {
pub:
id StreamId
direction StreamDirection
pub mut:
send &StreamSendHalf = unsafe { nil }
recv &StreamRecvHalf = unsafe { nil }
}
QuicStream is one stream's full state: an identity (id + direction) and whichever of send/recv halves THIS endpoint actually has, given who opened it relative to our own role -- a locally-initiated uni stream has only send; a peer-initiated uni stream has only recv; every bidi stream (either initiator) has both. send/recv are nilable POINTERS (matching Tls13ClientHandshake.verified_chain's established convention elsewhere in this module), not Optional VALUE fields -- an Optional struct field requires unwrap-mutate-reassign on every update (h := s.recv or {...}; h.note_data(...)!; s.recv = h), which is easy to get subtly wrong (mutating the unwrapped copy and forgetting the final reassignment silently drops the update). A pointer field lets every caller mutate the SAME shared half directly (s.recv.note_data(...)!) with no copy-back step to forget.
fn (QuicStream) has_send #
fn (s &QuicStream) has_send() bool
has_send reports whether this stream has a send half on this endpoint.
fn (QuicStream) has_recv #
fn (s &QuicStream) has_recv() bool
has_recv reports whether this stream has a receive half on this endpoint.
struct QuicStreamSet #
struct QuicStreamSet {
mut:
role QuicRole
streams map[u64]&QuicStream
next_local_bidi u64
next_local_uni u64
}
QuicStreamSet tracks every stream known to one connection, keyed by raw stream ID.
fn (QuicStreamSet) get #
fn (s &QuicStreamSet) get(raw_id u64) ?&QuicStream
get returns an already-known stream, or none.
fn (QuicStreamSet) open_local_stream #
fn (mut s QuicStreamSet) open_local_stream(direction StreamDirection) &QuicStream
open_local_stream allocates the next available LOCALLY-initiated stream ID of direction (RFC 9000 §2.1's "streams of the same type are created in sequentially increasing order" rule) and registers a new QuicStream for it. This is the send-side mirror of get_or_create's receive-side auto-creation: get_or_create NEVER fabricates a locally-initiated stream on the peer's say-so (see its own doc comment) -- this function is how one of those streams actually comes into existence, driven by THIS endpoint's own decision to open it.
Unlike get_or_create, this function does NOT itself enforce RFC 9000 §4.6's max_streams limit (the peer's current MAX_STREAMS/ initial_max_streams_* advertisement) -- that check requires state this set doesn't own (the peer's currently-advertised limit, which changes over the connection's life) and belongs to the connection-level caller that DOES own it (Phase 9's QuicConn), the same way ACK-driven and application-read-driven state transitions above are documented hooks for a later phase rather than something this file drives on its own. The caller MUST check against the peer's current limit before calling this function.
fn (QuicStreamSet) len #
fn (s &QuicStreamSet) len() int
len returns the number of streams currently known to this set.
fn (QuicStreamSet) get_or_create #
fn (mut s QuicStreamSet) get_or_create(raw_id u64, max_streams u64) !&QuicStream
get_or_create returns an existing stream, or creates one IF raw_id is legally something the PEER may open unilaterally by simply referencing it in a frame. RFC 9000 §2.1: "Before a stream is created, all streams of the same type with lower-numbered stream IDs MUST be created" is satisfied here by also creating every lower-numbered, same-category stream that doesn't exist yet (up to and including raw_id) -- matching how a real peer's own stream numbering works (a peer opening stream 8 implies streams 0 and 4 already exist, even if we never separately saw traffic on them).
max_streams is the CURRENTLY advertised limit (RFC 9000 §4.6) for this ID's category -- the caller supplies it (sourced from flow_control.v's own state), keeping this function decoupled from owning that state itself.
Locally-initiated stream IDs are NEVER auto-created this way -- a peer referencing an ID that's ours to open, but that we haven't opened, is a protocol violation (STREAM_STATE_ERROR), not something to paper over by fabricating a stream on their behalf.
struct QuicTransportParameters #
struct QuicTransportParameters {
pub mut:
original_destination_connection_id ?[]u8
max_idle_timeout ?u64
stateless_reset_token ?[]u8
max_udp_payload_size ?u64
initial_max_data ?u64
initial_max_stream_data_bidi_local ?u64
initial_max_stream_data_bidi_remote ?u64
initial_max_stream_data_uni ?u64
initial_max_streams_bidi ?u64
initial_max_streams_uni ?u64
ack_delay_exponent ?u64
max_ack_delay ?u64
disable_active_migration bool
preferred_address ?PreferredAddress
active_connection_id_limit ?u64
initial_source_connection_id ?[]u8
retry_source_connection_id ?[]u8
}
QuicTransportParameters holds every RFC 9000 §18.2 parameter. Fields use V's ?T Optional (not RFC-matching zero-value defaults) so "not present" is unambiguous and distinct from "present with a zero/default value" — both are wire-legal and spec-equivalent, but this struct preserves which one actually happened; applying the spec's stated defaults (e.g. max_udp_payload_size's 65527, ack_delay_exponent's 3) to an absent field is a later phase's job (Phase 9's QuicConn, once it actually consumes these values), not this file's.
The four server-only parameters (original_destination_connection_id, stateless_reset_token, preferred_address, retry_source_connection_id) are included so this struct can represent EITHER side's parameter set unchanged in Phase 13's server support, per the plan's role-field design — encode_transport_parameters does not reject a client populating them (RFC 9000 §18.2's "a server MUST treat receipt of any of these as TRANSPORT_PARAMETER_ERROR" is the RECEIVING side's responsibility to enforce, not the sending side's struct shape); v1's client-side caller simply must not populate them.
struct QuicVersionNegotiation #
struct QuicVersionNegotiation {
pub mut:
dcid []u8
scid []u8
versions []u32
}
QuicVersionNegotiation represents a parsed Version Negotiation packet (RFC 9000 §17.2.1) — distinguished by version == 0, and structurally distinct from every other long-header packet: no packet number, no Length field, no encrypted payload, just a list of the versions the server supports.
struct ReceiveWindow #
struct ReceiveWindow {
mut:
received u64 // bytes actually received so far (network progress)
read u64 // bytes the application has consumed (frees window)
advertised u64 // the limit we've told the peer via MAX_DATA/MAX_STREAM_DATA
initial_limit u64
}
ReceiveWindow tracks how much data THIS endpoint is willing to RECEIVE (its own advertised limit to the peer) and how much of it the application has actually consumed, deciding when to advertise a higher limit. RFC 9000 §4.1 recommends sending updates before the window is fully exhausted, not only once it hits zero, to avoid a throughput stall while the peer waits for permission to keep sending.
fn (ReceiveWindow) advertised_limit #
fn (w &ReceiveWindow) advertised_limit() u64
advertised_limit returns the cumulative-offset limit we've told the peer via MAX_DATA/MAX_STREAM_DATA.
fn (ReceiveWindow) note_received #
fn (mut w ReceiveWindow) note_received(new_total_received u64) !
note_received records that the peer has sent data up to new_total_received (a cumulative offset, not a delta), checking against the CURRENTLY advertised limit -- a peer exceeding what we advertised is a FLOW_CONTROL_ERROR. Non-regressing: an out-of-order frame reporting a smaller cumulative total than already recorded is not an error, just a no-op (the larger total already reflects it).
fn (ReceiveWindow) note_read #
fn (mut w ReceiveWindow) note_read(new_total_read u64)
note_read records that the application has consumed up to new_total_read (a cumulative offset). Capped to received: the application can never have read more than has actually arrived over the network, so a new_total_read claiming otherwise (a caller bug, once a real application-read API exists in a later phase) is capped rather than trusted -- otherwise should_advertise_more() could grant flow-control credit for bytes that were never received.
fn (ReceiveWindow) should_advertise_more #
fn (w &ReceiveWindow) should_advertise_more() bool
should_advertise_more reports whether it's time to raise and send a new MAX_DATA/MAX_STREAM_DATA limit to the peer: once the application has consumed at least half of the currently-advertised window. A simple, standard auto-tuning heuristic that keeps the peer from ever actually hitting zero available window in ordinary steady-state use (avoiding a throughput stall) while still bounding how much unread data this endpoint commits to buffering at once.
fn (ReceiveWindow) next_advertised_limit #
fn (w &ReceiveWindow) next_advertised_limit() u64
next_advertised_limit returns the new limit to advertise, once should_advertise_more() is true -- extends the window by another initial_limit's worth. The caller sends the corresponding MAX_DATA/ MAX_STREAM_DATA frame and then calls mark_advertised to commit it.
fn (ReceiveWindow) mark_advertised #
fn (mut w ReceiveWindow) mark_advertised(new_limit u64)
mark_advertised commits new_limit as sent to the peer, once the caller has actually transmitted the corresponding MAX_DATA/MAX_STREAM_DATA frame. Non-regressing: a smaller/stale limit is silently ignored.
struct ResetStreamFrame #
struct ResetStreamFrame {
pub:
stream_id u64
error_code u64
final_size u64
}
ResetStreamFrame represents a RESET_STREAM frame (type 0x04, RFC 9000 §19.4): the sender is abandoning the send side of stream_id, and final_size is the exact total size that stream would have reached had it not been reset -- reconciled against any data already received via StreamReassembler.note_final_size (FINAL_SIZE_ERROR on mismatch).
struct RttEstimator #
struct RttEstimator {
pub mut:
min_rtt time.Duration
smoothed_rtt time.Duration
rttvar time.Duration
latest_rtt time.Duration
has_sample bool
}
RttEstimator holds the one connection-wide RTT estimate. min_rtt and has_sample are meaningless before the first call to update(); smoothed_rtt/rttvar are pre-seeded to kInitialRtt/kInitialRtt/2 so a caller reading them before any sample still gets RFC 9002's specified pre-sample values rather than a misleading zero.
fn (RttEstimator) update #
fn (mut r RttEstimator) update(space QuicPacketNumberSpace, latest_rtt time.Duration, raw_ack_delay time.Duration, max_ack_delay time.Duration, handshake_confirmed bool)
update applies one new RTT sample (RFC 9002 §5.3's UpdateRtt). space is the packet number space the acknowledged, RTT-sampled packet was sent in -- raw_ack_delay (the ACK frame's own, not-yet-scaled ACK Delay field, already converted to a Duration by the caller via scaled_ack_delay_micros) is used ONLY for the application_data space; for Initial/Handshake it is unconditionally treated as zero (RFC 9002 §5.3: "An endpoint always ignores the ACK Delay field... for packets sent in the Initial and Handshake packet number space"). Deciding this HERE, from the space parameter, rather than trusting every caller to pre-zero the delay themselves, eliminates the trap of a caller forgetting the rule for one of the three spaces.
max_ack_delay is the peer's own max_ack_delay transport parameter (RFC 9000 §18.2, in the same Duration units as everything else here); per RFC 9002 §5.3 the effective ack_delay is additionally clamped to this value, but ONLY once the handshake is confirmed -- handshake_confirmed mirrors handshake_confirm.v's own is_confirmed() checkpoint.
fn (RttEstimator) pto_period #
fn (r &RttEstimator) pto_period() time.Duration
pto_period returns the (smoothed_rtt + max(4*rttvar, kGranularity)) component RFC 9002 §6.2.1's GetPtoTimeAndSpace formula shares across every space -- loss_detection.v scales this by 2^pto_count (and, for the application_data space only, adds max_ack_delay) itself.
struct SentPacketInfo #
struct SentPacketInfo {
pub:
packet_number u64
time_sent u64
sent_bytes u64
is_ack_eliciting bool
in_flight bool
}
SentPacketInfo is RFC 9002 Appendix A.1's per-packet sent_packets entry. time_sent is a time.sys_mono_now()-sourced monotonic nanosecond instant, not a wall-clock time (matching vlib/time's own StopWatch convention) -- QUIC's RTT/loss timers must never be perturbed by a system clock adjustment.
struct SettingsFrame #
struct SettingsFrame {
pub:
settings []H3Setting
}
SettingsFrame carries zero or more configuration parameters (§7.2.4). settings includes every syntactically valid pair the payload contained, INCLUDING grease and other unrecognized identifiers -- deciding what to actually apply is a Phase 12 concern once real connection state exists to apply settings to (see decode_h3_frame_payload doc comment for what IS rejected outright at parse time).
struct StatelessResetTracker #
struct StatelessResetTracker {
mut:
known_tokens map[string][]u8 // hex-encoded connection ID -> 16-byte token
}
StatelessResetTracker records the stateless-reset tokens this endpoint has learned (from the peer's transport parameters and/or NEW_CONNECTION_ID frames), keyed by the connection ID they protect.
fn (StatelessResetTracker) record_token #
fn (mut t StatelessResetTracker) record_token(connection_id []u8, token []u8) !
record_token associates a 16-byte stateless-reset token with the connection ID it protects.
fn (StatelessResetTracker) is_stateless_reset #
fn (t &StatelessResetTracker) is_stateless_reset(connection_id []u8, datagram []u8) bool
is_stateless_reset reports whether datagram's trailing 16 bytes match the token recorded for connection_id. MUST only be called after normal packet processing (header parse + AEAD decrypt) has already failed for this datagram -- see the file-level doc comment. The comparison is constant-time: a token is a secret shared only between this endpoint and the one that issued it, and a variable-time byte-by-byte compare would leak how many leading bytes an attacker's guess got right.
struct StopSendingFrame #
struct StopSendingFrame {
pub:
stream_id u64
error_code u64
}
StopSendingFrame represents a STOP_SENDING frame (type 0x05, RFC 9000 §19.5): a request that the peer abandon sending on stream_id.
struct StreamDataBlockedFrame #
struct StreamDataBlockedFrame {
pub:
stream_id u64
maximum_stream_data u64
}
StreamDataBlockedFrame represents a STREAM_DATA_BLOCKED frame (type 0x15, RFC 9000 §19.13): same as DataBlockedFrame, but for one stream's limit.
struct StreamFrame #
struct StreamFrame {
pub:
stream_id u64
offset u64
fin bool
data []u8
}
StreamFrame represents a STREAM frame (type 0x08-0x0f, RFC 9000 §19.8): a chunk of one stream's byte data at offset, optionally marking the end of the stream (fin). Reassembling multiple (possibly out-of-order, possibly overlapping) StreamFrames into a contiguous per-stream byte stream is stream_reassembly.v's job, not this one's.
struct StreamId #
struct StreamId {
pub:
value u64
}
StreamId wraps a raw stream ID with its RFC 9000 §2.1 category derivation.
fn (StreamId) initiator #
fn (id StreamId) initiator() StreamInitiator
initiator returns which side of the connection opened this stream, per bit 0 of the stream ID (RFC 9000 §2.1).
fn (StreamId) direction #
fn (id StreamId) direction() StreamDirection
direction returns whether this stream is bidirectional or unidirectional, per bit 1 of the stream ID (RFC 9000 §2.1).
fn (StreamId) is_locally_initiated #
fn (id StreamId) is_locally_initiated(role QuicRole) bool
is_locally_initiated reports whether role (this endpoint's own role) is the one that opened the stream with this ID.
struct StreamReassembler #
struct StreamReassembler {
mut:
// base_offset is the stream-absolute offset of received[0] -- bytes
// before this position have already been consumed and discarded, and
// `received` holds only the window from base_offset onward.
base_offset u64
received []u8
pending []StreamDataFragment
final_size ?u64
}
fn (StreamReassembler) consumed_len #
fn (r &StreamReassembler) consumed_len() u64
consumed_len returns the STREAM-ABSOLUTE offset marking how much has been contiguously received so far. Unaffected by discard(): freeing already- consumed bytes doesn't change how much of the stream has actually arrived, only how much of it this reassembler still holds in memory.
fn (StreamReassembler) data #
fn (r &StreamReassembler) data() []u8
data returns the currently-HELD window of contiguous bytes, starting at whatever offset base_offset last advanced to via discard() -- NOT necessarily the whole stream from its start.
fn (StreamReassembler) discard #
fn (mut r StreamReassembler) discard(new_base u64) !
discard drops bytes before new_base (a STREAM-ABSOLUTE offset) from the held window, freeing the memory for data the caller has already consumed -- this is what lets total stream size exceed max_stream_buffered_bytes, by keeping the UNCONSUMED window bounded instead of the whole stream. new_base must not exceed consumed_len() (discarding data not yet received would silently create a gap this reassembler can no longer detect). Idempotent/tolerant of a stale new_base at or before the current base_offset -- a no-op, not an error, since a caller re-confirming consumption it already reported is normal.
fn (StreamReassembler) is_finished #
fn (r &StreamReassembler) is_finished() bool
is_finished reports whether every byte up to the (already learned) final size has been received.
fn (StreamReassembler) note_final_size #
fn (mut r StreamReassembler) note_final_size(final_size u64) !
note_final_size records the stream's final size (from a FIN-carrying STREAM frame's offset+length, or from RESET_STREAM's Final Size field) and validates it against everything already received or buffered. RFC 9000 §4.5: a final size SMALLER than data already received, or that DISAGREES with an already-established final size, is a FINAL_SIZE_ERROR. Idempotent: calling this again with the SAME value (e.g. a retransmitted FIN) is a no-op, not an error.
fn (StreamReassembler) add #
fn (mut r StreamReassembler) add(offset u64, data []u8) !
add ingests one STREAM frame's (offset, data) into the reassembler. Frames may arrive out of order; both immediately-contiguous and out-of-order fragments are accepted and, for the latter, held until the gap before them closes. The max_stream_buffered_bytes cap is enforced against the UNCONSUMED window (received + pending), not the frame's wire offset -- see append_or_validate for the received-side check and below for the pending-side one.
struct StreamRecvHalf #
struct StreamRecvHalf {
pub mut:
state RecvStreamState
reassembler &StreamReassembler = unsafe { nil }
final_size ?u64
error_code ?u64 // set once reset_recvd (peer's RESET_STREAM error code)
}
RecvHalf is the receive-side state for a stream THIS endpoint may receive on (present on every bidi stream, and on uni streams the PEER opened).
fn (StreamRecvHalf) note_size_known #
fn (mut h StreamRecvHalf) note_size_known(final_size u64) !
note_size_known transitions recv -> size_known once the final size is learned (a FIN-carrying STREAM frame, or a RESET_STREAM frame).
A no-op once state has already reached reset_recvd/reset_read: RFC 9000 §3.2's Receive Stream State Machine (Figure 2) makes Reset Recvd a terminal classification for how this stream's data delivery is understood -- reordering means a STREAM frame carrying the FIN this endpoint was still expecting can legitimately arrive AFTER a RESET_STREAM that raced ahead of it on the wire, and processing it here would otherwise silently flip state from Reset Recvd back to Data Recvd, which the state machine never permits.
fn (StreamRecvHalf) note_data #
fn (mut h StreamRecvHalf) note_data(offset u64, data []u8) !
note_data records incoming STREAM frame data and promotes recv/ size_known -> data_recvd once the reassembler has everything up to a known final size.
A no-op once reset -- see note_size_known's doc comment for the same reordering rationale (a reset stream must not be promoted back to data_recvd by a stray, reordered STREAM frame).
fn (StreamRecvHalf) mark_reset_recvd #
fn (mut h StreamRecvHalf) mark_reset_recvd(error_code u64, final_size u64) !
mark_reset_recvd transitions to reset_recvd on receiving RESET_STREAM -- legal from recv or size_known, and also from data_recvd (RFC 9000 §3.2 explicitly allows this as implementation-defined: "It is possible that all stream data has already been received when a RESET_STREAM is received... An implementation is free to manage this situation as it chooses."). Reconciles final_size against everything already received via the reassembler's own FINAL_SIZE_ERROR check (RFC 9000 §4.5) -- the same validation note_size_known already applies to a FIN-carrying STREAM frame's final size, applied here too so a reset claiming a final size smaller than data already received, or disagreeing with an already-established final size (including a differing retransmitted RESET_STREAM), is rejected rather than silently accepted.
A no-op once state is reset_read: the application has already consumed the reset, matching note_size_known/note_data's own terminal-state rationale.
struct StreamSendHalf #
struct StreamSendHalf {
pub mut:
state SendStreamState
offset u64
final_size ?u64
error_code ?u64 // set once reset_sent (our own RESET_STREAM's error code)
}
StreamSendHalf is the send-side state for a stream THIS endpoint may send on (present on every bidi stream, and on uni streams this side opened).
fn (StreamSendHalf) mark_data_queued #
fn (mut h StreamSendHalf) mark_data_queued()
mark_data_queued transitions ready -> send the first time data is queued to send on this stream.
fn (StreamSendHalf) mark_fin_sent #
fn (mut h StreamSendHalf) mark_fin_sent(final_size u64)
mark_fin_sent transitions send -> data_sent once a FIN-carrying STREAM frame has been sent, recording the stream's final size.
fn (StreamSendHalf) mark_reset_sent #
fn (mut h StreamSendHalf) mark_reset_sent(error_code u64)
mark_reset_sent transitions to reset_sent from any state prior to data_recvd/reset_recvd -- RFC 9000 §3.1 permits resetting a stream at any point before its send side has fully completed, but its Sending Stream State Machine draws "Send RESET_STREAM" only from Ready/Send/ Data Sent, and its prose states a sender MUST NOT send RESET_STREAM once already in a terminal state (Data Recvd or Reset Recvd). A no-op from either terminal state, matching mark_fin_sent's own guarded-transition pattern above.
struct StreamsBlockedFrame #
struct StreamsBlockedFrame {
pub:
direction StreamDirection
maximum_streams u64
}
StreamsBlockedFrame represents a STREAMS_BLOCKED frame (type 0x16 bidirectional, 0x17 unidirectional, RFC 9000 §19.14): informs the peer the sender wanted to open another stream of direction but was blocked by the max_streams limit.
struct Tls13ClientHandshake #
struct Tls13ClientHandshake {
mut:
state ClientHandshakeState
// Running concatenation of every handshake message's bytes (header
// included, RFC 8446 §4.4.1 "Messages"), in RFC 8446 §4.4.1 order --
// no QUIC/TLS record-layer framing, since QUIC has none. Re-hashed
// (not incrementally hashed) at each checkpoint that needs a
// Transcript-Hash: simpler than a streaming hash state, and a full
// handshake's messages are a handful of KB, not a hot path.
transcript []u8
// Set once a HelloRetryRequest is seen, so a second one can be
// rejected (RFC 8446 §4.1.4). Note: this handshake does not yet
// generate a valid ClientHello2 in response to a first HRR (see
// process_server_hello's own doc comment) -- ClientHello1's bytes are
// therefore not separately retained yet either; RFC 8446 §4.4.1's
// synthetic message_hash substitution is a follow-up's job once HRR
// response generation exists.
got_hello_retry_request bool
ecdhe_private ecdsa.PrivateKey
ca_bundle_pem string
// The SNI hostname this client sent in its own ClientHello -- retained
// so process_certificate_or_request can pass it to
// verify_server_certificate_chain for SAN/CN matching (RFC 6066 §3);
// without this, chain-of-trust verification alone says nothing about
// which host the certificate is actually FOR.
server_name string
// Mirrors ClientHandshakeParams.alpn_protocols -- retained so
// process_encrypted_extensions can check the server's ALPN selection
// (RFC 7301 §3.2) is actually one this client offered.
alpn_protocols []string
handshake_secrets HandshakeSecrets
application_secrets ApplicationSecrets
verified_chain &VerifiedCertificateChain = unsafe { nil }
// Transcript-Hash(ClientHello...Certificate) -- RFC 8446 §4.4.3's
// "Transcript-Hash(Handshake Context, Certificate)" input to
// certificate_verify_signed_content. Computed when Certificate is
// processed, consumed when CertificateVerify is processed next.
certificate_transcript_hash []u8
// Guards free() against a second call -- see free()'s own doc comment
// for why this can't just be "check whether ecdhe_private was already
// freed" the way VerifiedCertificateChain.free() checks its own
// pointer: ecdsa.PrivateKey.free() has no such self-check, and calling
// it twice on the same value is a real, empirically-confirmed crash
// (OpenSSL's EVP_PKEY_free aborts on a double-free of the same
// pointer -- unlike the mbedTLS heap elsewhere in this codebase, which
// doesn't reliably crash on a double-free of a similar size).
freed bool
pub mut:
peer_transport_parameters QuicTransportParameters
}
Tls13ClientHandshake drives a single QUIC-scoped TLS 1.3 client handshake (RFC 8446, restricted to the subset RFC 9001 needs) from ClientHello construction through the client's own Finished message. Callers feed it each handshake message as QUIC's CRYPTO stream delivers it (Phase 4's job, not yet built) and get back either the next message to send or an error carrying the QUIC CONNECTION_CLOSE code to use (see handshake_error). The caller MUST call free() when done, successful or not -- ecdhe_private and verified_chain both own C/OpenSSL-heap resources with no GC visibility.
ANY error from a process_* method is fatal to the whole handshake, not just to that one call: the caller must tear down the QUIC connection (using the returned error's .code(), a CONNECTION_CLOSE error per RFC 9001 §4.8) and must not call any further process_* method on this object -- only free() remains safe to call. This is not merely convention: some failure paths (e.g. a key-schedule derivation error in process_server_hello, practically unreachable given this handshake's fixed-size inputs, but not structurally excluded) already accumulate a message into the transcript before the call that can still fail, so the object's internal state is not guaranteed consistent enough to resume from after an error.
fn (Tls13ClientHandshake) state #
fn (h &Tls13ClientHandshake) state() ClientHandshakeState
state returns which handshake message this client is currently waiting to receive.
fn (Tls13ClientHandshake) application_secrets #
fn (h &Tls13ClientHandshake) application_secrets() ApplicationSecrets
application_secrets returns the derived 1-RTT traffic secrets. Only meaningful once state() == .connected -- Phase 3's job to turn these into actual AEAD keys via hkdf_expand_label's "quic key"/"quic iv" labels.
fn (Tls13ClientHandshake) handshake_secrets #
fn (h &Tls13ClientHandshake) handshake_secrets() HandshakeSecrets
handshake_secrets returns the derived Handshake-level traffic secrets. Meaningful once state() has advanced past .wait_server_hello -- unlike application_secrets, needed mid-handshake (Phase 3's job to install Handshake-level packet protection keys as soon as ServerHello arrives, before the rest of the handshake completes).
fn (Tls13ClientHandshake) free #
fn (mut h Tls13ClientHandshake) free()
free releases ecdhe_private (an OpenSSL EVP_PKEY, Phase 1's crypto.ecdsa) and verified_chain (an mbedTLS certificate chain, Phase 2c's net.mbedtls), if either was ever allocated. Idempotent: safe to call more than once -- guarded by freed rather than checking each owned resource's own pointer (VerifiedCertificateChain.free() can do that, since it nulls its own pointer after freeing, but ecdsa.PrivateKey.free() has no equivalent self-guard, so a bare second h.ecdhe_private.free() call would still double-free even if verified_chain's own check were copied here).
fn (Tls13ClientHandshake) process_server_hello #
fn (mut h Tls13ClientHandshake) process_server_hello(msg HandshakeMessage, framed_message []u8) !HandshakeSecrets
process_server_hello handles the message immediately following ClientHello, which per RFC 8446 §4.1.3/§4.1.4 is either a real ServerHello or a HelloRetryRequest (the same wire type, distinguished by a magic random value). Returns the derived Handshake-level secrets once a real ServerHello completes the ECDHE exchange; Phase 3's job to turn .client_secret/.server_secret into actual packet-protection keys, once it exists.
fn (Tls13ClientHandshake) process_encrypted_extensions #
fn (mut h Tls13ClientHandshake) process_encrypted_extensions(msg HandshakeMessage, framed_message []u8, peer_initial_scid []u8, original_dcid []u8, retry_scid ?[]u8) !
process_encrypted_extensions handles EncryptedExtensions (RFC 8446 §4.3.1), the first message protected under the Handshake-level keys.
peer_initial_scid is the Source Connection ID this client actually observed on the server's first Initial/Handshake packet -- RFC 9001 §8.2 requires the initial_source_connection_id transport parameter to match it exactly, a check this function does since it is the first point that parameter's value is available.
original_dcid is the Destination Connection ID THIS CLIENT chose for its own very first Initial packet, and retry_scid is the Retry packet's Source Connection ID if (and only if) a Retry occurred (RFC 9000 §7.3, the anti-tampering check: these two transport parameters let the client detect an off-path attacker that injected a spoofed Retry or otherwise interfered with connection establishment). All three values come from Phase 4/9 (packet headers, QuicConn), not yet built, so callers must supply them explicitly until then.
fn (Tls13ClientHandshake) process_certificate_or_request #
fn (mut h Tls13ClientHandshake) process_certificate_or_request(msg HandshakeMessage, framed_message []u8) !
process_certificate_or_request handles the message immediately following EncryptedExtensions, which per RFC 8446 §4.1.4 is either an optional CertificateRequest or the mandatory Certificate. v1 supports no client-cert auth (there is no client identity to offer), so a CertificateRequest is rejected outright here rather than answered with an empty Certificate -- PROGRESS.md's stated scope for this. The verified chain is stored internally (h.verified_chain); the caller has no other way to reach it, since that field is private -- the only observable effect of success is the state advancing to .wait_certificate_verify.
fn (Tls13ClientHandshake) process_certificate_verify #
fn (mut h Tls13ClientHandshake) process_certificate_verify(msg HandshakeMessage, framed_message []u8) !
process_certificate_verify handles CertificateVerify (RFC 8446 §4.4.3), checking the server's signature over Transcript-Hash(ClientHello...Certificate) (captured when Certificate was processed, per that message type's own §4.4.3 requirement) against the verified chain's leaf public key.
fn (Tls13ClientHandshake) process_finished #
fn (mut h Tls13ClientHandshake) process_finished(msg HandshakeMessage, framed_message []u8) !([]u8, ApplicationSecrets)
process_finished handles the server's Finished (RFC 8446 §4.4.4), verifying its verify_data against Transcript-Hash(ClientHello... CertificateVerify), then derives the application traffic secrets and computes+returns this client's own Finished message (framed, ready to send) -- both keyed off the transcript hash as it stood at the moment each computation needed it, not the final post-client-Finished transcript.
struct TlsExtension #
struct TlsExtension {
pub:
typ u16
data []u8
}
TlsExtension is one parsed (type, data) entry from a generic TLS extension list (RFC 8446 §4.2).
struct UnprotectedPacket #
struct UnprotectedPacket {
pub:
header []u8 // the full header, through the now-plaintext packet number
packet_number u64 // full, reconstructed packet number
payload []u8 // decrypted plaintext payload
}
UnprotectedPacket is the result of successfully removing both header and packet protection from one received QUIC packet.
struct VerifiedCertificateChain #
struct VerifiedCertificateChain {
mut:
chain &C.mbedtls_x509_crt = unsafe { nil }
}
VerifiedCertificateChain wraps an mbedTLS certificate chain built from a parsed TLS 1.3 Certificate message (tls13_certificate.v's ParsedCertificate) once it has passed chain-trust validation. The caller MUST call free() when done — the underlying mbedtls_x509_crt chain holds C-heap-allocated buffers with no GC visibility (see net.mbedtls.build_certificate_chain's own doc comment).
fn (VerifiedCertificateChain) free #
fn (mut c VerifiedCertificateChain) free()
free releases the underlying mbedTLS chain. Nulls out chain after freeing so a second free() call (e.g. a defer racing an explicit early free) is a harmless no-op rather than a double-free — same discipline as net.mbedtls.SSLConn.shutdown()'s own documented guard for the identical class of repeated-cleanup-call bug.
fn (VerifiedCertificateChain) verify_certificate_verify_signature #
fn (c &VerifiedCertificateChain) verify_certificate_verify_signature(cv ParsedCertificateVerify, role CertificateVerifyRole, transcript_hash []u8) !
verify_certificate_verify_signature checks a parsed CertificateVerify message's signature against this chain's leaf certificate's public key. role/transcript_hash feed certificate_verify_signed_content's exact RFC 8446 §4.4.3 signed-content construction -- what was actually signed, not transcript_hash directly. Dispatches on cv.algorithm to the matching digest + mbedTLS verification call; parse_certificate_verify has already restricted cv.algorithm to v1's fixed offered set (sig_scheme_ecdsa_secp256r1_sha256/rsa_pss_rsae_sha256/384/512), so the else arm below is unreachable in practice, not a real fallback path.
Guards against being called after free(): mbedtls.get_leaf_public_key's own doc comment already states the precondition ("do not call this after free_certificate_chain") but doesn't enforce it -- free() nulls c.chain, and the C shim behind get_leaf_public_key computes &crt->pk (pointer arithmetic on a NULL crt), which is undefined behavior, not a clean nil dereference an or {} could catch. No caller does this today, but the upcoming client state machine will hold a VerifiedCertificateChain across multiple calls (trust check, then this), making the free-then-use ordering an easy mistake to introduce later -- same defensive rationale as free()'s own idempotency guard, just checked from the other side.
- README
- Constants
- fn build_client_hello
- fn certificate_verify_signed_content
- fn classify_h3_unidirectional_stream_type
- fn classify_qpack_stream_type
- fn compute_finished_verify_data
- fn compute_retry_integrity_tag
- fn decode_base
- fn decode_field_section_prefix
- fn decode_h3_frame_payload
- fn decode_packet_number
- fn decode_preferred_address
- fn decode_prefixed_int
- fn decode_prefixed_string
- fn decode_qpack_decoder_instruction
- fn decode_qpack_encoder_instruction
- fn decode_qpack_field_line
- fn decode_ric
- fn decode_transport_parameters
- fn decode_varint
- fn decrypt_packet_payload
- fn derive_application_secrets
- fn derive_early_secret
- fn derive_handshake_secrets
- fn derive_initial_secrets
- fn derive_packet_protection_keys
- fn derive_secret
- fn derive_updated_packet_protection_keys
- fn derive_updated_secret
- fn dial
- fn effective_idle_timeout
- fn empty_transcript_hash
- fn encode_ack_frame
- fn encode_base
- fn encode_cancel_push_frame
- fn encode_connection_close_frame
- fn encode_crypto_frame
- fn encode_data_blocked_frame
- fn encode_data_frame
- fn encode_field_section_prefix
- fn encode_goaway_frame
- fn encode_h3_control_stream_header
- fn encode_h3_push_stream_header
- fn encode_handshake_message
- fn encode_headers_frame
- fn encode_indexed_dynamic_post_base
- fn encode_indexed_dynamic_relative
- fn encode_indexed_static
- fn encode_literal_with_literal_name
- fn encode_literal_with_name_ref
- fn encode_literal_with_post_base_name_ref
- fn encode_long_header
- fn encode_max_data_frame
- fn encode_max_push_id_frame
- fn encode_max_stream_data_frame
- fn encode_max_streams_frame
- fn encode_packet_number
- fn encode_preferred_address
- fn encode_prefixed_int
- fn encode_prefixed_string
- fn encode_qpack_duplicate
- fn encode_qpack_insert_count_increment
- fn encode_qpack_insert_with_literal_name
- fn encode_qpack_insert_with_name_ref
- fn encode_qpack_section_ack
- fn encode_qpack_set_dynamic_table_capacity
- fn encode_qpack_stream_cancellation
- fn encode_reset_stream_frame
- fn encode_ric
- fn encode_settings_frame
- fn encode_short_header
- fn encode_stop_sending_frame
- fn encode_stream_data_blocked_frame
- fn encode_stream_frame
- fn encode_streams_blocked_frame
- fn encode_transport_parameters
- fn encode_varint
- fn encrypt_packet_payload
- fn find_extension
- fn first_stream_id_of
- fn fits_within_pmtu
- fn goaway_id_is_valid_client_initiated_bidi_stream_id
- fn handle_version_negotiation
- fn handshake_type_from_u8
- fn hkdf_expand_label
- fn initial_receive_limit_for_stream
- fn initial_send_limit_for_stream
- fn is_h3_frame_valid_on_stream
- fn is_h3_reserved_codepoint
- fn is_persistent_congestion
- fn new_connection_close_tracker
- fn new_crypto_stream_reassembler
- fn new_ecn_state
- fn new_flow_control_window
- fn new_h3_control_stream_state
- fn new_h3_frame_decoder
- fn new_handshake_completion_state
- fn new_idle_timeout_state
- fn new_key_update_state
- fn new_newreno_congestion_control
- fn new_packet_number_spaces
- fn new_qpack_decoder
- fn new_qpack_encoder
- fn new_qpack_stream_registry
- fn new_quic_loss_detection_timer
- fn new_quic_stream
- fn new_quic_stream_set
- fn new_receive_window
- fn new_rtt_estimator
- fn new_stateless_reset_tracker
- fn new_stream_reassembler
- fn pad_initial_payload
- fn parse_certificate
- fn parse_certificate_verify
- fn parse_encrypted_extensions
- fn parse_extension_list
- fn parse_frame
- fn parse_frames
- fn parse_h3_unidirectional_stream_header
- fn parse_handshake_message
- fn parse_long_header
- fn parse_retry_packet
- fn parse_server_hello
- fn parse_short_header
- fn parse_version_negotiation
- fn peek_header_form
- fn protect_header
- fn protect_packet
- fn qpack_blocked_streams_from_settings
- fn qpack_entry_size
- fn qpack_is_sensitive
- fn qpack_max_entries
- fn qpack_max_table_capacity_from_settings
- fn qpack_static_find
- fn qpack_static_find_name
- fn qpack_static_lookup
- fn scaled_ack_delay_micros
- fn split_coalesced_datagram
- fn synthetic_client_hello1_hash
- fn tls_alert_to_quic_error
- fn unprotect_header
- fn unprotect_packet
- fn varint_len
- fn verify_finished
- fn verify_retry_integrity_tag
- fn verify_server_certificate_chain
- fn CertificateVerifyRole.from
- fn ClientHandshakeState.from
- fn ConnectionCloseState.from
- fn ConnectionState.from
- fn H3ErrorCode.from
- fn H3StreamRole.from
- fn H3UnidirectionalStreamKind.from
- fn HandshakeType.from
- fn HeaderForm.from
- fn LongPacketType.from
- fn QpackErrorCode.from
- fn QpackStreamKind.from
- fn QuicEventKind.from
- fn QuicPacketNumberSpace.from
- fn QuicRole.from
- fn RecvStreamState.from
- fn SendStreamState.from
- fn StreamDirection.from
- fn StreamInitiator.from
- fn Tls13ClientHandshake.start
- fn TlsAlert.from
- type H3Frame
- type QpackDecoderInstruction
- type QpackEncoderInstruction
- type QuicFrame
- type ServerHelloMessage
- enum CertificateVerifyRole
- enum ClientHandshakeState
- enum ConnectionCloseState
- enum ConnectionState
- enum H3ErrorCode
- enum H3StreamRole
- enum H3UnidirectionalStreamKind
- enum HandshakeType
- enum HeaderForm
- enum LongPacketType
- enum QpackErrorCode
- enum QpackStreamKind
- enum QuicEventKind
- enum QuicPacketNumberSpace
- enum QuicRole
- enum RecvStreamState
- enum SendStreamState
- enum StreamDirection
- enum StreamInitiator
- enum TlsAlert
- struct AckFrame
- struct AckProcessingResult
- struct AckRange
- struct ApplicationSecrets
- struct CancelPushFrame
- struct CertificateEntry
- struct ClientHandshakeParams
- struct ClientHelloParams
- struct CoalescedPacket
- struct ConnectionCloseFrame
- struct ConnectionCloseTracker
- struct CryptoFrame
- struct CryptoStreamReassembler
- struct DataBlockedFrame
- struct DataFrame
- struct DialParams
- struct EcnCounts
- struct EcnState
- struct FlowControlWindow
- struct GoawayFrame
- struct H3ControlStreamState
- struct H3FrameDecodeResult
- struct H3FrameDecoder
- struct H3RawFrame
- struct H3Setting
- struct H3UnidirectionalStreamHeader
- struct HandshakeCompletionState
- struct HandshakeDoneFrame
- struct HandshakeMessage
- struct HandshakeSecrets
- struct HeadersFrame
- struct IdleTimeoutState
- struct InitialSecrets
- struct KeyResolution
- struct KeyUpdateState
- struct LossDetectionSpaceState
- struct LossTimeoutResult
- struct MaxDataFrame
- struct MaxPushIdFrame
- struct MaxStreamDataFrame
- struct MaxStreamsFrame
- struct NewRenoCongestionControl
- struct PacketNumberSpaceState
- struct PaddingFrame
- struct ParsedCertificate
- struct ParsedCertificateVerify
- struct ParsedHelloRetryRequest
- struct ParsedServerHello
- struct PingFrame
- struct PollResult
- struct PreferredAddress
- struct PushPromiseFrame
- struct QpackApplyInstructionResult
- struct QpackDecodeFieldSectionResult
- struct QpackDecodedDecoderInstruction
- struct QpackDecodedEncoderInstruction
- struct QpackDecodedFieldLine
- struct QpackDecoder
- struct QpackDuplicate
- struct QpackDynamicTable
- struct QpackDynamicTableEntry
- struct QpackEncodedFieldSection
- struct QpackEncoder
- struct QpackFieldLine
- struct QpackFieldSectionPrefix
- struct QpackInsertCountIncrement
- struct QpackInsertWithLiteralName
- struct QpackInsertWithNameRef
- struct QpackSectionAck
- struct QpackSetDynamicTableCapacity
- struct QpackStaticEntry
- struct QpackStreamCancellation
- struct QpackStreamRegistry
- struct QuicConn
- struct QuicDatagram
- struct QuicEvent
- struct QuicLongHeader
- struct QuicLossDetectionTimer
- struct QuicPacketNumberSpaces
- struct QuicPacketProtectionKeys
- struct QuicRetryPacket
- struct QuicShortHeader
- struct QuicStream
- struct QuicStreamSet
- struct QuicTransportParameters
- struct QuicVersionNegotiation
- struct ReceiveWindow
- struct ResetStreamFrame
- struct RttEstimator
- struct SentPacketInfo
- struct SettingsFrame
- struct StatelessResetTracker
- struct StopSendingFrame
- struct StreamDataBlockedFrame
- struct StreamFrame
- struct StreamId
- struct StreamReassembler
- struct StreamRecvHalf
- struct StreamSendHalf
- struct StreamsBlockedFrame
- struct Tls13ClientHandshake
- struct TlsExtension
- struct UnprotectedPacket
- struct VerifiedCertificateChain