# flare.quic.client

> A module of flare_mlake's API.

``flare.quic.client`` -- QUIC v1 client connection driver.

The client-role mirror of :class:`flare.quic.server.QuicListener`'s
per-connection path, collapsed to a single connection over one
connected UDP flow. It reuses every codec the server reuses --
packet headers (:mod:`flare.quic.packet`), frames
(:mod:`flare.quic.frame`), the sans-I/O state machine
(:class:`flare.quic.state.Connection`), Initial-level AEAD
(:func:`flare.quic.protection.protect_initial_packet` /
``unprotect_initial_packet``), and the rustls QUIC session for
Handshake / 1-RTT protection -- and only adds the client-role
bits: a client-chosen Initial DCID, the ClientHello first flight,
client-direction Initial secrets (``is_server=False``), and the
client packet-number spaces.

## Handshake flow (RFC 9001 §4 + RFC 9000 §7)

1. :meth:`QuicClientConnection.start` picks a random Initial DCID
   (the value both ends derive Initial secrets from, RFC 9001
   §5.2) and a client Source CID, builds a
   :class:`flare.tls.rustls_quic.RustlsQuicSession` via the
   connector, drains the ClientHello, and emits the first Initial
   packet padded to >= 1200 bytes (RFC 9000 §14.1).
2. :meth:`poll` drains the inbound socket queue: the server's
   Initial (ServerHello) decrypts off the Initial secret; its
   Handshake flight (EncryptedExtensions / Certificate /
   CertificateVerify / Finished) decrypts through the rustls
   session. Inbound CRYPTO feeds rustls; outbound CRYPTO (the
   client Finished) drains back out and ships in a Handshake
   packet. ACKs are emitted per packet-number space.
3. Once rustls reports the handshake complete and installs 1-RTT
   keys, the connection is ESTABLISHED and the H3 layer
   drives request / response STREAM frames through
   :meth:`send_stream` / :meth:`poll`.

## Scope / ceilings

This driver targets the common single-path, low-loss case the
HTTP/3 client needs:

- Minimal PTO-driven retransmission for the 1-RTT (request) path
  via :mod:`flare.quic._loss_recovery`: ack-eliciting 1-RTT packets
  are tracked, retired as ACKs arrive, and retransmitted on a probe
  timeout. This is not the full RFC 9002 implementation -- it omits
  ACK-based loss detection, an RTT estimator + congestion control, and
  handshake-level (Initial / Handshake CRYPTO) + server-side
  response retransmit. Those are tracked as a loss-recovery
  follow-up. During the handshake a dropped Finished is still
  recovered by the peer re-driving our flight.
- STREAM sends are fragmented into path-MTU-sized packets. The caller
  pumps the connection when peer flow-control credit is exhausted.
- The server's advertised transport parameters are decoded once the
  handshake completes (:meth:`QuicClientConnection._apply_peer_transport_
  params`): the client clamps its egress datagram size to the peer's
  ``max_udp_payload_size`` and gates STREAM sends on the peer's
  per-stream / connection flow-control limits, including subsequent
  MAX_DATA / MAX_STREAM_DATA updates.

References:
- RFC 9000 §7 "Cryptographic and Transport Handshake".
- RFC 9001 §4-§5 "Packet Protection" + "Initial secrets".
- RFC 9000 §14.1 "Initial datagram size" (>= 1200 bytes).

## Structs

### `QuicClientConnection`

```mojo
struct QuicClientConnection
```

A single client-role QUIC connection over a connected UDP flow.

Owns the UDP socket, the rustls client session, and the
sans-I/O :class:`flare.quic.state.Connection`. Construct via
:meth:`start` (non-blocking: sends the first Initial) or
:meth:`connect` (blocks until the handshake completes). After
the handshake the H3 layer drives streams via
:meth:`open_bidi_stream`, :meth:`send_stream`, and :meth:`poll`.

Implements: `Movable`

Fields:

