flare_mlake · API · quic

flare.quic.state

QUIC connection + stream state machines (RFC 9000 §3 + §13).

Pure sans-I/O state model for a QUIC connection. The reactor wrapping wraps this in UDP I/O + crypto; the codec layer here just walks the state machines as packets / frames / timer ticks arrive.

Public surface:

  • :class:StreamState -- the 7-state RFC 9000 §3.1 / §3.2 stream state machine (IDLE / OPEN / HALF_CLOSED_LOCAL / HALF_CLOSED_ REMOTE / RESET_SENT / RESET_RECVD / CLOSED).
  • :class:Stream -- one bidi or uni stream's state, including send/recv high-water-marks for flow control.
  • :class:ConnectionState -- the connection-level state (HANDSHAKE / ESTABLISHED / CLOSING / DRAINING / CLOSED) per RFC 9000 §10.
  • :class:Connection -- top-level container holding
    • per-side keying state (handshake_complete flag),
    • flow-control limits,
    • per-stream :class:Stream instances keyed by stream-id,
    • the timer-driven idle / pto / loss-detection deadlines.
  • :func:handle_frame_buf -- per-buffer ingestion entry point. Reads one wire frame from the start of buf, dispatches the matching :trait:FrameHandler callback into the connection state machines, and returns the number of bytes consumed. The caller drains a packet payload by advancing its cursor and re-invoking the dispatcher.
  • :class:ConnectionEvents -- the value the driver returns from one tick: bytes the caller should send, the next deadline at which to call back, plus connection-level events (handshake done, stream finished, connection closed).
  • :func:apply_stream / :func:apply_ack / etc. -- per-typed payload helpers the dispatcher calls; exposed so the reactor layer (and tests) can drive transitions directly when they already have a typed payload in hand.

The state-machine layer is intentionally sans-I/O: there are no sockets, no TLS handshake, no time source. The caller passes now_us into every entry point and receives :class:ConnectionEvents carrying the next deadline; the timer shape lives in the reactor wrapper.

References:

  • RFC 9000 §3 "Stream States" + §10 "Connection Termination".
  • RFC 9000 §13 "Packetization and Reliability".

Structs

Connection

struct Connection

Top-level connection state.

Holds the connection-level state, the per-stream map, and the timer-shape fields the driver uses to schedule the next tick. The connection here only tracks the data-plane state machines.

Uses :class:Dict for the stream map so the driver can look up by stream id in O(1); RFC 9000 caps stream ids at 2**62-1 so the key space is wide enough that stream-id overflow is a non-concern.

Implements Copyable, Movable

