Skip to content

Spec coverage

quicz aims to implement the IETF QUIC transport protocol as defined by the QUIC WG at https://quicwg.org/.

Initial target documents:

  • RFC 8999: Version-Independent Properties of QUIC
  • RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport
  • RFC 9001: Using TLS to Secure QUIC
  • RFC 9002: QUIC Loss Detection and Congestion Control
  • RFC 9369: QUIC Version 2

The verifiable transport implementation task matrix is maintained in quic_transport_tasks.md. That document is the working checklist for the first implementation scope; this file describes the current implementation shape.

  • Single-path, IPv4 only
  • QUIC v1 connection behavior by default, with configured QUIC v2 protected long-packet and Retry version use plus v2 packet/key/token primitives and RFC 9000 reserved-version greasing recognition available where noted
  • Basic packet/header parsing and serialization
  • One connection per UDP 4-tuple
  • Basic stream support, send-side PING emission, STREAM and per-packet-number-space CRYPTO fragmentation, inbound out-of-order CRYPTO buffering with duplicate retransmission discard, out-of-order STREAM receive reassembly with duplicate retransmission discard, local RESET_STREAM and STOP_SENDING emission, inbound RESET_STREAM and STOP_SENDING handling, PATH_CHALLENGE response queuing, outbound PATH_CHALLENGE tracking with PTO-based retry, failure counting, matching PATH_RESPONSE validation with caller-committed endpoint route path update, modeled server anti-amplification send limiting with explicit peer-address validation, server-side Retry datagram issuance, first-client-Initial DCID length validation, server-Initial token validation, RFC 9000 Initial UDP datagram 1200-byte expansion/discard checks, HMAC-SHA256 address-bound expiring token generation/validation with originating-version binding, bounded token replay filtering, client-side Retry processing, Retry token consumption, handshake CID transport-parameter validation/export, local and peer-issued connection ID lifecycle handling, server-side HANDSHAKE_DONE and NEW_TOKEN issuing, client-only NEW_TOKEN storage, HANDSHAKE_DONE receive validation, handshake confirmation, RFC 9001 Initial discard after client Handshake send/server Handshake receive, and Handshake-space discard after valid client-side HANDSHAKE_DONE, server-side sendHandshakeDone, or backend-confirmed no-output Handshake drive, flow-control and stream-count limits, strict stream direction validation, max_idle_timeout handling, local/remote CONNECTION_CLOSE handling, and close-state handling
  • Transactional processing for malformed in-memory frame payloads so partial receive, recovery, flow-control, and close-state updates roll back on failure
  • Simplified loss detection and congestion control with automatic ACK generation, ACK range handling, unsent-packet ACK rejection, sent-packet tracking, ACK-driven STREAM and frame-payload/protected CRYPTO retransmission requeue, frame-payload packet-number-space ACK/recovery isolation, connection-level bytes-in-flight congestion admission, RTT estimate sharing, and PTO backoff across packet number spaces, modeled Initial/Handshake packet number space discard cleanup including ECN state, Initial/Handshake RTT ACK-delay suppression, Application ACK delay exponent and post-confirmation max_ack_delay handling, packet-threshold and time-threshold loss detection with deterministic timeout handling, NewReno slow-start/congestion-avoidance growth, NewReno-style recovery period handling, new-congestion-event one-packet recovery probe, persistent congestion response with min-RTT refresh, ACK_ECN CE congestion response, packet-number-space PTO PING/new-data/in-flight-CRYPTO/in-flight-STREAM timeout handling, and frame-payload ACK_ECN counter validation
  • Address-validation tokens serialize and authenticate the originating QUIC version. Existing unversioned helpers keep QUIC v1 defaults, while validateForVersion() and the connection/endpoint *ForVersion() helpers allow QUIC v2 callers to reject cross-version token reuse before replay state changes.

  • RFC 9368 version_information is represented as a typed transport parameter. Connections export the configured chosen and available versions, validate peer values according to endpoint role, allow reserved versions only in Available Versions, enforce server Version Information downgrade checks after a client reacts to Version Negotiation, classify parsed version-negotiation semantic close failures as VERSION_NEGOTIATION_ERROR, and expose the transport error code for callers that build the remaining Compatible Version Negotiation state machine. VersionCompatibility and selectCompatibleVersion() model explicit, directional first-flight compatibility without assuming compatibility between any two QUIC versions. Connection stores peer Version Information after successful transport-parameter application and provides server-side compatible-version apply helpers that require the selected version to match the connection’s configured chosen version. The driveCryptoBackendInSpaceWithCompatibleVersion() and driveCryptoBackendInSpaceWithCompatibleVersionOrClose() wrappers route the same compatible Version Information policy through CryptoBackend peer transport-parameter bytes before backend output is pulled and report the selected version in CryptoBackendProgress.

  • Client connections can validate one RFC 8999 Version Negotiation packet, ignore unsafe packets that contain the Original Version or mismatched CIDs, select a non-reserved mutual version from local available_versions, carry that selection into a follow-up connection through Config.version_negotiation_selected_version, validate the server’s authenticated Version Information against it, and let Tls13ClientEndpoint replace the owned transport while emitting the first route-bound follow-up Initial. If follow-up connection creation or Initial emission fails after route registration, the follow-up route is retired.

  • Tls13ServerEndpoint can validate a Retry follow-up Initial on its switched route, consume the one-time address-validation token only after packet authentication, then drive Initial and Handshake TLS output with committed UDP routes.

  • The pure Zig TLS 1.3 handshake always sends the QUIC quic_transport_parameters extension, including empty parameter lists, and rejects ClientHello or EncryptedExtensions messages that omit it.

  • The pure Zig TLS client rejects unrequested ServerHello extensions instead of silently continuing the handshake transcript.

  • The pure Zig TLS client rejects unrequested EncryptedExtensions extensions while still requiring QUIC transport parameters.

  • The pure Zig TLS certificate path rejects empty CertificateEntry values and the test builder no longer emits empty certificate entries.

  • The pure Zig TLS certificate path rejects per-entry Certificate extensions, matching the current ClientHello that does not request OCSP/SCT data.

  • The pure Zig TLS ClientHello emits early_data only when a stored session ticket is also available for the matching pre_shared_key offer.

  • Implemented: QUIC varint helpers, minimal long/short header codecs with RFC 9369 QUIC v2 long-header packet type bit mapping, short-header spin-bit preservation, and explicit packet number truncation/reconstruction APIs, protected short-packet spin-bit peeking, RFC 9000 long/short packet envelope codecs with opaque payload handling, RFC 9000 packet number encoding selection/reconstruction, Retry packet codec, RFC 8999 Version Negotiation packet codec plus reserved-version greasing detection, client-side selection, and RFC 9368 downgrade-validation state, stateless reset datagram helpers with constant-time token matching and NEW_CONNECTION_ID token uniqueness checks, HMAC-SHA256 address-validation token helpers with originating-version binding plus bounded replay-filter helper with snapshot export/restore, in-memory endpoint AddressValidationPolicy with versioned token issuance/validation, secret rotation, AddressValidationSecretSet export/restore, and replay-filter snapshot export/restore, in-memory endpoint DCID/IPv4-tuple routing with unsupported-version Version Negotiation response generation, client Initial Source CID route registration, supported-version unknown-DCID Initial accept classification, accepted Initial Original DCID/server Initial SCID route registration, zero-length CID tuple routing, sequence/retire-prior-to/connection-handle route retirement, endpoint replacement-CID registration, inactive-CID stateless reset token lookup/datagram construction helpers, route/version-negotiation/reset/drop/accept receive classification, and per-UDP-path endpoint ECN state, typed RFC 9000/RFC 9368 transport parameter codec with reserved-parameter greasing helper/ignore, connection-level export/application helpers, and TLS extension byte encode/apply helpers, pluggable CryptoBackend callback contract for CRYPTO byte-stream bridging, transport-parameter extension byte handoff, and mock Handshake/0-RTT/1-RTT traffic-secret handoff, typed RFC 9000/RFC 9368 transport error code helpers, RFC 9001 QUIC v1 and RFC 9369 QUIC v2 Initial secret/key/IV/header-protection key derivation, RFC 9001 quic ku key-update secret/key/IV derivation, caller-owned and connection-installed short-packet key-phase state and selection, AEAD_AES_128_GCM payload protection helpers, protected long-packet seal/open helpers that use configured v2 wire versions and type bits, configured v2 Retry issue/process validation, v1/v2 Retry Integrity Tag helpers, and AES header-protection mask application helpers, and basic frame codecs for STREAM and CRYPTO with end-offset validation, PADDING, PING, ACK/ACK_ECN with additional ranges, RESET_STREAM, STOP_SENDING, MAX_DATA, MAX_STREAM_DATA, MAX_STREAMS_BIDI/UNI with stream-count cap validation, DATA_BLOCKED, STREAM_DATA_BLOCKED, STREAMS_BLOCKED_BIDI/UNI with stream-count cap validation, NEW_TOKEN with non-empty token validation, NEW_CONNECTION_ID, RETIRE_CONNECTION_ID, PATH_CHALLENGE/PATH_RESPONSE, HANDSHAKE_DONE, and connection-close variants.

  • Connection implements an in-memory stream send/receive skeleton with send-side PING emission, STREAM fragmentation, local resetStream() RESET_STREAM emission, local stopSending() STOP_SENDING emission, per-packet-number-space CRYPTO send/receive byte streams and backend driving, including local/peer transport-parameter byte exchange and mock Handshake/0-RTT/1-RTT traffic-secret installation, via sendCryptoInSpace() / recvCryptoInSpace() / pollTxInSpace() / driveCryptoBackendInSpace(), protected Initial/Handshake CRYPTO long-packet bridging via pollProtectedLongCryptoDatagramInSpace() / processProtectedLongDatagramInSpace() with first-client-Initial DCID length validation, server-Initial token validation, and RFC 9000 Initial UDP datagram 1200-byte expansion/discard checks, installed-key Handshake long-packet CRYPTO/ACK/PING bridging via pollProtectedHandshakeDatagramWithInstalledKeys() / processProtectedHandshakeDatagramWithInstalledKeys(), coalesced protected Initial/Handshake CRYPTO/ACK/PING plus caller-keyed or installed-key 0-RTT STREAM/RESET_STREAM/STOP_SENDING long-datagram transmit and receive routing via pollProtectedLongDatagram() / processProtectedLongDatagram() and pollProtectedZeroRttDatagram() / processProtectedZeroRttDatagram() / pollProtectedZeroRttDatagramWithInstalledKeys() / processProtectedZeroRttDatagramWithInstalledKeys(), caller-supplied and installed-key 1-RTT short protected PING/ACK/CRYPTO/HANDSHAKE_DONE/NEW_TOKEN/NEW_CONNECTION_ID/PATH_CHALLENGE/PATH_RESPONSE/RETIRE_CONNECTION_ID/MAX_*/BLOCKED/STREAM/RESET_STREAM/STOP_SENDING/CONNECTION_CLOSE transmit and receive via pollProtectedShortDatagram() / processProtectedShortDatagram(), pollProtectedShortDatagramWithInstalledKeys() / processProtectedShortDatagramWithInstalledKeys(), and explicit short-packet key-phase send/receive via pollProtectedShortDatagramWithKeyPhase() / processProtectedShortDatagramWithKeyUpdate(), plus the default Application-space sendCrypto() / recvCrypto() wrappers, local transport parameter export via localTransportParameters() and encodeLocalTransportParameters() including configured local ack_delay_exponent/max_ack_delay, server original_destination_connection_id after the first client Initial, server retry_source_connection_id after issueRetryDatagram(), the first sent Initial SCID as initial_source_connection_id when known, plus configured server-only stateless_reset_token and preferred_address, peer transport parameter application through applyPeerTransportParameters() and applyPeerTransportParameterBytes() including Original DCID, Retry CID, peer Initial SCID validation, peer max_udp_payload_size recovery max_datagram_size/initial-cwnd resync, disable_active_migration observability, peer transport-parameter stateless reset token storage, peer preferred-address fixed storage, and peer ACK delay policy for recovery via applyPeerTransportParameters(), peerActiveMigrationDisabled(), peerStatelessResetToken(), and peerPreferredAddress(), inbound out-of-order STREAM range buffering, duplicate retransmission discard, and contiguous reassembly, inbound RESET_STREAM handling, STOP_SENDING-to-RESET_STREAM response handling, PATH_CHALLENGE response queuing, sendPathChallenge() outbound challenge tracking with PTO-based retry through checkPathValidationTimeouts(), retry exhaustion reporting through failedPathValidationCount(), matching PATH_RESPONSE validation, modeled RFC 9000 server anti-amplification send limiting through recordPeerAddressBytesReceived(), antiAmplificationLimitRemaining(), validatePeerAddress(), server-side Retry datagram issuance through issueRetryDatagram(), HMAC-SHA256 address-validation token issuing/validation through issueAddressValidationToken() / validateAddressValidationToken() / validateAddressValidationTokenWithSecrets() plus endpoint peer-address binding and in-memory token lifecycle through endpoint.Udp4Tuple.peerAddressValidationBinding() and endpoint.AddressValidationPolicy, client-side Retry datagram handling through processRetryDatagram() with latestRetryToken() / originalDestinationConnectionId() / retrySourceConnectionId() storage, and one-time server Retry token consumption through issueRetryToken() / validateRetryToken(), peer-issued NEW_CONNECTION_ID tracking with retire_prior_to-driven RETIRE_CONNECTION_ID queuing, local NEW_CONNECTION_ID issuing through issueConnectionId() with peer active-CID-limit enforcement and inbound RETIRE_CONNECTION_ID retirement, read-only stateless reset token detection through detectStatelessReset(), server-side HANDSHAKE_DONE and NEW_TOKEN issuing through sendHandshakeDone() and issueNewToken(), client-side NEW_TOKEN storage, HANDSHAKE_DONE role validation, explicit handshake progress observability through handshakeState(), handshake confirmation, RFC 9001 Initial-space cleanup after a client Handshake packet send or server Handshake packet receive, and valid client-side HANDSHAKE_DONE-driven or server-side sendHandshakeDone-driven Handshake space/key discard, MAX_STREAM_DATA stream-state validation, basic connection and stream flow control with outbound DATA_BLOCKED/STREAM_DATA_BLOCKED/STREAMS_BLOCKED reporting, receive-side MAX_DATA/MAX_STREAM_DATA/MAX_STREAMS_BIDI/UNI credit refresh, and peer BLOCKED observability with receive-state creation/validation for STREAM_DATA_BLOCKED, stale-limit MAX retransmission, plus configured receive-window growth for DATA_BLOCKED/STREAM_DATA_BLOCKED and stream-count-window growth for STREAMS_BLOCKED_BIDI/UNI, bidirectional and unidirectional stream-count limits, remote close-state handling and peerClose() diagnostics for CONNECTION_CLOSE/APPLICATION_CLOSE, local close emission/retransmission via closeConnection() / closeApplication(), max_idle_timeout export/application through Config.max_idle_timeout_ms, effectiveIdleTimeoutMillis(), idleTimeoutDeadlineMillis(), and checkIdleTimeouts() modeled closure, an explicit ConnectionState lifecycle for active/closing/draining/closed with 3x PTO close-state expiry, an explicit HandshakeState lifecycle for initial/handshake/confirmed progress, explicit PacketNumberSpace handling for Initial/Handshake/Application frame-payload ACK, explicit FramePacketType validation for Initial, Handshake, 0-RTT, and 1-RTT frame-payload packet types, Initial/Handshake discard cleanup including installed Handshake key clearing through discardPacketNumberSpace(), installed 0-RTT key cleanup through discardZeroRttProtectionKeys(), ACK_ECN validation/CE congestion response, ACK delay scaling/capping, packet-threshold and time-threshold loss detection, NewReno slow-start/congestion-avoidance growth, NewReno-style recovery period handling, persistent congestion response, packet-number-space PTO PING timeout handling with queued-data/in-flight-CRYPTO/in-flight-STREAM probe selection, and recovery isolation, automatic ACK generation for ACK-eliciting payloads, ACK-only emission, ACK coalescing with PING/CRYPTO/HANDSHAKE_DONE/NEW_TOKEN/STREAM/PATH_CHALLENGE/PATH_RESPONSE/NEW_CONNECTION_ID/RETIRE_CONNECTION_ID/RESET_STREAM/STOP_SENDING/MAX_DATA/MAX_STREAM_DATA/MAX_STREAMS_BIDI/UNI frames when space allows, ACK-driven sent-packet tracking, ACK-driven frame-payload STREAM/CRYPTO and protected CRYPTO retransmission requeue, and a simplified recovery/congestion state object.

  • applyPeerTransportParameterBytesOrClose() is an opt-in wrapper for peer transport-parameter extension bytes. It preserves the successful application behavior of applyPeerTransportParameterBytes(), but queues transport CONNECTION_CLOSE with TRANSPORT_PARAMETER_ERROR and CRYPTO frame_type for malformed extensions or invalid peer-parameter semantics before returning InvalidPacket.

  • Aes128KeyPhaseState provides current/next 1-RTT key-phase state for one packet-protection direction. Callers can still own it directly through pollProtectedShortDatagramWithKeyPhaseState() and processProtectedShortDatagramWithKeyPhaseState(). The connection can also install OneRttTrafficSecrets from a backend, derive local/peer AES-128-GCM packet-protection keys, and own local/peer key-phase state for pollProtectedShortDatagramWithInstalledKeys() and processProtectedShortDatagramWithInstalledKeys(). Receive state advances only after packet authentication and Application-frame processing succeed. Connection-owned local key-update initiation requires modeled handshake confirmation, rejects a second update until an Application ACK covers a packet sent with the new key phase, and rolls that confirmation state back if a later frame in the same payload is invalid.

  • Config.enable_spin_bit enables a deterministic single-path 1-RTT spin-bit model on protected short packets. It defaults to false, so packetization keeps the prior false wire value; when enabled, server connections reflect the peer spin bit, client connections invert the latest server spin bit, nextOutgoingSpinBit() exposes the next value, and resetSpinBitForPath() clears state for a future path or destination-CID switch.

  • endpoint.EndpointRouter maps destination connection IDs to caller-owned connection handles and IPv4 UDP tuples. It can route long-header datagrams by the encoded DCID, write an RFC 8999 Version Negotiation response for unsupported long-header versions, register a client Initial Source CID before sending Initial so server Initial and Version Negotiation responses route back to the client connection, classify supported-version unknown-DCID Initial datagrams for future server accept loops with borrowed Original DCID, client Source CID, token, version, and path metadata, register the Original DCID plus server Initial SCID after accept without leaving partial routes on invalid/duplicate input, route short-header datagrams by registered-CID prefix matching, route zero-length CIDs by exact UDP tuple, reject unknown or ambiguous CIDs, reject stateless reset token reuse across different CIDs, store optional NEW_CONNECTION_ID sequence numbers, switch an Initial route from the original DCID to the Retry Source CID after Retry, commit caller-validated migration to a server preferred-address CID and UDP tuple, retire routes by CID, sequence number, retire_prior_to threshold, or caller connection handle including path-specific zero-CID routes, register a replacement CID and apply its retire_prior_to threshold as one endpoint policy operation, update a route to a caller-validated new path while rejecting stale path updates, enforce active_migration_disabled as a deterministic path-change rejection, expose stateless reset tokens for inactive or retired CIDs while suppressing tokens for active routes on the same path, write stateless reset datagrams with caller-supplied unpredictable bytes when the reset is smaller than the triggering datagram, classify received datagrams as routed, version negotiation, stateless reset, accept_initial, or dropped, and derive a stable remote IPv4/UDP peer-address binding for address-validation tokens. endpoint.AddressValidationPolicy owns one active token secret, a bounded previous-secret set, and a bounded replay filter for in-memory endpoint token issuance, rotation, validation, and replay rejection; it can export/restore AddressValidationSecretSet snapshots and replay-filter snapshots for external persistence or worker distribution. endpoint.EcnPathPolicy stores ECN validation state per UDP tuple so a migrated path starts from unknown instead of inheriting another path’s capable or failed state. Socket I/O, socket-backed reset emission, automatic path validation, production secret/replay storage, real IP ECN marking, and Connection ownership remain outside these helpers.

  • Locally initiated bidirectional streams must be created with openStream() before sendOnStream(). openStream() enforces the peer’s bidirectional stream limit until a larger MAX_STREAMS_BIDI frame is received.

  • Locally initiated unidirectional streams must be created with openUniStream() before sendOnStream(). openUniStream() enforces the peer’s unidirectional stream limit until a larger MAX_STREAMS_UNI frame is received.

  • sendOnStream() accepts observed peer-initiated bidirectional streams so the in-memory echo examples can reply on the peer’s stream, and it accepts opened locally initiated unidirectional streams. It rejects unobserved peer-initiated streams, locally initiated streams that were not opened, peer-initiated unidirectional stream IDs, streams that already sent FIN, and flow-control-blocked writes. Connection, stream, and stream-count blocked writes queue the matching BLOCKED frame before returning FlowControlBlocked; queued STREAM_DATA_BLOCKED is dropped before transmit if that send side later reaches FIN or reset.

  • resetStream() aborts the send side of an opened local stream or an observed peer-initiated bidirectional reply stream. It queues one RESET_STREAM with the current send offset as final size, marks the send side closed, ignores duplicate local resets, and drops unsent queued STREAM data after the reset frame is emitted.

  • stopSending() asks the peer to stop sending on an opened local bidirectional stream or an observed peer-initiated receive stream that is still in Recv or Size Known. It queues one STOP_SENDING, rejects send-only local unidirectional streams and unobserved peer streams, returns StreamClosed once all bytes through final size have already arrived, ignores duplicate local stop requests, drops queued STOP_SENDING frames before transmit if the receive side reaches Data Recvd or Reset Recvd first, and leaves the peer to respond with RESET_STREAM.

  • Inbound DATA_BLOCKED, STREAM_DATA_BLOCKED, and STREAMS_BLOCKED_* frames update highest-observed peer blocked limits exposed by peerDataBlockedLimit(), peerStreamDataBlockedLimit(), peerStreamsBlockedBidiLimit(), and peerStreamsBlockedUniLimit(). STREAM_DATA_BLOCKED validates that the stream has a receive side at this endpoint, creates receive state before any STREAM data when valid, and rejects send-only, unopened local bidirectional, or receive stream-count-exhausted stream IDs. Once a stream final size is known, STREAM_DATA_BLOCKED is discarded and no longer requeues or grows MAX_STREAM_DATA for that stream. If the peer reports a limit below the current receive-side credit, the matching MAX_DATA, MAX_STREAM_DATA, or MAX_STREAMS_* frame is requeued. If configured, current-limit peer BLOCKED reports grow MAX_DATA/MAX_STREAM_DATA from the receive byte windows and MAX_STREAMS_BIDI/UNI from receive_stream_count_window. Invalid multi-frame payloads roll these reports, receive-state creation, and any requeued or grown MAX frames back.

  • recvOnStream() reads bidirectional streams and peer-initiated unidirectional receive streams. Locally initiated unidirectional stream IDs are send-only and are rejected on the receive API. When an inbound frame opens a higher-numbered receive stream, lower-numbered streams of the same type are also created and read as idle until data arrives. Successful reads increase the receive-side connection and stream credit by the consumed byte count, queueing MAX_DATA and MAX_STREAM_DATA frames while dropping obsolete lower queued limits. Queued MAX_STREAM_DATA frames are also dropped before transmit once the stream leaves Recv by learning a final size or reset. Fully consumed peer-initiated FIN streams release one receive stream-count credit and queue MAX_STREAMS_BIDI or MAX_STREAMS_UNI, including zero-length FIN streams observed through the receive API. Peer-initiated reset streams release the same stream-count credit once the reset is observed through recvOnStream(). recvStreamFinalSize() exposes the final size learned from STREAM FIN or RESET_STREAM, and recvStreamFinished() reports successful FIN completion only after all bytes through the final size have been consumed.

  • Inbound MAX_STREAM_DATA updates send credit only for streams where this endpoint has a send side and that send side has not already sent FIN. It rejects unopened locally initiated streams and receive-only peer unidirectional streams. For peer-initiated bidirectional streams, it can create both receive and send state before any STREAM data so future replies use the advertised credit.

  • processDatagram() accepts modeled bidirectional STREAM/RESET_STREAM receive state and peer-initiated unidirectional STREAM/RESET_STREAM receive state in the Application packet number space; processDatagramInSpace() allows the same frame-payload skeleton to be exercised in Initial, Handshake, or Application packet number space, and processDatagramForPacketType() validates frame legality for Initial, Handshake, 0-RTT, or 1-RTT while mapping 0-RTT and 1-RTT to the shared Application packet number space. Initial and Handshake packet types reject RFC 9000-forbidden frame types, including STREAM, flow-control, connection-ID, path-validation, NEW_TOKEN, application close, and HANDSHAKE_DONE frames. The 0-RTT packet type accepts application frames including STREAM, RESET_STREAM, and STOP_SENDING, rejects ACK, ACK_ECN, CRYPTO, HANDSHAKE_DONE, NEW_TOKEN, PATH_RESPONSE, and RETIRE_CONNECTION_ID frames, and preserves Application-space ACK/recovery accounting; invalid multi-frame payloads roll back earlier state from the same payload. processDatagramOrClose(), processDatagramInSpaceOrClose(), processDatagramForPacketTypeOrClose(), and the protected long/short *OrClose receive wrappers are opt-in APIs that keep that rollback boundary for success paths, but queue transport CONNECTION_CLOSE for classified frame encoding such as invalid ACK/ACK_ECN ranges and STREAMS_BLOCKED limits, packet-type violations including 0-RTT ACK/ACK_ECN frames, or semantic frame-processing failures such as ACK/ACK_ECN frames that acknowledge unsent packet numbers, conflicting STREAM data, and invalid stream-control frames before returning InvalidPacket. Stream receive handling buffers non-overlapping out-of-order STREAM ranges until gaps are filled, ignores identical duplicate retransmitted data that is already contiguous or exactly matches a pending range, trims already-received contiguous prefixes before appending new suffix bytes, ignores final-size-bounded late STREAM data once all data has been received, applies same-final-size RESET_STREAM to Size Known streams that still have gaps by accounting the remaining final-size credit and closing the receive side, ignores later STREAM data after RESET_STREAM when it stays within the known final size, and rejects inbound local bidirectional stream IDs that were not opened, inbound local unidirectional stream IDs, peer-initiated streams beyond receive stream-count limits, conflicting or ambiguous overlapping stream data before Data Recvd, data after final size, STREAM FINs that would change the reset final size, inconsistent RESET_STREAM final sizes, oversized frame payloads, and ACK/ACK_ECN frames for packet numbers that were never sent in the selected packet number space.

  • EndpointConnectionLifecycle exposes direct and routed *OrClose variants for protected Initial, long-header, 0-RTT, short-header, explicit key-phase, and installed-key receive helpers. These helpers route by endpoint CID/tuple first, then delegate to the matching connection close-propagating receive API, leaving the older lifecycle rollback-only receive APIs unchanged.

  • ACK_ECN frames share ACK range handling and unsent-packet close classification with regular ACK frames for sent-packet recovery. The connection skeleton also validates modeled ECT(0)/ECT(1) sent packets against cumulative ACK_ECN counters per packet number space. A validated increase in the ECN-CE counter enters the same NewReno congestion recovery period used for loss without treating the acknowledged packet as lost; repeated CE increases for packets sent before that recovery start do not reduce the congestion window again. Validation failure records EcnValidationState.failed instead of closing the connection, because the frame-payload API does not yet own real IP ECN marking or network path identity.

  • discardPacketNumberSpace() models the recovery side effect of discarding Initial or Handshake keys. It clears pending ACK, largest acknowledged packet, sent-packet tracking, queued/received CRYPTO state, bytes in flight, loss deadline, PTO backoff, and ECN validation counters/state for the discarded space, clears installed Handshake packet-protection keys when the Handshake space is discarded, rejects later frame-payload use of that space, and leaves Application data space intact. Client-side successful Handshake packet sends and server-side successful Handshake packet receives now trigger Initial-space discard only after the send/receive commit succeeds; failed sends, invalid payloads, and packet-authentication failures preserve Initial state. Server-side sendHandshakeDone() discards Handshake state while keeping the 1-RTT HANDSHAKE_DONE frame queued, backend-confirmed no-output Handshake drives discard the same state, and installing Handshake keys after discard is rejected. Installed peer 0-RTT receive keys require explicit acceptZeroRtt() before processProtectedZeroRttDatagramWithInstalledKeys() can commit early data, and rejectZeroRtt() discards those keys before use. discardZeroRttProtectionKeys() explicitly clears installed early-data keys without discarding the shared Application packet number space; clients also clear installed 0-RTT keys when 1-RTT keys are installed, and servers clear installed 0-RTT keys only after a 1-RTT short packet authenticates and its Application-frame payload is accepted.

  • Initial, Handshake, and Application CRYPTO byte streams have independent offsets, send queues, receive buffers, out-of-order pending buffers, pending ACKs, sent-packet tracking, and rollback behavior. CRYPTO receive handling bounds accepted end offsets with Config.max_crypto_buffer_size, buffers gaps until contiguous bytes are available to recvCryptoInSpace(), ignores identical retransmissions against already-buffered contiguous bytes or exact pending ranges, and rejects conflicting overlaps. Close-on-error receive APIs map configured CRYPTO receive-buffer violations to CRYPTO_BUFFER_EXCEEDED. driveCryptoBackendInSpace() can give local transport-parameter extension bytes to a caller-supplied CryptoBackend, apply peer transport-parameter extension bytes returned by that backend, install returned Handshake, 0-RTT, and 1-RTT traffic secrets, deliver contiguous CRYPTO bytes, queue backend-produced bytes back into the matching CRYPTO send stream, and mark the modeled handshake confirmed when the backend reports completion. If a Handshake-space backend drive confirms the handshake without queuing outbound CRYPTO, it discards Handshake state and installed Handshake keys in the same call; if outbound Handshake CRYPTO was queued, the space is preserved so the flight can be sent first. This is still a frame-payload TLS bridge hook; it does not yet run a real TLS 1.3 backend.

  • RTT updates decode ACK Delay with the peer ack_delay_exponent. Initial and Handshake ACK Delay are ignored, and each valid largest-acknowledged RTT sample updates connection-level RTT estimates across non-discarded packet number spaces. Once HANDSHAKE_DONE or confirmHandshake() marks the handshake confirmed, Application-space RTT updates cap decoded ACK Delay by the peer max_ack_delay.

  • ACK processing applies simplified packet-threshold and time-threshold loss detection in the selected packet number space. When an unacknowledged sent packet is at least three packet numbers older than the largest newly acknowledged packet, or when its sent_time + 9/8 * max(latest_rtt, smoothed_rtt) deadline has elapsed with the 1ms granularity floor, it is removed from sent tracking and reported to the recovery state as lost. The recovery state grows the congestion window by newly acknowledged bytes during slow start, and by at least one byte using max_datagram_size * bytes_acked / congestion_window during congestion avoidance. applyPeerTransportParameters() now resyncs the recovery max_datagram_size; if peer max_udp_payload_size lowers the effective send size, all packet-number-space recovery states reset their congestion window to the recalculated initial congestion window for that smaller size. A NewReno-style recovery period prevents repeated congestion-window reductions for losses or ECN-CE congestion events involving packets sent before recovery started, and ACKs for those packets do not grow the congestion window. When a loss or ECN-CE signal starts a new recovery period and there is already ack-eliciting data queued, the selected packet number space gets a one-packet recovery probe allowance so the retransmission can be emitted even when bytes in flight now fills the reduced congestion window. If a contiguous lost interval sent after the first RTT sample spans the RFC 9002 persistent congestion duration, the congestion window is reduced to the minimum window; when that ACK also produced the newest RTT sample, min_rtt is refreshed to that sample and re-synced across packet number spaces. Non-contiguous lost packet numbers spanning the same duration stay on the normal NewReno recovery path. lossDetectionDeadlineMillis() exposes the next modeled loss deadline and checkLossDetectionTimeouts() applies due deadlines under a controlled clock. ptoDeadlineMillis() exposes the simplified PTO deadline, lossDetectionTimerDeadlineMillis() selects the aggregate loss-time-before-PTO timer deadline across packet number spaces, and serviceLossDetectionTimer() is the connection helper for servicing that due aggregate timer. EndpointLossDetectionTimers owns endpoint/event-loop scheduling across caller connection handles, refreshes each handle after timer service, and is used by the socket-backed UDP loss/PTO examples to arm, service, and disarm protected-packet timers after ACK or loss cleanup. PTO handling uses one connection-level backoff count, resets it on acknowledged ack-eliciting packets except client Initial ACKs or Initial/Handshake space discard, disarms server PTO while anti-amplification-limited with no send credit, re-arms and services expired PTO when recordPeerAddressDatagramReceived() grants new send credit after a received datagram, and still prefers already queued ack-eliciting data, in-flight CRYPTO, then Application STREAM as the probe before queueing PING probes in due Initial, Handshake, or Application packet number spaces. Full socket-owned connection lifecycle and TLS-owned protected-packet timer integration remain pending.

  • Inbound STOP_SENDING is accepted only for streams where this endpoint has a send side. For peer-initiated bidirectional streams, it can create receive state before any STREAM data, resets only the local send side, and leaves the receive side open for later peer data. It uses the same send-side close and RESET_STREAM queueing behavior as resetStream().

  • Inbound NEW_TOKEN is accepted only by client connections and stored as opaque future address-validation data up to Config.max_stored_new_tokens. Servers can create and later validate HMAC-SHA256 address-bound, version-bound, expiring NEW_TOKEN values with issueAddressValidationToken(), issueAddressValidationTokenForVersion(), validateAddressValidationToken(), validateAddressValidationTokenForVersion(), or validateAddressValidationTokenWithSecrets(); endpoint code can use AddressValidationPolicy to issue tokens for Udp4Tuple.peerAddressValidationBinding(), validate across a rotated secret set, export/restore the active/previous token secrets through AddressValidationSecretSet, export/restore replay-filter fingerprints through ReplayFilterSnapshot, and reject reuse after restore. Server-side receipt fails the payload as invalid and rolls back any earlier changes from that payload.

  • Server-side sendHandshakeDone() marks the modeled handshake confirmed, discards Handshake packet-number-space state plus installed Handshake keys, and leaves the 1-RTT HANDSHAKE_DONE frame queued until a later pollTx() or protected short-packet send commits. Inbound HANDSHAKE_DONE is accepted only by client connections, marks the modeled handshake confirmed, and after the full payload is accepted discards the same Handshake state. Server-side receipt fails the payload as invalid and rolls back any earlier changes from that payload.

  • Invalid multi-frame payloads roll back state changed earlier in that payload, including handshake confirmation, CRYPTO receive and pending buffers, stream receive buffers, RESET_STREAM state, stored NEW_TOKEN values, queued PATH_RESPONSE/RETIRE_CONNECTION_ID/RESET_STREAM values, outstanding PATH_CHALLENGE values and retry metadata, peer-issued and local connection ID retired flags, MAX_DATA/MAX_STREAMS_BIDI/UNI updates, selected-space pending ACK state, selected-space sent-packet recovery, packet/time-threshold loss, and ECN validation state, and close-state changes.

  • Current pollTx / processDatagram behavior moves unencrypted QUIC frame payload bytes. pollTx may emit ACK-only payloads, queued CONNECTION_CLOSE/APPLICATION_CLOSE, PING, CRYPTO, HANDSHAKE_DONE, NEW_TOKEN, PATH_CHALLENGE, PATH_RESPONSE, NEW_CONNECTION_ID, RETIRE_CONNECTION_ID, MAX_/BLOCKED, RESET_STREAM, or STREAM payloads, or coalesce a pending ACK when space allows. Successful send/receive activity refreshes the modeled idle timeout deadline; invalid frame payloads do not. On server connections with an unvalidated peer address, pollTx() and pollTxInSpace() enforce the modeled 3x anti-amplification budget across packet number spaces until validatePeerAddress() is called directly or a matching Retry token is consumed. Server connections can issue a Retry datagram with issueRetryDatagram(), which records the Retry Source Connection ID for local transport-parameter export and registers the token for one-time validation. issueAddressValidationToken() creates HMAC-SHA256 address-bound, version-bound, expiring Retry or NEW_TOKEN values, AddressValidationPolicy can issue those tokens for Udp4Tuple.peerAddressValidationBinding(), validate them across current/previous secrets, and reject already-used token fingerprints through its replay filter, while validateAddressValidationToken() / validateAddressValidationTokenWithSecrets() validate the token kind, originating version, peer address, lifetime, and MAC before lifting the anti-amplification limit; Retry tokens are also consumed once from the pending Retry-token set. Client connections can validate a Retry datagram with processRetryDatagram(); a stored Retry token is reused by protected Initial packetization when the explicit Initial token argument is empty, and applyPeerTransportParameters() validates original_destination_connection_id plus retry_source_connection_id against the stored Retry CIDs. The first successfully sent protected client Initial records the Original Destination Connection ID for later server transport-parameter validation. The first successfully sent protected Initial records this endpoint’s Initial Source Connection ID through localInitialSourceConnectionId(), and localTransportParameters() exports it as initial_source_connection_id once known. The first successfully opened protected client Initial records the Original Destination Connection ID for server localTransportParameters().original_destination_connection_id export. The first successfully opened protected Initial records the peer’s Initial Source Connection ID through peerInitialSourceConnectionId(), and applyPeerTransportParameters() validates initial_source_connection_id against that recorded value when it is known. Initial and Handshake CRYPTO, ACK-only, and PING packets can also be emitted as protected long packets, coalesced through pollProtectedLongDatagram(), and received either one packet at a time through processProtectedLongDatagramInSpace() or as a coalesced long datagram through processProtectedLongDatagram(); the matching *OrClose variants queue CONNECTION_CLOSE after authenticated protected plaintext frame errors. Handshake packets can also use connection-installed keys through pollProtectedHandshakeDatagramWithInstalledKeys() / processProtectedHandshakeDatagramWithInstalledKeys(). Caller-keyed or installed-key 0-RTT STREAM/RESET_STREAM/STOP_SENDING packets use the Application packet number space and 0-RTT frame rules through pollProtectedZeroRttDatagram() / processProtectedZeroRttDatagram(), pollProtectedZeroRttDatagramWithInstalledKeys() / processProtectedZeroRttDatagramWithInstalledKeys(), or pollProtectedLongDatagram() / processProtectedLongDatagram() with keys.zero_rtt; the 0-RTT receive helpers also expose close-propagating *OrClose variants. Protected 1-RTT short PING/ACK/CRYPTO/HANDSHAKE_DONE/NEW_TOKEN/NEW_CONNECTION_ID/PATH_CHALLENGE/PATH_RESPONSE/RETIRE_CONNECTION_ID/MAX_/BLOCKED/STREAM/RESET_STREAM/STOP_SENDING/CONNECTION_CLOSE datagrams can be emitted through pollProtectedShortDatagram() and opened through processProtectedShortDatagram() with caller-supplied keys; processProtectedShortDatagramOrClose() and the key-update/installed-key *OrClose variants queue CONNECTION_CLOSE for authenticated protected plaintext frame errors. When enabled, the short-header spin bit is updated only after packet authentication and Application-frame processing succeed.

  • Not implemented yet: real TLS 1.3 backend integration, endpoint datagram APIs beyond the current caller-keyed/installed-key protected long/short packet bridges, full TLS-owned live 1-RTT key-update scheduling and old-key discard, full TLS 0-RTT replay policy, remaining TLS-triggered key discard scheduling beyond the implemented Initial/HANDSHAKE_DONE hooks, full TLS backend transcript ownership for transport parameters, socket-owned protected-packet RFC 9002 loss/PTO timer lifecycle integration, socket-backed UDP 4-tuple connection ownership, socket-owned endpoint Retry issuance/accept loops, automatic socket-backed migration to a server preferred_address, production endpoint token-secret/replay storage and distribution around exported snapshots, ECN validation bound to real IP-header marking, automatic socket-backed path validation binding to endpoint path identity, full address-validation policy, full DCID routing lifecycle, socket-owned connection ID replacement policy, socket-backed stateless reset emission for unknown CIDs, full QUIC v2 behavior beyond Initial key derivation, long-header type-bit mapping, configured protected long-packet/Retry version use, Retry integrity helpers, token version binding, and RFC 9368 version-information primitives, and full path migration policy.

  • 0-RTT resumption (RFC 8446 §2.2 + RFC 9001 §4.6): the pure-Zig TLS 1.3 stack implements the full client-side key chain (resumption_master_secret -> PSK -> early_traffic_secret -> binder_key -> psk_binder), ClientHello carries pre_shared_key + early_data extensions, the CryptoBackend pull_zero_rtt_traffic_secrets hook exposes the 0-RTT early traffic secret, and the Connection installs the local 0-RTT packet-protection key and emits early-data STREAM. The server validates the ClientHello pre_shared_key identity and binder vectors without truncating identities, derives the early secret from a configured PSK, constant-time-verifies the psk_binder, and only on a matching binder derives the client early traffic secret and installs it as the peer 0-RTT key; acceptZeroRtt() then gates processProtectedZeroRttDatagramWithInstalledKeys() for early-data delivery. The server emits a post-handshake NewSessionTicket after the handshake; the client validates its handshake length, ticket vector, mandatory extensions vector, and local storage bound before deriving a resumption PSK and storing the ticket, closing the PSK source for 0-RTT. rejectZeroRtt() discards installed peer 0-RTT keys before use; clients also clear 0-RTT keys when 1-RTT keys are installed.

Further phases will extend towards full RFC coverage.