- `conn: Connection`: Sans-I/O connection state: per-stream + flow-control bookkeeping, reused unchanged from the server path.
- `session: RustlsQuicSession`: The rustls client session: drives the TLS 1.3 handshake inside CRYPTO frames and holds the Handshake / 1-RTT AEAD keys.
- `sock: UdpSocket`
- `peer: SocketAddr`
- `initial_dcid: ConnectionId`: Client-chosen Destination CID placed on the first Initial. Both ends derive Initial secrets from this value (RFC 9001 §5.2); it is the AEAD ``dcid`` for every Initial packet regardless of any later CID change.
- `first_dcid: ConnectionId`: The DCID of our very first Initial, before any Retry. The server must echo it as ``original_destination_connection_id``.
- `retry_scid: List[UInt8]`: Source CID of the Retry we followed, empty if none.
- `server_scid: List[UInt8]`: Source CID of the server's first Initial; the server must echo it as ``initial_source_connection_id``.
- `dcid: ConnectionId`: Outbound Destination CID for routing. Starts equal to :attr:`initial_dcid`; switches to the server's Source CID once the first server long-header packet is parsed (RFC 9000 §7.2).
- `scid: ConnectionId`: Client Source CID -- our own identity. Its length is the pinned DCID length for parsing inbound short-header 1-RTT packets (RFC 9000 §17.3).
- `aead_choice: Int`
- `max_udp_payload_size: Int`
- `tx_initial_pn: UInt64`
- `tx_initial_offset: UInt64`
- `tx_handshake_pn: UInt64`
- `tx_handshake_offset: UInt64`
- `tx_1rtt_pn: UInt64`
- `key_phase: Bool`: 1-RTT key phase bit we send; flips on every key update (RFC 9001 sec 6).
- `tx_initial_crypto: List[UInt8]`: Outbound Initial-level CRYPTO awaiting egress (the ClientHello before it is first sent; empty afterwards).
- `tx_handshake_crypto: List[UInt8]`: Outbound Handshake-level CRYPTO (the client Finished).
- `tx_1rtt_crypto: List[UInt8]`: Outbound 1-RTT-level CRYPTO (rare client post-handshake messages).
- `rx_initial_ranges: List[UInt64]`
- `rx_handshake_ranges: List[UInt64]`
- `rx_1rtt_ranges: List[UInt64]`
- `rx_initial_ack_pending: Bool`
- `rx_handshake_ack_pending: Bool`
- `rx_1rtt_ack_pending: Bool`
- `rx_initial_largest: UInt64`
- `rx_handshake_largest: UInt64`
- `rx_1rtt_largest: UInt64`
- `reasm: _CryptoReasm`
- `have_hs_keys: Bool`
- `have_1rtt_keys: Bool`
- `got_server_cid: Bool`
- `established: Bool`
- `retry_token: List[UInt8]`: Address-validation token from an inbound Retry (RFC 9000 sec 8.1), echoed on every subsequent Initial. Empty until a Retry arrives.
- `retried: Bool`: True once a Retry has been consumed; further Retries are ignored (RFC 9000 sec 17.2.5.2).
- `first_initial_crypto: List[UInt8]`: Cached ClientHello CRYPTO so the Initial can be re-sent verbatim (with the token + new Initial keys) after a Retry.
- `next_bidi_stream: UInt64`: Next client-initiated bidirectional stream id to hand out (RFC 9000 §2.1: client bidi ids are 0, 4, 8, ...).
- `next_uni_stream: UInt64`: Next client-initiated unidirectional stream id to hand out (RFC 9000 §2.1: client uni ids are 2, 6, 10, ...). H3 opens three of these per connection: control + QPACK encoder/decoder.
- `send_offsets: Dict[UInt64, UInt64]`: Per-stream cumulative send offset for outbound STREAM frames.
- `enable_0rtt: Bool`: Opt-in TLS session resumption / 0-RTT (RFC 9001 §4.6). Off by default so the standard path is byte-for-byte unchanged. When the same :class:`RustlsQuicConnector` is reused for a later connection to a server that issued a 0-RTT-capable ticket, rustls resumes the session and (in-process only -- rustls 0.23 has no cross-process ticket export) exposes early keys.
- `ecn_rx: List[UInt64]`: ECT(0), ECT(1) and CE counts of 1-RTT packets received, reported in ACK_ECN frames (RFC 9000 sec 13.4.1).