Fields
state: Int
handshake_complete: Bool
idle_timeout_us: UInt64
last_activity_us: UInt64
max_data_send: UInt64
max_data_recv: UInt64
bytes_in_flight: UInt64
streams: Dict[UInt64, Stream]
ack_pending: Bool
largest_received_packet: UInt64
largest_acked_by_peer: UInt64
close_error_code: UInt64
close_reason: List[UInt8]
peer_cids: Dict[UInt64, _PeerConnId]: Peer-issued Source CIDs (RFC 9000 §5.1.1) keyed by sequence number, learned from NEW_CONNECTION_ID. The migration path picks a spare entry as the next active Destination CID.
active_dcid_seq: UInt64: Sequence number of the peer CID currently used as the active Destination CID. 0 is the handshake CID (§5.1.1).
peer_retire_prior_to: UInt64: Highest retire_prior_to the peer has sent; every sequence number below it is retired for good.
outgoing_path_challenge: List[UInt8]: The 8-byte PATH_CHALLENGE payload we last sent while probing a new path; an inbound PATH_RESPONSE must echo it to validate the path (§8.2). Empty when no probe is outstanding.
path_validated: Bool: True once the outstanding path probe was confirmed by a matching PATH_RESPONSE.
is_server: Bool: True for the server role (set by :class:QuicConnection). A server treats a received HANDSHAKE_DONE as a PROTOCOL_VIOLATION (RFC 9000 §19.20).
adv_max_streams_bidi: UInt64: Peer-initiated bidirectional streams we have allowed (initial_max_streams_bidi plus any MAX_STREAMS raise); a stream id above it is a STREAM_LIMIT_ERROR (RFC 9000 §4.6).
adv_max_streams_uni: UInt64: The same for peer-initiated unidirectional streams.
check_stream_ids: Bool: Whether :func:check_stream_frame_id runs on received stream frames. Set by the server (:class:QuicConnection) and the client (:class:QuicClientConnection); off for a bare sans-I/O connection, which has no notion of which streams its driver opened.
next_local_bidi: UInt64: The next locally initiated bidirectional stream id that has not been opened (client 0, 4, ...; server 1, 5, ...). The client advances it in open_bidi_stream; the server opens none.
next_local_uni: UInt64: The same for unidirectional streams (client 2, 6, ...; server 3, ...).
peer_initial_max_stream_data: UInt64: Send limit a peer-initiated stream starts with: the peer's initial_max_stream_data_bidi_local (RFC 9000 sec 18.2). The server holds it at 0 until it has read the client's transport parameters, then raises it and every open stream's limit.
new_token: List[UInt8]: The latest NEW_TOKEN token the server sent (client side, RFC 9000 sec 8.1.3), for the Initial of a later connection to it.
ack_freq_set: Bool: The peer sent ACK_FREQUENCY (draft-ietf-quic-ack-frequency): ACKs follow its threshold and delay instead of going out with every burst of packets.
ack_freq_seq: UInt64: The lowest ACK_FREQUENCY sequence number still applied.
ack_threshold: UInt64
ack_max_delay_us: UInt64
ack_immediate: Bool
ack_unacked: Int: Ack-eliciting packets received since the last ACK was sent.
ack_first_us: UInt64
max_datagram_recv: UInt64: The max_datagram_frame_size we advertised (RFC 9221 sec 3); 0 when we did not, and then any DATAGRAM frame is a PROTOCOL_VIOLATION, as is one larger than this.

Functions

note_ack_eliciting

def note_ack_eliciting(mut conn: Connection, now_us: UInt64)

An ack-eliciting packet was received.

ack_due

def ack_due(conn: Connection, now_us: UInt64) -> Bool

Whether an owed ACK goes out now: always, unless the peer asked for fewer with ACK_FREQUENCY.

note_ack_sent

def note_ack_sent(mut conn: Connection)

new_connection

def new_connection(idle_timeout_us: UInt64 = UInt64(30000000), initial_max_data: UInt64 = UInt64(1048576), is_server: Bool = False, adv_max_streams_bidi: UInt64 = UInt64(100), adv_max_streams_uni: UInt64 = UInt64(3), check_stream_ids: Bool = False) -> Connection

Build a fresh :class:Connection in the HANDSHAKE state.

mark_local_stream_opened

def mark_local_stream_opened(mut conn: Connection, sid: UInt64)