#### `__init__`

```mojo
def __init__(out self, var conn: Connection, var session: RustlsQuicSession, var sock: UdpSocket, peer: SocketAddr, var initial_dcid: ConnectionId, var scid: ConnectionId, aead_choice: Int, max_udp_payload_size: Int)
```

#### `start`

```mojo
def start(peer: SocketAddr, connector: RustlsQuicConnector, server_name: String, max_idle_timeout_ms: UInt64 = UInt64(30000), initial_max_data: UInt64 = UInt64(1048576), max_udp_payload_size: Int = Int(1452), aead_choice: Int = Int(0), enable_0rtt: Bool = False, max_datagram_frame_size: UInt64 = UInt64(0), token: List[UInt8] = List(), ecn: Bool = True) -> Self
```

Open a client connection and emit the first Initial (ClientHello) packet without blocking on the response.

Picks the Initial DCID + client SCID, builds the rustls
client session with the encoded client transport
parameters, drains the ClientHello, and ships the padded
Initial datagram. Drive the handshake to completion with
:meth:`poll` (or use :meth:`connect` for the blocking
convenience).

``max_datagram_frame_size`` > 0 accepts DATAGRAM frames up to
that size (RFC 9221); they arrive in ``poll``'s
``events.datagrams``. ``token`` is an earlier connection's
:meth:`address_token` for the same server, which spares a Retry
round trip (RFC 9000 sec 8.1.3). ``ecn`` marks packets ECT(0) and
reports the server's marks (RFC 9000 sec 13.4; Linux).

#### `connect`

```mojo
def connect(peer: SocketAddr, connector: RustlsQuicConnector, server_name: String, handshake_timeout_ms: Int = Int(5000), max_idle_timeout_ms: UInt64 = UInt64(30000), initial_max_data: UInt64 = UInt64(1048576), max_udp_payload_size: Int = Int(1452), aead_choice: Int = Int(0), enable_0rtt: Bool = False, max_datagram_frame_size: UInt64 = UInt64(0), token: List[UInt8] = List(), ecn: Bool = True) -> Self
```

Open a client connection and block until the QUIC + TLS handshake completes (or ``handshake_timeout_ms`` elapses).

Raises on timeout. On return the connection is
ESTABLISHED and ready for :meth:`send_stream`.

``enable_0rtt`` opts into session resumption (RFC 9001 §4.6):
reuse the same :class:`RustlsQuicConnector` across connections
so a later one resumes the cached session. Off by default.

#### `poll`

```mojo
def poll(mut self, timeout_ms: Int = Int(100)) -> ConnectionEvents
```

Receive a burst of inbound datagrams, advance the handshake / stream state, flush pending ACKs + CRYPTO, and return the per-call :class:`ConnectionEvents`.

Blocks up to ``timeout_ms`` for the first datagram, then
drains the rest of the socket queue non-blocking (up to
:data:`_POLL_BATCH`) so one poll consumes a whole server
flight. Returns empty events on a clean timeout.

#### `open_bidi_stream`

```mojo
def open_bidi_stream(mut self) -> UInt64
```

Allocate the next client-initiated bidirectional stream id (RFC 9000 §2.1: 0, 4, 8, ...). The H3 request layer opens one bidi stream per request.

#### `open_uni_stream`

```mojo
def open_uni_stream(mut self) -> UInt64
```

Allocate the next client-initiated unidirectional stream id (RFC 9000 §2.1: 2, 6, 10, ...). H3 opens three of these per connection (control + QPACK encoder/decoder).

#### `send_stream`

```mojo
def send_stream(mut self, stream_id: UInt64, data: List[UInt8], fin: Bool)
```

Send ``data`` (with optional FIN) on ``stream_id``, fragmenting across as many 1-RTT STREAM frames / packets as the path MTU requires and advancing the per-stream offset. An empty FIN-only send emits a single zero-length frame so a body-less request still closes its stream.

#### `send_datagram`

```mojo
def send_datagram(mut self, payload: Span[UInt8]) -> Bool
```

Send ``payload`` now as an unreliable DATAGRAM (RFC 9221) in a packet of its own, never retransmitted (sec 5.2). False, and nothing sent, before the server's transport parameters are known, when it takes no DATAGRAMs or the frame exceeds its ``max_datagram_frame_size`` or one packet, or when the congestion window is full.

#### `cancel_stream`

```mojo
def cancel_stream(mut self, stream_id: UInt64)
```

Cancel both directions of an HTTP/3 request (H3_REQUEST_CANCELLED).

#### `send_capacity`

```mojo
def send_capacity(self, stream_id: UInt64) -> Int
```

Current flow-control credit for a client-initiated stream.

#### `stream_reset`

```mojo
def stream_reset(self, stream_id: UInt64) -> Bool
```

#### `release_stream_credit`

```mojo
def release_stream_credit(mut self, stream_id: UInt64, consumed: UInt64)
```

Replenish receive credit for newly consumed, ordered stream bytes.

#### `send_stream_early`

```mojo
def send_stream_early(mut self, stream_id: UInt64, data: List[UInt8], fin: Bool)
```

Send ``data`` (with optional FIN) on ``stream_id`` at 0-RTT (EarlyData), before the handshake completes.

The 0-RTT counterpart of :meth:`send_stream`: same
MTU-bounded STREAM-frame fragmentation and per-stream offset
bookkeeping (it shares :attr:`send_offsets`), but each frame
rides a :meth:`_build_0rtt` packet protected with the resumed
connection's EarlyData keys. Requires
:attr:`_early_keys_ready` (a resumed connection that installed
0-RTT keys -- see :meth:`early_data_ready`).

The full send is recorded in :attr:`_early_flight` so
:meth:`finish_early_data` can replay it at 1-RTT if the server
rejects early data. The emitted 0-RTT packets are registered with
the loss-recovery tracker (see :meth:`_build_0rtt`), so a lost
0-RTT packet is retransmitted as 1-RTT once keys are up (the
shared packet-number space makes this transparent).

On the *reject* path the registered 0-RTT packets are
never acked (the server discarded them), so the PTO eventually
re-sends their frames in addition to :meth:`finish_early_data`'s
explicit 1-RTT replay. Both carry identical stream offsets, so
the receiver dedupes them -- the only cost is a few redundant
packets on rejection. Upgrade path: a drop-by-pn on the loss
tracker invoked from the reject branch.

#### `finish_early_data`

```mojo
def finish_early_data(mut self) -> Bool
```

Resolve the 0-RTT flight after the handshake completes.

Call once the connection is ESTABLISHED. Returns ``True`` when
the server accepted early data (the 0-RTT flight stands, nothing
more to send) and ``False`` when it rejected it (RFC 9001 sec
4.6) -- in which case the recorded flight
(:attr:`_early_flight`) is replayed at 1-RTT so the request
still completes, transparently to the caller.

A rejected 0-RTT flight leaves no stream state on the server
(rustls drops the early data), so the replay resends the
identical STREAM frames on the **same** stream ids from offset
0: each distinct early stream's offset is reset, then every
recorded send is re-emitted in order via :meth:`send_stream`
(1-RTT). The H3 response reader keys on the stream id, so it is
oblivious to which flight delivered the request.

#### `keepalive`

```mojo
def keepalive(mut self)
```

Send a 1-RTT PING to keep a pooled-idle connection alive and elicit a server ACK (RFC 9000 §10.1.2). A no-op until 1-RTT keys are installed.