Record that we opened local stream sid by sending on it outside conn.streams (the server's HTTP/3 control stream), so :func:check_stream_frame_id accepts the peer's STOP_SENDING / MAX_STREAM_DATA for it (RFC 9000 sec 19.10).

check_stream_frame_id

def check_stream_frame_id(mut conn: Connection, sid: UInt64, kind: Int)

RFC 9000 §4.6 / §19.4 / §19.5 / §19.8 / §19.10 / §19.13: check the stream id of a received stream frame (kind is one of STREAM_FRAME_*).

A peer-initiated stream above the stream limit we advertised is a STREAM_LIMIT_ERROR; a frame for a half the stream does not have (a send-only stream the peer initiated, a stream we initiated that is receive-only) is a STREAM_STATE_ERROR, and so is STOP_SENDING / MAX_STREAM_DATA / STREAM for a stream we initiated and have not opened. Closes the connection and raises on a violation.

apply_stream

def apply_stream(mut conn: Connection, sf: StreamFrame, mut events: ConnectionEvents)

Apply a STREAM frame (§19.8) to the connection.

Creates the stream on first sight, advances the recv offset high-water-mark, enforces the flow-control limit, and emits a finished_streams entry when the FIN bit closes the recv side. Raises on flow-control violation. The full :class:StreamFrame is also appended to :attr:ConnectionEvents.stream_chunks so the H3 reactor wrapper can route the payload bytes to its per-connection :class:flare.http3.Http3Connection without re-parsing the QUIC packet.

apply_ack

def apply_ack(mut conn: Connection, ack: AckFrame)

Apply an ACK frame to the connection-level bookkeeping.

largest_acknowledged names the largest packet WE sent that the peer received; it lives in its own packet-number space and must NOT touch largest_received_packet (the largest packet WE received, which seeds inbound pn reconstruction).

apply_connection_close

def apply_connection_close(mut conn: Connection, cc: ConnectionCloseFrame, mut events: ConnectionEvents)

Apply a CONNECTION_CLOSE frame to the connection. Marks the connection as DRAINING and stores the close reason.

apply_max_data

def apply_max_data(mut conn: Connection, m: MaxDataFrame)

Apply a MAX_DATA frame: monotonically advance the connection's send-side flow-control limit.

apply_max_stream_data

def apply_max_stream_data(mut conn: Connection, m: MaxStreamDataFrame)

Apply a MAX_STREAM_DATA frame: advance the per-stream send high-water-mark for the addressed stream (no-op if the stream has not been seen yet).

apply_reset_stream

def apply_reset_stream(mut conn: Connection, rs: ResetStreamFrame)

Apply a RESET_STREAM frame: transition the stream to RESET_RECVD and record the receive-half reset in peer_reset (no-op if the stream is unknown).

apply_stop_sending

def apply_stop_sending(mut conn: Connection, ss: StopSendingFrame)

Apply a STOP_SENDING frame: transition the stream to RESET_SENT and record the send-half reset in send_reset (no-op if the stream is unknown).

apply_handshake_done

def apply_handshake_done(mut conn: Connection, mut events: ConnectionEvents)

Apply a HANDSHAKE_DONE frame: flip the connection to ESTABLISHED and surface the event.

Acts only in HANDSHAKE (as :func:mark_handshake_complete does). CLOSING, DRAINING and CLOSED only lead to closed (RFC 9000 §10.2), so a HANDSHAKE_DONE that arrives after CONNECTION_CLOSE must not make the connection usable again; an ESTABLISHED connection has nothing left to change.

apply_new_connection_id

def apply_new_connection_id(mut conn: Connection, ncid: NewConnectionIdFrame, mut events: ConnectionEvents)

Apply a NEW_CONNECTION_ID frame (RFC 9000 §19.15).

Records the peer's spare Source CID + stateless-reset token in :attr:Connection.peer_cids, then honors retire_prior_to: every stored CID with a lower sequence number is dropped and a RETIRE_CONNECTION_ID is queued in :attr:ConnectionEvents.retire_connection_ids (§5.1.2). If the active DCID is among those retired, the active sequence advances to this freshly issued CID.

Validates the wire fields at this trust boundary: CID length in [1, 20], a 16-byte reset token, and retire_prior_to no greater than sequence_number (a PROTOCOL_VIOLATION per §19.15 otherwise).

apply_retire_connection_id

def apply_retire_connection_id(mut conn: Connection, rcid: RetireConnectionIdFrame, mut events: ConnectionEvents)

Apply a RETIRE_CONNECTION_ID frame (RFC 9000 §19.16): the peer is retiring one of the CIDs WE issued. The driver owns SCID issuance, so the sequence number is surfaced in :attr:ConnectionEvents.retired_local_cids for it to unroute, validate against what it issued, and replace.

apply_path_challenge

def apply_path_challenge(mut conn: Connection, pc: PathChallengeFrame, mut events: ConnectionEvents)

Apply a PATH_CHALLENGE frame (RFC 9000 §19.17): queue a PATH_RESPONSE echoing the exact 8-byte payload so the peer can validate the path (§8.2). Validates the fixed 8-byte length at this trust boundary.

apply_path_response

def apply_path_response(mut conn: Connection, pr: PathResponseFrame, mut events: ConnectionEvents)

Apply a PATH_RESPONSE frame (RFC 9000 §19.18): if it echoes our outstanding PATH_CHALLENGE byte-for-byte, the probed path is validated -- clear the probe and surface path_validated so the driver can promote the new path (§8.2). A non-matching response is ignored (a stale or spoofed echo). Validates the fixed 8-byte length at this trust boundary.

handle_frame_buf

def handle_frame_buf(mut conn: Connection, buf: Span[UInt8], now_us: UInt64, mut events: ConnectionEvents) -> Int

Apply one wire frame from the start of buf to the connection state machines.

Returns the number of bytes the dispatcher consumed; the caller drains the rest of a packet payload by advancing its cursor and re-invoking the dispatcher on the remainder. Internally builds a small adapter implementing :trait:flare.quic.frame.FrameHandler and delegates to :func:flare.quic.frame.parse_frame_into.

dispatch_frames

def dispatch_frames(mut conn: Connection, payload: Span[UInt8], now_us: UInt64, mut events: ConnectionEvents, before_1rtt: Bool)

Apply every frame in a decrypted packet payload.

before_1rtt is set for Initial and Handshake packets. A frame those levels do not permit is a PROTOCOL_VIOLATION connection error (RFC 9000 sec 12.4): the connection is closed and this raises, so the caller drops the packet. The Initial keys come from the public DCID, so without this rule anyone who saw a client's first packet could send STREAM frames at Initial and have the server run them as a request, no handshake needed.

mark_handshake_complete

def mark_handshake_complete(mut conn: Connection, now_us: UInt64, mut events: ConnectionEvents)

Explicit hook the TLS handshake adapter calls when its key schedule reaches HANDSHAKE_DONE. Mirrors the :data:FRAME_TYPE_HANDSHAKE_DONE path so the connection state advances regardless of which side the signal came from.

is_idle_timeout_expired

def is_idle_timeout_expired(conn: Connection, now_us: UInt64) -> Bool

Whether the idle timeout has elapsed since last_activity_us.

Returns False if no traffic has been observed yet (the handshake hasn't started); the reactor uses this to retire stalled connections per RFC 9000 §10.1.

connection_close

def connection_close(mut conn: Connection, error_code: UInt64, reason: String, application: Bool = False)

Mark the connection as closing with the given error code + reason. The driver later emits a CONNECTION_CLOSE frame and transitions to DRAINING.

Aliases

comptime CONN_STATE_HANDSHAKE = Int(0)
comptime CONN_STATE_ESTABLISHED = Int(1)
comptime CONN_STATE_CLOSING = Int(2)
comptime CONN_STATE_DRAINING = Int(3)
comptime CONN_STATE_CLOSED = Int(4)
comptime MIN_ACK_DELAY_US = UInt64(1000)

The min_ack_delay both ends advertise (draft-ietf-quic-ack- frequency): a requested ACK delay below it is refused.

comptime MAX_QUEUED_DATAGRAM_BYTES = Int(65536)

Most DATAGRAM payload bytes a connection queues in either direction (RFC 9221 sec 5: unreliable, so past it a datagram is dropped rather than letting a peer or a slow application pin memory).

comptime STREAM_FRAME_RECV_HALF = Int(0)

RESET_STREAM / STREAM_DATA_BLOCKED: the frame concerns the sender's half, our receiving half.

comptime STREAM_FRAME_SEND_HALF = Int(1)

STOP_SENDING / MAX_STREAM_DATA: the frame concerns our sending half.

comptime STREAM_FRAME_DATA = Int(2)

STREAM: data for our receiving half.

comptime QUIC_PROTOCOL_VIOLATION = UInt64(10)

RFC 9000 sec 20.1 transport error code.

comptime QUIC_CONNECTION_ID_LIMIT_ERROR = UInt64(9)
comptime QUIC_FLOW_CONTROL_ERROR = UInt64(3)
comptime QUIC_STREAM_LIMIT_ERROR = UInt64(4)
comptime QUIC_STREAM_STATE_ERROR = UInt64(5)
comptime LOCAL_ACTIVE_CONNECTION_ID_LIMIT = Int(2)

The active_connection_id_limit both flare endpoints advertise.