#### `migrate`

```mojo
def migrate(mut self, rebind: Bool = True) -> Bool
```

Migrate the connection to a fresh network path.

Switches the active Destination CID to a spare the server
granted via NEW_CONNECTION_ID (RFC 9000 §9.5 requires an
unused CID per path so the two paths cannot be linked),
optionally rebinds the local UDP socket to a new ephemeral
port (the address change that defines a migration), and
probes the new path with a PATH_CHALLENGE (§8.2). The peer
echoes a PATH_RESPONSE which :meth:`poll` matches to mark
:attr:`path_validated`. The old CID is retired.

Returns ``False`` if the connection is not established or the
server has not granted a spare CID yet (no migration is
possible without one). Use :meth:`migrate_and_validate` for
the blocking convenience that polls until the new path is
confirmed.

#### `migrate_and_validate`

```mojo
def migrate_and_validate(mut self, rebind: Bool = True, timeout_ms: Int = Int(2000)) -> Bool
```

Migrate (see :meth:`migrate`) then block until the new path is validated by a PATH_RESPONSE or ``timeout_ms`` elapses. Returns ``True`` on a validated path.

#### `initiate_key_update`

```mojo
def initiate_key_update(mut self)
```

Start a 1-RTT key update (RFC 9001 sec 6.1): later packets go out under the next key generation and the other phase bit.

#### `address_token`

```mojo
def address_token(self) -> List[UInt8]
```

The server's latest NEW_TOKEN token (empty if none), to pass as ``token`` when connecting to it again (RFC 9000 sec 8.1.3).

#### `is_established`

```mojo
def is_established(self) -> Bool
```

Whether the QUIC + TLS handshake has completed and 1-RTT keys are installed.

#### `early_data_ready`

```mojo
def early_data_ready(self) -> Bool
```

Whether this connection resumed a session and rustls installed 0-RTT (EarlyData) keys (RFC 9001 §4.6). True only when :attr:`enable_0rtt` was set, the same connector was reused, and the server's prior ticket allowed early data.

When True, :meth:`send_stream_early` emits application STREAM
frames in 0-RTT (EarlyData) packets and
:meth:`finish_early_data` resolves acceptance after the
handshake (replaying at 1-RTT on rejection).

#### `early_data_accepted`

```mojo
def early_data_accepted(self) -> Bool
```

Whether the server accepted early data for this resumed connection (RFC 8446 §4.2.10; meaningful after the handshake completes). False on a fresh connection or when 0-RTT was not enabled. A client that sent 0-RTT data must replay it at 1-RTT when this is False.

#### `path_validated`

```mojo
def path_validated(self) -> Bool
```

Whether the most recent :meth:`migrate` probe has been confirmed by a matching PATH_RESPONSE (RFC 9000 §8.2).

#### `active_dcid_seq`

```mojo
def active_dcid_seq(self) -> UInt64
```

Sequence number of the peer Connection ID currently used as the active Destination CID (0 is the handshake CID).

#### `peer_cid_count`

```mojo
def peer_cid_count(self) -> Int
```

Number of spare peer Connection IDs learned via NEW_CONNECTION_ID. A migration needs at least one.

#### `alpn`

```mojo
def alpn(self) -> String
```

Negotiated ALPN identifier (e.g. ``"h3"``). Meaningful only after the handshake completes.

#### `local_addr`

```mojo
def local_addr(self) -> SocketAddr
```

The ephemeral local UDP address the client bound to.

#### `shutdown`

```mojo
def shutdown(mut self)
```

Gracefully close: send a 1-RTT application-level CONNECTION_CLOSE (RFC 9000 sec 10.2, error 0) so the peer tears the connection down immediately instead of waiting out its idle timeout, then close the socket. A no-op send until 1-RTT keys are installed.

#### `close`

```mojo
def close(mut self)
```

Close the underlying UDP socket. Idempotent.
