// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// A client connection (RFC 9000 §7, §10; RFC 9001 §4, §6): the counterpart of `Server`. It
// drives moontls's QUIC client, splits the ClientHello across as many Initial packets as it
// needs, seals them with the Initial keys the connection ID alone yields, reassembles the
// server's flights per encryption level, and installs the keys each flight brings. It holds no
// socket and no clock: `poll_out`, `recv` and `tick` all take the time from the caller.
//
// Everything that can make a client stop making progress lives here: the probe timeout on the
// handshake's own CRYPTO streams, the anti-deadlock probe a client owes a server whose
// amplification budget is spent, Retry, version negotiation, and the closing and draining
// periods.
///|
/// The largest path id the client's own transport parameters offer, or 0 when they offer none.
/// A client declares `initial_max_path_id` in the block its caller passes, so the connection reads
/// its own declaration back rather than being told twice.
fn declared_paths(params : Bytes) -> UInt64 {
match decode_opt(params[:]) {
Some(block) =>
match find(block[:], InitialMaxPathId) {
Some(p) => p.as_int().unwrap_or(0UL)
None => 0UL
}
None => 0UL
}
}
///|
/// The transport parameters to declare, with `version_information` added (RFC 9368 §3). A caller
/// hands this connection an encoded block; the version it chose is this connection's to say, so
/// the parameter is appended here rather than left to every caller to get right. A block that
/// does not decode is left alone: the peer refuses it, which is the same answer a wrong block
/// would have earned anyway.
fn with_version_information(
params : Bytes,
chosen : UInt32,
offered : ArrayView[UInt32],
) -> Bytes {
match decode_opt(params[:]) {
Some(block) => encode([..block, version_information(chosen, offered)])
None => params
}
}
///|
/// The frames one CRYPTO stream run may be split into, and what a packet spends on everything
/// but frames. An Initial packet's room is computed from the header that will actually carry it
/// rather than assumed: a Retry token makes the header longer, and a fixed room would then build
/// a packet past the 1200-byte floor RFC 9000 §14.1 sets, which is a packet a server drops.
fn crypto_room(
dcid : Bytes,
scid : Bytes,
token : Bytes,
handshake : Bool,
) -> Int32 {
let token_field = if handshake {
0
} else {
// The token's length varint (two bytes covers any token this library issues) and the token.
2 + token.length()
}
let header = 1 +
4 +
1 +
dcid.length() +
1 +
scid.length() +
token_field +
2 +
4 +
16
let frame = 1 + 8 + 2
let room = min_datagram - header - frame
if room < 64 {
64
} else {
room
}
}
///|
/// A client connection mid-handshake.
pub struct Client {
/// What the handshake is built from, kept so a version negotiated after a Version Negotiation
/// packet can start a new ClientHello that names the version it settled on (RFC 9368 §2.1).
priv config : @tls.Client
/// The caller's transport parameters, before this connection's own are added: a restarted
/// first flight declares them again, under the new version.
priv declared : Bytes
priv rand : (Int32) -> Bytes
mut tls : @tls.Conn
/// The id the first Initial was addressed to, which a Retry's tag is bound to (§17.2.5.2).
original_dcid : Bytes
scid : Bytes
mut dcid : Bytes
/// The id the server's first authenticated packet named, which every packet after it must
/// come from (§7.2).
mut server_scid : Bytes
/// The token a Retry handed back, carried on every Initial from then on.
mut token : Bytes
/// The id a Retry named, which `retry_source_connection_id` has to match (§7.3).
mut retry_scid : Bytes?
mut retried : Bool
/// The versions a Version Negotiation packet offered, empty when none arrived (§6.2).
mut versions : Array[UInt32]
/// The versions this client offered, in preference order (RFC 9368 §3).
offered : Array[UInt32]
/// The version this client's packets carry: the first it offered, or the one a Version
/// Negotiation packet left it with (RFC 9000 §6.2).
mut version : UInt32
/// The version the first flight went out in. A peer that negotiated compatibly still accepts
/// Initial packets under it, so its keys are kept (RFC 9368 §2.3).
mut sent_version : UInt32
/// The version's constants — salt, labels, Retry key (RFC 9369 §3.3).
mut constants : @crypto.Version
/// The connection id the Initial keys derive from, which a Retry replaces (§17.2.5.2).
mut salt_dcid : Bytes
/// Whether a Version Negotiation packet has been acted on, which RFC 9368 §8 needs when the
/// server's parameters carry no version_information.
mut negotiated : Bool
/// The Initial keys of the version the first flight used, while a negotiated version differs.
mut initial_rx_other : @crypto.Keys?
mut initial_tx : @crypto.Keys
mut initial_rx : @crypto.Keys
mut hs_tx : @crypto.Keys?
mut hs_rx : @crypto.Keys?
mut app_tx : @crypto.Keys?
mut app_rx : @crypto.Keys?
mut suite : @crypto.Suite
core : Conn
/// The handshake's own CRYPTO streams, with what is in flight on each.
priv initial_out : Outbox
priv handshake_out : Outbox
/// The next offset to write application-level CRYPTO at, which is a server's tickets.
mut app_crypto_at : UInt64
/// Initial keys go when the first Handshake packet does, Handshake keys on HANDSHAKE_DONE
/// (RFC 9001 §4.9).
mut initial_gone : Bool
mut handshake_gone : Bool
mut seen_server_cid : Bool
/// 1-RTT and 0-RTT: the send loop, the receiving streams, and what the application is told.
sender : Sender
streams : @stream.Streams
events : Array[Event]
datagrams : Array[Bytes]
mut key_phase : Bool
mut rx_phase : Bool
mut app_tx_secret : Bytes
mut app_rx_secret : Bytes
mut phase_start : Int64
mut confirmed : Bool
mut next_uni : UInt64
/// The early keys a ticket brought, and whether the server took the data sent under them.
mut zero_rtt_tx : @crypto.Keys?
mut zero_sent : Bool
mut zero_rejected : Bool
mut reset_token : Bytes?
offered_ids : Array[(UInt64, Bytes, Bytes)]
mut dcid_seq : UInt64
reset_tokens : Array[Bytes]
mut phase : Phase
mut close_at : Int64
mut close_frame : @frame.Frame?
mut close_level : Level
mut last_rx : Int64
mut last_tx : Int64
mut idle_timeout : Int64
/// The time an entry point was last called at, so a close raised while reading the handshake
/// can date its closing period.
mut at : Int64
cfg : Config
/// Probes the timers have decided to send, waiting for the next `poll_out`.
priv probes : Array[Bytes]
/// The name the ticket is filed under, and the store it is filed in, so this connection can
/// remember what the next one needs to resume (RFC 9000 §7.4.1).
host : String
sessions : @tls.Sessions?
wall : Int64
/// The server's transport parameters exactly as they arrived, which is half of what is kept.
mut peer_params : Bytes
/// This connection's paths beyond the one it started on (draft-ietf-quic-multipath §3). A
/// client holds no addresses — its caller owns the sockets — so a path is a number here, and
/// `recv_on` says which one a datagram came in on.
priv paths : Paths
priv lanes : Map[UInt64, Lane[Unit]]
/// Datagrams for the further paths, each tagged with the path it belongs on.
priv lane_out : Array[(UInt64, Bytes)]
}
///|
/// A client for `server_name`, with `config` holding its roots and ALPN protocols. `dcid` is
/// the connection id the Initial keys derive from (RFC 9001 §5.2) and `scid` the one this
/// client calls itself; both are the caller's, so a test can make them reproducible.
pub fn Client::new(
config : @tls.Client,
server_name : String,
params : Bytes,
rand : (Int32) -> Bytes,
dcid : Bytes,
scid : Bytes,
wall : Int64,
cfg~ : Config = Config::default(),
resumption? : Remembered,
) -> Client raise {
let version = match cfg.versions {
[first, ..] => first
[] => @packet.version1
}
let constants = @crypto.Version::of(version).unwrap()
// RFC 9368 §3: every first flight says which version it chose and which ones it would accept,
// so the answer can be held to it without another round trip.
let declared = params
let params = with_version_information(declared, version, cfg.versions[:])
let tls = @tls.Conn::quic_client(
config,
server_name,
params~,
rand~,
now=wall,
)
let (tx, rx) = @crypto.initial(dcid[:], version=constants)
let core = Conn::new(false)
// What early data may spend: the ticket's own parameters, unless Config overrules them
// (RFC 9000 §7.4.1). A client with no ticket and no overrides has no credit, so early data
// waits for the handshake instead of risking a flow-control error.
let (early_data, early_bidi, early_uni, early_datagram) = early_credit(
cfg, resumption,
)
let sender = Sender::new(
early_data,
if early_bidi < early_uni {
early_bidi
} else {
early_uni
},
cfg.max_datagram,
cfg.max_ack_delay,
cfg.granularity,
cfg.max_frame,
)
sender.sched.on_params(data=early_data, bidi=early_bidi, uni=early_uni)
sender.on_peer_datagram(early_datagram)
let client : Client = {
config,
declared,
rand,
tls,
original_dcid: dcid,
scid,
dcid,
server_scid: b"",
token: b"",
retry_scid: None,
retried: false,
versions: [],
offered: cfg.versions.copy(),
version,
sent_version: version,
constants,
salt_dcid: dcid,
negotiated: false,
initial_rx_other: None,
initial_tx: tx,
initial_rx: rx,
hs_tx: None,
hs_rx: None,
app_tx: None,
app_rx: None,
suite: @crypto.suite,
core,
initial_out: Outbox::new(),
handshake_out: Outbox::new(),
app_crypto_at: 0UL,
initial_gone: false,
handshake_gone: false,
seen_server_cid: false,
sender,
streams: @stream.Streams::new(
cfg.initial_max_data,
cfg.initial_max_stream_data,
server=false,
bidi=@stream.Quota::new(cfg.streams_bidi),
uni=@stream.Quota::new(cfg.streams_uni),
),
events: [],
datagrams: [],
key_phase: false,
rx_phase: false,
app_tx_secret: b"",
app_rx_secret: b"",
phase_start: 0L,
confirmed: false,
next_uni: 0UL,
zero_rtt_tx: None,
zero_sent: false,
zero_rejected: false,
reset_token: None,
offered_ids: [],
dcid_seq: 0UL,
reset_tokens: [],
phase: Active,
close_at: 0L,
close_frame: None,
close_level: Level::Initial,
last_rx: 0L,
last_tx: 0L,
idle_timeout: cfg.idle_timeout,
at: 0L,
cfg,
probes: [],
host: server_name,
sessions: config.sessions,
wall,
peer_params: b"",
paths: Paths::new(local_max=declared_paths(params)),
lanes: Map([
(
0UL,
Lane::new(
0UL,
(),
core.space(Application),
sender.recovery,
cfg.max_datagram,
),
),
]),
lane_out: [],
}
match resumption {
Some(session) => client.adopt_ticket(session)
None => ()
}
client.absorb()
client
}
///|
/// Do what the handshake asks: install the keys it derives now, and queue the bytes it wants
/// sent with the level they belong at.
fn Client::absorb(self : Client) -> Unit {
for step in self.tls.steps() {
match step {
Send(level, data) =>
match level {
Initial => self.initial_out.write(data)
Handshake => self.handshake_out.write(data)
Application => self.queue_app_crypto(data)
}
Early(secret~, suite~) =>
self.zero_rtt_tx = Some(
@crypto.zero_rtt(
secret[:],
suite=suite_of(suite),
version=self.constants,
),
)
Keys(level, client~, server~) => {
if self.tls.suite() is Some(s) {
self.suite = suite_of(s)
}
// A client writes under the client secret and reads under the server's.
let tx = @crypto.Keys::of(
client[:],
suite=self.suite,
version=self.constants,
)
let rx = @crypto.Keys::of(
server[:],
suite=self.suite,
version=self.constants,
)
match level {
Handshake => {
self.hs_tx = Some(tx)
self.hs_rx = Some(rx)
}
Application => {
self.app_tx = Some(tx)
self.app_rx = Some(rx)
self.app_tx_secret = client
self.app_rx_secret = server
}
Initial => ()
}
}
}
}
self.adopt_peer_params()
self.settle_zero_rtt()
}
///|
/// Queue post-handshake TLS bytes — a session ticket — as 1-RTT CRYPTO frames, so a lost one is
/// sent again (RFC 9001 §4.1.3).
fn Client::queue_app_crypto(self : Client, data : Bytes) -> Unit {
let mut at = 0
while at < data.length() {
let end = if data.length() - at > app_crypto_chunk {
at + app_crypto_chunk
} else {
data.length()
}
self.sender.queue_control(
Crypto(offset=self.app_crypto_at, data=data[at:end].to_owned()),
)
self.app_crypto_at = self.app_crypto_at + (end - at).to_uint64()
at = end
}
}
///|
/// Take the server's transport parameters once EncryptedExtensions has brought them: the
/// flow-control credit that opens the send loop, the largest datagram it will take, and the
/// three connection ids RFC 9000 §7.3 holds the handshake to.
fn Client::adopt_peer_params(self : Client) -> Unit {
guard self.tls.peer_params() is Some(raw) else { return }
self.peer_params = raw
self.reset_token = find(decode(raw[:]), StatelessResetToken).map(p => p.value) catch {
_ => None
}
guard decode_opt(raw[:]) is Some(params) else {
return self.close(transport_parameter_error, b"", self.at)
}
match params_fault(params[:], server=false) {
Some(fault) => return self.close(fault.code, fault.reason, self.at)
None => ()
}
self.check_version_information(params[:])
// §7.3: the server confirms the id the client invented, the id a Retry named, and the id it
// chose for itself. A handshake that does not add up is not this connection.
if !same_param(params, OriginalDcid, self.original_dcid) {
return self.close(transport_parameter_error, b"", self.at)
}
match self.retry_scid {
Some(scid) =>
if !same_param(params, RetryScid, scid) {
return self.close(transport_parameter_error, b"", self.at)
}
None =>
if find(params[:], RetryScid) is Some(_) {
return self.close(transport_parameter_error, b"", self.at)
}
}
if !same_param(params, InitialScid, self.server_scid) {
return self.close(transport_parameter_error, b"", self.at)
}
let peer = Limits::read(params[:])
// A stream this client opens is "remote" from the server's side of the ledger.
self.sender.sched.on_params(
data=peer.data,
bidi=peer.stream_data_bidi_remote,
uni=peer.stream_data_uni,
)
self.sender.on_max_data(peer.data)
self.sender.on_peer_datagram(peer.datagram_frame)
// draft-ietf-quic-multipath §2.1: the server's initial_max_path_id is a MAX_PATH_ID, and this
// client may open a path up to it.
match find(params[:], InitialMaxPathId) {
Some(p) =>
match p.as_int() {
Some(max) => self.paths.on_max_path_id(max)
None => ()
}
None => ()
}
self.sender.ack_exponent = peer.ack_delay_exponent.to_int()
if find(params[:], MaxIdleTimeout) is Some(_) {
self.idle_timeout = idle_timeout(
self.cfg.idle_timeout,
peer.idle_timeout.reinterpret_as_int64() * 1000L,
)
}
}
///|
/// Hold the server's `version_information` to the version this handshake settled on (RFC 9368
/// §4): the chosen version must be the negotiated one, and the list must be one this client would
/// have picked the same version out of. The packet that carries it is authenticated, so a middlebox
/// cannot rewrite it into a version this client did not choose.
///
/// A peer that sends no parameter at all is let through: implementations that speak version 2
/// without RFC 9368 are still about, and §8 reads a missing parameter as the version 1 deployment
/// a client that answered a Version Negotiation packet would have been talking to. Nothing is lost
/// by it — RFC 9369 §8 gives the two versions the same security properties, so a fall from one to
/// the other is not a downgrade in strength, only in name.
fn Client::check_version_information(
self : Client,
params : ArrayView[Param],
) -> Unit {
let at = self.at
match find(params, VersionInformation) {
Some(p) =>
match read_version_information(p.value[:]) {
Some((chosen, available)) => {
if chosen != self.version {
return self.close(version_negotiation_error, b"chosen version", at)
}
// §4's second test is what a Version Negotiation packet is held to: the version this
// client acted on has to be the one it would have chosen from the list the server
// publishes now, or a middlebox rewrote the list on the way. A connection that
// negotiated compatibly never saw a Version Negotiation packet, so there is nothing for
// that test to catch — the peer picked from what the handshake itself carried.
guard self.negotiated else { return }
if available.length() == 0 {
return self.close(version_negotiation_error, b"no versions", at)
}
let offered = available.copy()
offered.push(self.version)
match self.would_pick(offered[:]) {
Some(pick) =>
if pick != self.version {
return self.close(
version_negotiation_error, b"a version this client would not have chosen",
at,
)
}
None => ()
}
}
None => return self.close(version_negotiation_error, b"malformed", at)
}
None => ()
}
}
///|
/// The version this client would choose from `offered`, by its own order of preference.
fn Client::would_pick(self : Client, offered : ArrayView[UInt32]) -> UInt32? {
for version in self.offered {
if offered.contains(version) {
return Some(version)
}
}
None
}
///|
/// Whether the parameter `kind` is present and worth `want`.
fn same_param(params : Array[Param], kind : Kind, want : Bytes) -> Bool {
match find(params[:], kind) {
Some(p) => p.value == want
None => false
}
}
///|
/// What becomes of the early data once the handshake has said (RFC 9000 §4.6.2). Taken, and the
/// early keys have done their work; refused, and everything still in flight has to go again
/// under the 1-RTT keys, which the send loop does once the contents are back in the queue.
fn Client::settle_zero_rtt(self : Client) -> Unit {
guard self.zero_sent && self.tls.is_connected() && !self.zero_rejected else {
return
}
if self.tls.early_accepted() {
self.sender.requeue_all()
} else {
self.zero_rejected = true
self.sender.requeue_all()
}
// RFC 9001 §4.9.3: the early keys are of no further use either way.
self.zero_rtt_tx = None
}
///|
/// The datagrams to put on the wire: the queued CRYPTO bytes split so no Initial packet runs
/// past a datagram, each Initial padded to the 1200-byte floor (RFC 9000 §14.1), the early data
/// the send loop has ready, then 1-RTT traffic once the handshake is done.
pub fn Client::poll_out(self : Client, now : Int64) -> Array[Bytes] {
self.at = now
let out : Array[Bytes] = []
match self.phase {
Closing => {
if now < self.close_at {
match self.close_frame {
Some(frame) => {
let pn = self.core.next_packet_number(self.close_level)
out.push(self.seal_level(self.close_level, pn, [frame]))
}
None => ()
}
}
return out
}
Draining => return out
Active => ()
}
if !self.initial_gone {
// A post-quantum hello is more than one datagram, and each Initial is its own.
while true {
match self.handshake_packet(now, Level::Initial) {
Some(packet) => out.push(packet)
None => break
}
}
}
if !self.handshake_gone && self.hs_tx is Some(_) {
while true {
match self.handshake_packet(now, Level::Handshake) {
Some(packet) => out.push(packet)
None => break
}
}
// RFC 9001 §4.9.1: the first Handshake packet is the last use of the Initial keys.
self.initial_gone = true
self.initial_out.clear()
}
for packet in self.probes {
out.push(packet)
}
self.probes.clear()
if !self.tls.is_connected() {
self.zero_rtt_packets(now, out)
}
if self.tls.is_connected() {
while true {
let space = self.core.space(Level::Application)
let ack = if space.ack_pending() { space.build_ack(0UL) } else { None }
let next = self.sender.poll_send(now) catch { _ => None }
match next {
Some((pn, frames)) => {
space.advance_past(pn)
let all = match ack {
Some(a) => [a, ..frames]
None => frames
}
out.push(self.seal_app(pn, all))
}
None => {
match ack {
Some(a) => {
// The send loop numbers this space too. A number used twice is a nonce used twice.
let pn = space.next_packet_number()
self.sender.advance_past(pn)
out.push(self.seal_app(pn, [a]))
}
None => ()
}
break
}
}
}
}
if out.length() > 0 {
self.last_tx = now
}
out
}
///|
/// One Initial or Handshake packet: the ACK owed at that level and one run of CRYPTO, sealed
/// with the token a Retry left. An Initial datagram is padded to the floor whatever it carries,
/// because a server may not answer a smaller one.
fn Client::handshake_packet(
self : Client,
now : Int64,
level : Level,
) -> Bytes? {
let space = self.core.space(level)
let frames : Array[@frame.Frame] = []
if space.ack_pending() {
if space.build_ack(0UL) is Some(ack) {
frames.push(ack)
}
}
let outbox = if level == Level::Initial {
self.initial_out
} else {
self.handshake_out
}
let room = crypto_room(
self.dcid,
self.scid,
self.token,
level == Level::Handshake,
)
let run = outbox.take(room)
if run is Some((at, size)) {
frames.push(
Crypto(offset=at.to_uint64(), data=outbox.bytes[at:at + size].to_owned()),
)
}
guard frames.length() > 0 else { return None }
let pn = self.core.next_packet_number(level)
if run is Some(sent) {
// What this packet carries goes out again when the probe timeout fires.
outbox.flight[pn] = sent
outbox.deadline = now + self.sender.recovery.pto()
}
Some(self.seal_level(level, pn, frames))
}
///|
/// A protected packet at `level` carrying `frames`. An Initial one is padded to the datagram
/// floor; a Handshake one is not, and a level whose keys are gone gives an empty packet.
fn Client::seal_level(
self : Client,
level : Level,
pn : Int64,
frames : Array[@frame.Frame],
) -> Bytes {
match level {
Initial => {
let payload = @packet.payload(frames)
let build = body => {
seal_long(
Initial,
self.version,
self.dcid,
self.scid,
pn,
4,
body,
self.initial_tx,
token=self.token,
)
}
pad(payload, build, min_datagram)
}
Handshake =>
match self.hs_tx {
Some(keys) =>
send_long(
Handshake,
self.version,
self.dcid,
self.scid,
pn,
4,
frames,
keys,
)
None => b""
}
Application =>
match self.app_tx {
Some(keys) =>
send_short(self.dcid, pn, 4, frames, keys, key_phase=self.key_phase)
None => b""
}
}
}
///|
/// The 0-RTT packets the send loop has ready. Early data is only usable before the handshake
/// finishes, and only while a ticket's keys are in hand.
fn Client::zero_rtt_packets(
self : Client,
now : Int64,
out : Array[Bytes],
) -> Unit {
guard self.zero_rtt_tx is Some(keys) && !self.zero_rejected else { return }
let space = self.core.space(Level::Application)
while true {
let next = self.sender.poll_send(now, zero=true) catch { _ => None }
match next {
Some((pn, frames)) => {
space.advance_past(pn)
out.push(
send_zero_rtt(
self.dcid,
self.scid,
pn,
4,
frames,
keys,
version=self.sent_version,
),
)
self.zero_sent = true
}
None => break
}
}
}
///|
/// A protected 1-RTT packet carrying `frames`.
fn Client::seal_app(
self : Client,
pn : Int64,
frames : Array[@frame.Frame],
) -> Bytes {
match self.app_tx {
Some(keys) =>
send_short(self.dcid, pn, 4, frames, keys, key_phase=self.key_phase)
None => b""
}
}
///|
/// The highest level this client can send at.
fn Client::send_level(self : Client) -> Level {
if self.app_tx is Some(_) {
Level::Application
} else if self.hs_tx is Some(_) {
Level::Handshake
} else {
Level::Initial
}
}
///|
/// Take a datagram from the server: split it into packets, unprotect each at the level its
/// header names, reassemble its CRYPTO stream, and drive the handshake with what becomes
/// contiguous. A server's first datagram may coalesce packets; the keys one brings are
/// installed before the next is read.
pub fn Client::recv(self : Client, datagram : Bytes, now : Int64) -> Unit raise {
self.recv_on(0UL, datagram, now)
}
///|
/// Take a datagram that arrived on path `path`. A client has no address of its own — its caller
/// owns the sockets — so the path a datagram belongs to is something only the caller knows, and
/// it says so here (draft-ietf-quic-multipath §2.3). The path picks the packet-number space and
/// the nonce the packet was sealed with.
pub fn Client::recv_on(
self : Client,
path : UInt64,
datagram : Bytes,
now : Int64,
) -> Unit raise {
self.at = now
if self.phase == Draining {
return
}
let mut at = 0
while at < datagram.length() {
let view = datagram[at:]
guard view.length() > 0 else { break }
if (view[0].to_int() & 0x80) == 0 {
// 1-RTT: every Application packet runs to the end of the datagram.
guard self.app_rx is Some(keys) else { break }
let space = self.core.space_on(path, Level::Application)
let opened = self.open_app(
view.to_owned(),
keys,
space.largest_received(),
path~,
) catch {
_ => return self.close(frame_encoding_error, b"", now)
}
guard opened is Some((frames, pn)) else {
// RFC 9000 §10.3.1: a packet that will not open may be the server saying it has forgotten
// this connection. Only a packet that fails to open is compared, and it ends the
// connection at once, since nothing more can come.
if self.known_reset(view) {
self.events.push(Closed(0UL))
}
break
}
if reserved_set(view.to_owned(), keys, 1 + self.scid.length(), short=true) {
return self.close(protocol_violation, b"reserved bits set", now)
}
match
frames_fault(
Level::Application,
frames,
self.standing(Level::Application, path~),
) {
Some(fault) => return self.close(fault.code, fault.reason, now)
None => ()
}
// A PATH_ACK is judged against the path it names, not the path its packet came in on.
match
path_ack_fault(frames, p => {
self.core.space_on(p, Level::Application).issued()
}) {
Some(fault) => return self.close(fault.code, fault.reason, now)
None => ()
}
space.on_packet_received(pn, @packet.payload_elicits_ack(frames))
self.on_app_frames(frames, now, path~)
break
}
guard @packet.read_long(view) is Some((h, _)) else { break }
if h.version == 0U {
self.on_versions(view, h)
break
}
if !self.accepts(h.version) {
// RFC 9000 §6: a version this client did not offer is not this connection's business.
break
}
if h.packet_type is Retry {
// A Retry is a datagram of its own (RFC 9000 §17.2.5).
self.on_retry(view.to_owned(), h)
break
}
guard @packet.read_header(view) is Some((_, tail)) else { break }
let size = tail.at + tail.length.to_int()
guard size <= view.length() else { break }
let packet = view[0:size].to_owned()
at = at + size
match h.packet_type {
Initial =>
match self.initial_rx_for(h.version) {
Some(keys) => self.on_long(Level::Initial, h, packet, keys, now)
None => ()
}
Handshake =>
match self.hs_rx {
Some(keys) => self.on_long(Level::Handshake, h, packet, keys, now)
None => ()
}
_ => ()
}
}
}
///|
/// Whether a long header's version is one this connection may read (RFC 9368 §2.3). The version
/// this client sends, and — until the handshake is confirmed — the one the first flight went out
/// in, since a peer that converted keeps answering Initial packets under it for a while.
fn Client::accepts(self : Client, version : UInt32) -> Bool {
version == self.version ||
(!self.confirmed && version == self.sent_version) ||
// A version this client offered, which a peer switched to without a Version Negotiation
// packet: the compatible negotiation of RFC 9368 §2.3.
(!self.confirmed && self.offered.contains(version))
}
///|
/// The keys a server's Initial packet under `version` opens with. The version this connection
/// speaks has its keys installed; another version this client offered yields them from the same
/// connection id, which is what a peer that converted sends under (RFC 9368 §2.3).
fn Client::initial_rx_for(self : Client, version : UInt32) -> @crypto.Keys? {
if version == self.version {
return Some(self.initial_rx)
}
if version == self.sent_version {
match self.initial_rx_other {
Some(keys) => return Some(keys)
None => ()
}
}
match @crypto.Version::of(version) {
Some(constants) =>
Some(@crypto.initial(self.salt_dcid[:], version=constants).1)
None => None
}
}
///|
/// What a frame check on this client runs against.
fn Client::standing(
self : Client,
level : Level,
path~ : UInt64 = 0UL,
) -> Standing {
{
server: false,
streams: self.streams,
sched: self.sender.sched,
sent: self.core.space_on(path, level).issued(),
}
}
///|
/// A Retry (RFC 9000 §17.2.5, RFC 9001 §5.8): check the tag over the id the first Initial used,
/// take the token, address the new id, derive the Initial keys again, and send the ClientHello
/// from the start — under packet numbers that carry on where they were. A second Retry is
/// ignored, and so is one whose tag does not add up.
fn Client::on_retry(self : Client, packet : Bytes, h : @packet.Long) -> Unit {
guard !self.retried && !self.seen_server_cid else { return }
guard @packet.read_retry(packet[:]) is Some((_, token, _)) else { return }
guard @crypto.retry_ok(packet[:], self.original_dcid[:]) else { return }
self.retried = true
self.token = token
self.dcid = h.scid
self.retry_scid = Some(h.scid)
self.server_scid = h.scid
self.seen_server_cid = true
// A Retry has to carry the version of the first flight it answers (RFC 9368 §7.1), so the keys
// that replace the old ones are that version's.
let constants = @crypto.Version::of(self.sent_version).unwrap()
let (tx, rx) = @crypto.initial(h.scid[:], version=constants)
self.salt_dcid = h.scid
self.initial_tx = tx
self.initial_rx = rx
// The ClientHello goes out again from offset zero; the packet numbers do not restart, since
// they belong to the space, not to the attempt.
self.initial_out.rewind()
}
///|
/// A Version Negotiation packet (RFC 9000 §6.2): the versions the server speaks. Only the first
/// packet of a connection may be one, and only addressed to this client's own id.
fn Client::on_versions(
self : Client,
packet : BytesView,
h : @packet.Long,
) -> Unit raise {
// §6.2: a Version Negotiation packet is discarded once anything else has been processed, and
// one that lists the version this client sent is not an answer to it.
guard !self.seen_server_cid && !self.negotiated else { return }
guard h.dcid == self.scid else { return }
match @packet.read_versions(packet[:]) {
Some((dcid, scid, offered)) => {
guard dcid == self.scid && scid == self.dcid else { return }
// The list is kept for the caller to read even when it is not acted on.
self.versions = offered
guard !offered.contains(self.version) else { return }
// RFC 9368 §2.1: the connection restarts in a version both ends speak. The search runs
// over this client's own preference order, so a server cannot talk it into a version it
// ranked lower than one it could have had.
match self.pick_version(offered[:]) {
Some(chosen) => self.restart(chosen)
None => ()
}
}
None => ()
}
}
///|
/// The version to answer a Version Negotiation packet with: the first of this client's own
/// preferences that the peer offers, and not the one already refused (RFC 9368 §2.1). `None`
/// when the two lists do not meet, which leaves the caller to abandon the attempt.
fn Client::pick_version(self : Client, offered : ArrayView[UInt32]) -> UInt32? {
for version in self.offered {
if offered.contains(version) && version != self.sent_version {
return Some(version)
}
}
None
}
///|
/// Start again in `version` after a Version Negotiation packet (RFC 9368 §2.1): the same
/// connection id, a first flight under that version's Initial keys, and the packet numbers
/// carrying on where they were. Early data goes: §7.3 in a
/// version it negotiated to.
fn Client::restart(self : Client, version : UInt32) -> Unit raise {
let constants = match @crypto.Version::of(version) {
Some(constants) => constants
None => return
}
// The first flight is a new one, not the old bytes under new keys: its version_information has
// to name the version it is sent in, and a peer holds the connection to that (RFC 9368 §4).
self.tls = @tls.Conn::quic_client(
self.config,
self.host,
params=with_version_information(self.declared, version, self.offered[:]),
rand=self.rand,
now=self.wall,
)
let (tx, rx) = @crypto.initial(self.salt_dcid[:], version=constants)
self.negotiated = true
self.version = version
self.sent_version = version
self.constants = constants
self.initial_tx = tx
self.initial_rx = rx
self.initial_rx_other = None
if self.zero_sent || self.zero_rtt_tx is Some(_) {
self.zero_rtt_tx = None
self.zero_rejected = true
self.sender.requeue_all()
}
// What the handshake wants sent is the new ClientHello and nothing else: the old bytes stay in
// the outbox unless it is emptied, and two hellos on one CRYPTO stream is a peer's
// unexpected_message.
self.initial_out.clear()
self.handshake_out.clear()
self.absorb()
}
///|
/// Take the version a peer answered in, when it is one this client offered and the handshake
/// still has room for it (RFC 9368 §2.3). Called only for a packet that opened, so an off-path
/// packet cannot move the connection to another version.
fn Client::adopt_version(self : Client, version : UInt32) -> Unit {
guard version != self.version && !self.confirmed else { return }
let constants = match @crypto.Version::of(version) {
Some(constants) => constants
None => return
}
// What the first flight went out under stays readable until the handshake is confirmed
// (RFC 9368 §2.3), and what this client sends from now on is the negotiated version.
self.initial_rx_other = Some(self.initial_rx)
self.initial_rx = @crypto.initial(self.salt_dcid[:], version=constants).1
self.initial_tx = @crypto.initial(self.salt_dcid[:], version=constants).0
self.version = version
self.constants = constants
if self.zero_rtt_tx is Some(_) {
// §4.1: 0-RTT is sent in the version the first flight used, so a connection that moved on
// cannot keep spending it.
self.zero_rtt_tx = None
self.zero_rejected = true
self.sender.requeue_all()
}
}
///|
/// One long-header packet at `level`, under `keys`: check it against what a peer may send,
/// record its number, reassemble its CRYPTO bytes, feed the contiguous run to the handshake, and
/// do what it asks. The server's chosen id is adopted only here, from a packet that
/// authenticated (RFC 9000 §7.2).
fn Client::on_long(
self : Client,
level : Level,
h : @packet.Long,
packet : Bytes,
keys : @crypto.Keys,
now : Int64,
) -> Unit {
let space = self.core.space(level)
let opened : (Array[@frame.Frame], Int64)? = recv_long(
packet,
keys,
largest=space.largest_received(),
) catch {
_ => return self.close(frame_encoding_error, b"", now)
}
guard opened is Some((frames, pn)) else { return }
// The packet authenticated under its own version's keys, so the version it names is the one the
// peer is answering in: this is where a compatible negotiation lands (RFC 9368 §2.3).
self.adopt_version(h.version)
if self.seen_server_cid && h.scid != self.server_scid {
// RFC 9000 §7.2: after the first authenticated packet, another source id is another
// connection's traffic.
return
}
let at = match @packet.read_header(packet[:]) {
Some((_, tail)) => tail.at
None => return
}
if reserved_set(packet, keys, at, short=false) {
return self.close(protocol_violation, b"reserved bits set", now)
}
guard !space.seen(pn) else { return }
match frames_fault(level, frames, self.standing(level)) {
Some(fault) => return self.close(fault.code, fault.reason, now)
None => ()
}
if !self.seen_server_cid {
self.dcid = h.scid
self.server_scid = h.scid
self.seen_server_cid = true
}
self.last_rx = now
self.core.on_packet_received(level, pn, @packet.payload_elicits_ack(frames))
let mut closing : UInt64? = None
let ready = Buffer()
for frame in frames {
match frame {
Crypto(offset~, data~) => {
let contiguous = self.core.on_crypto_frame(level, offset, data) catch {
_ => return self.close(crypto_buffer_exceeded, b"", now)
}
ready.write_bytes(contiguous)
}
ConnectionClose(error_code~, ..) => closing = Some(error_code)
_ => ()
}
}
if ready.length() > 0 {
self.tls.crypto(tls_level(level), ready.to_bytes()[:]) catch {
@alert.Alert(kind~, ..) =>
return self.close(
crypto_error + kind.code().to_uint64(),
b"TLS alert",
now,
)
}
}
self.absorb()
match closing {
Some(code) => {
self.phase = Draining
self.close_at = now + self.cfg.close_period
self.events.push(Closed(code))
}
None => ()
}
}
///|
/// A short-header packet's frames. One that will not open under the current keys may be the next
/// phase, and only a real packet opens under those (RFC 9001 §6.3). If the server moved first this
/// client follows it, since it may not keep writing the old phase once it has read the new one.
fn Client::open_app(
self : Client,
packet : Bytes,
keys : @crypto.Keys,
largest : Int64,
path~ : UInt64 = 0UL,
) -> (Array[@frame.Frame], Int64)? raise @packet.Refused {
let n = self.scid.length()
let opened = recv_short(packet, n, keys, largest~, path~)
guard opened is None && self.app_rx_secret.length() > 0 else { return opened }
let (secret, next) = roll(
self.app_rx_secret,
keys,
self.suite,
version=self.constants,
)
let moved = recv_short(packet, n, next, largest~, path~)
guard moved is Some(_) else { return None }
self.app_rx = Some(next)
self.app_rx_secret = secret
self.rx_phase = !self.rx_phase
if self.rx_phase != self.key_phase {
self.next_write_phase()
}
moved
}
///|
fn Client::next_write_phase(self : Client) -> Unit {
guard self.app_tx is Some(keys) else { return }
let (secret, next) = roll(
self.app_tx_secret,
keys,
self.suite,
version=self.constants,
)
self.app_tx = Some(next)
self.app_tx_secret = secret
self.key_phase = !self.key_phase
self.phase_start = self.sender.next_pn
}
///|
/// Whether a packet that would not open ends in a token one of this server's ids came with.
fn Client::known_reset(self : Client, packet : BytesView) -> Bool {
(self.reset_token is Some(token) && is_stateless_reset(packet, token[:])) ||
self.reset_tokens.iter().any(token => is_stateless_reset(packet, token[:]))
}
///|
/// An id from the server, for when this client wants a new one (RFC 9000 §5.1.2). Anything below
/// `retire_prior_to` has to go, the one in use with it.
fn Client::on_new_id(
self : Client,
seq : UInt64,
retire_prior_to : UInt64,
cid : Bytes,
token : Bytes,
) -> Unit {
if self.offered_ids.iter().all(entry => entry.0 != seq) && seq > self.dcid_seq {
self.offered_ids.push((seq, cid, token))
self.reset_tokens.push(token)
}
let kept = []
for entry in self.offered_ids {
if entry.0 < retire_prior_to {
self.sender.queue_control(RetireConnectionId(entry.0))
} else {
kept.push(entry)
}
}
self.offered_ids.clear()
self.offered_ids.append(kept)
if self.dcid_seq < retire_prior_to {
ignore(self.next_cid())
}
}
///|
/// Start addressing the server by the next id it offered, and retire the one in use. Do this before
/// sending from a new address on purpose, so the two paths cannot be linked (RFC 9000 §9.5). False
/// when it has offered none.
pub fn Client::next_cid(self : Client) -> Bool {
guard self.offered_ids.length() > 0 else { return false }
let (seq, cid, _) = self.offered_ids.remove(0)
self.sender.queue_control(RetireConnectionId(self.dcid_seq))
self.dcid_seq = seq
self.dcid = cid
true
}
///|
/// Move to the next key phase (RFC 9001 §6). The answer is false when it is not allowed yet: before
/// the handshake is confirmed, or before something written in the current phase has been
/// acknowledged, which is what stops an endpoint outrunning its peer's two sets of keys (§6.1, §6.2).
pub fn Client::update_keys(self : Client) -> Bool {
guard self.confirmed &&
self.rx_phase == self.key_phase &&
self.sender.recovery.flight.largest_acked() >= self.phase_start else {
return false
}
self.next_write_phase()
true
}
///|
/// Whether the handshake has completed.
pub fn Client::is_connected(self : Client) -> Bool {
self.tls.is_connected()
}
///|
/// Where this connection is in its lifecycle.
pub fn Client::phase(self : Client) -> Phase {
self.phase
}
///|
/// The ALPN protocol the handshake settled on.
pub fn Client::alpn(self : Client) -> String? {
self.tls.alpn()
}
///|
/// The TLS code of the cipher suite in force, 0 before one is chosen.
pub fn Client::suite_code(self : Client) -> Int32 {
match self.tls.suite() {
Some(suite) => suite.code()
None => 0
}
}
///|
/// The connection id the client addresses the server by — the server's, once it has named it.
pub fn Client::server_cid(self : Client) -> Bytes {
self.dcid
}
///|
/// The QUIC version this connection settled on. It can differ from the version the first flight
/// went out in: a server that converted that flight (RFC 9368 §2.3) answers in its own preferred
/// version, which this client reads off the long headers and adopts.
pub fn Client::negotiated_version(self : Client) -> UInt32 {
self.version
}
///|
/// The versions a Version Negotiation packet offered, empty when none came. A non-empty answer
/// means this attempt is over: RFC 9000 §6.2 leaves the caller to start another with one of them.
pub fn Client::versions(self : Client) -> Array[UInt32] {
self.versions.copy()
}
///|
/// Install the client's 0-RTT (early) keys (RFC 9001 §5.2) by hand. A client given a ticket and
/// `early=true` has them from its handshake already; this is for keys that came from elsewhere.
pub fn Client::install_zero_rtt(self : Client, keys : @crypto.Keys) -> Unit {
self.zero_rtt_tx = Some(keys)
}
///|
/// Whether the server accepted the early data this client offered.
pub fn Client::early_accepted(self : Client) -> Bool {
self.tls.early_accepted()
}
///|
/// Act on a 1-RTT packet's frames: acknowledgements to the send loop, stream data into the
/// reassembler, flow-control updates to the scheduler, and the rest to the application.
fn Client::on_app_frames(
self : Client,
frames : Array[@frame.Frame],
now : Int64,
path~ : UInt64 = 0UL,
) -> Unit raise {
for frame in frames {
match frame {
Ack(largest~, ..) | AckEcn(largest~, ..) => {
self.core.on_ack_received(
Level::Application,
largest.reinterpret_as_int64(),
)
self.sender.on_ack(frame, now)
}
// §4.1: an acknowledgement for another path settles that path's numbers, and leaves this
// one's congestion state alone.
PathAck(path_id~, ..) | PathAckEcn(path_id~, ..) =>
self.on_path_ack(frame, path_id, now)
Stream(id~, offset~, fin~, data~) => {
self.streams.accept(id, offset, data, fin~)
let event = Readable(id)
if self.streams.streams.contains(id) && !self.events.contains(event) {
self.events.push(event)
}
}
// RFC 9001 §4.1.3: TLS goes on speaking at this level, and what it says is a ticket.
Crypto(offset~, data~) => {
let ready = self.core.on_crypto_frame(Level::Application, offset, data) catch {
_ => return self.close(crypto_buffer_exceeded, b"", now)
}
if ready.length() > 0 {
self.tls.crypto(Application, ready[:]) catch {
@alert.Alert(kind~, ..) =>
return self.close(
crypto_error + kind.code().to_uint64(),
b"TLS alert",
now,
)
}
self.absorb()
}
}
HandshakeDone => {
// RFC 9001 §4.9.2: the Handshake keys are done once the server says so.
self.confirmed = true
self.handshake_gone = true
self.handshake_out.clear()
}
NewConnectionId(seq~, retire_prior_to~, conn_id~, reset_token~) =>
self.on_new_id(seq, retire_prior_to, conn_id, reset_token)
MaxData(v) => self.sender.on_max_data(v)
MaxStreamData(id~, max~) => self.sender.on_max_stream_data(id, max)
ResetStream(id~, error_code~, ..) =>
self.events.push(Reset(id, error_code))
StopSending(id~, error_code~) => self.events.push(Stopped(id, error_code))
// The peer's side of the path bookkeeping (draft-ietf-quic-multipath §3, §4): the id to
// address a further path by, the room it offers for more paths, and its word on each one.
// They arrive before anything has been sent on the path they name, which is what they are
// for — a path is opened on the id the peer gives for it, not the other way round.
PathNewConnectionId(path_id~, seq~, conn_id~, ..) =>
if path_id > self.paths.local_max() {
return self.close(protocol_violation, b"a path never agreed", now)
} else {
ignore(self.paths.on_peer_cid(path_id, seq, conn_id))
}
PathRetireConnectionId(path_id~, seq~) =>
ignore(self.paths.retire_peer_cid(path_id, seq))
PathStatus(path_id~, seq~, available~) =>
if path_id > self.paths.local_max() {
return self.close(protocol_violation, b"a path never agreed", now)
} else {
ignore(self.paths.on_status(path_id, seq, available))
}
PathAbandon(path_id~, ..) => ignore(self.paths.abandon(path_id))
MaxPathId(max~) => self.paths.on_max_path_id(max)
// §4.7: a report that the peer has no more connection ids for a path, which this end acts
// on by issuing one; nothing is owed back.
PathCidsBlocked(..) => ()
// A challenge is answered on the path it came from, and a path is proven once its own
// challenge comes back unchanged (draft-ietf-quic-multipath §3.1).
PathChallenge(data) =>
if path == 0UL {
self.sender.queue_control(PathResponse(data))
} else {
ignore(self.send_on(path, [PathResponse(data)], now))
}
PathResponse(data) =>
if path != 0UL {
ignore(self.paths.prove(path, data))
}
@frame.Datagram(data~) => {
self.datagrams.push(data)
if !self.events.contains(Event::Datagram) {
self.events.push(Event::Datagram)
}
}
ConnectionClose(error_code~, ..) => {
self.phase = Draining
self.close_at = now + self.cfg.close_period
self.events.push(Closed(error_code))
return
}
_ => ()
}
}
// A path of its own is acknowledged on itself, in a packet that names it (§4.1).
if path != 0UL && self.phase == Active {
match self.ack_on(path, 0UL) {
Some(ack) => ignore(self.send_on(path, [ack], now))
None => ()
}
}
}
///|
/// Open a bidirectional stream this end initiates, without putting anything on it. A caller that
/// places a stream's frames on a path of its own choosing — `send_on` — has to say the stream
/// exists first: RFC 9000 §19.8 lets a peer answer only on a bidirectional stream this end opened,
/// and the send loop's record of having opened it is what that turns on. Answers false for a stream
/// this end may not initiate, which is a unidirectional one or the peer's own.
pub fn Client::open_stream(self : Client, id : UInt64) -> Bool {
guard self.phase == Active else { return false }
guard @stream.is_local(id, server=false) && @stream.is_bidi(id) else {
return false
}
self.sender.sched.queue(id, 0UL)
true
}
///|
/// Queue `data` on stream `id`, ending it after the data when `fin`. Before the handshake
/// finishes, data queued here leaves in 0-RTT packets when a ticket brought early keys, and the
/// send loop carries it over to 1-RTT if the server refuses them (RFC 9000 §4.6.2).
pub fn Client::write(
self : Client,
id : UInt64,
data : Bytes,
fin~ : Bool = false,
) -> Unit {
self.sender.queue_stream(id, data)
if fin {
self.sender.queue_fin(id)
}
}
///|
/// The bytes readable on stream `id` in order, emptied by the call; their flow-control credit
/// goes back to the server here.
pub fn Client::read(self : Client, id : UInt64) -> Bytes {
let data = self.streams.read(id)
if data.length() > 0 {
if self.streams.max_data() is Some(max) {
self.sender.queue_control(MaxData(max))
}
if self.streams.max_stream_data(id) is Some(max) {
self.sender.queue_control(MaxStreamData(id~, max~))
}
}
data
}
///|
/// Whether stream `id` has ended and every byte of it has been read.
pub fn Client::finished(self : Client, id : UInt64) -> Bool {
self.streams.finished(id)
}
///|
/// A new unidirectional stream from this client (RFC 9000 §2.1).
pub fn Client::open_uni(self : Client) -> UInt64 {
let id = self.next_uni * 4UL + 2UL
self.next_uni = self.next_uni + 1UL
id
}
///|
/// How many bytes written to stream `id` the server has not acknowledged.
pub fn Client::buffered(self : Client, id : UInt64) -> UInt64 {
self.sender.buffered(id)
}
///|
/// What has happened on this connection since the last call, emptied by it.
pub fn Client::take_events(self : Client) -> Array[Event] {
let out = self.events.copy()
self.events.clear()
out
}
///|
/// Queue an unreliable datagram (RFC 9221), answering whether the server will take it.
pub fn Client::send_datagram(self : Client, data : Bytes) -> Bool {
self.sender.queue_datagram(data)
}
///|
/// The datagrams that arrived since the last call, emptied by it.
pub fn Client::take_datagrams(self : Client) -> Array[Bytes] {
let out = self.datagrams.copy()
self.datagrams.clear()
out
}
///|
/// Run the timers at `now` (RFC 9002 §6.2): the probe timeout on each handshake level, the send
/// loop's own, the idle timeout, the closing period, and the probe a client owes a server it
/// cannot hear — the last of which is what keeps a handshake from wedging when the server's
/// amplification budget is spent (RFC 9002 §6.2.2.1).
pub fn Client::tick(self : Client, now : Int64) -> Unit {
self.at = now
match self.phase {
Closing => if now >= self.close_at { self.phase = Draining }
Draining => ()
Active => {
if self.idle_timeout > 0L && now - self.last_rx >= self.idle_timeout {
return self.close(0UL, b"", now)
}
let pto = self.sender.recovery.pto()
self.initial_out.tick(now, pto)
self.handshake_out.tick(now, pto)
self.tick_paths(now)
let _ = self.sender.on_pto_timeout(now)
if !self.tls.is_connected() &&
now - self.last_tx >= pto &&
self.initial_out.flight.length() == 0 &&
self.handshake_out.flight.length() == 0 {
// Nothing is in flight and the handshake is not moving: the server is waiting for
// something it can answer, which a padded Initial with a PING gives it.
self.last_tx = now
self.probes.push(self.build_probe())
}
}
}
}
///|
/// The datagrams to put on the wire for one more attempt at a deadlocked handshake: a PING in a
/// padded Initial, which a server with no amplification budget left can still answer.
pub fn Client::probe(self : Client, now : Int64) -> Array[Bytes] {
self.at = now
guard self.phase == Active && !self.tls.is_connected() else { return [] }
[self.build_probe()]
}
///|
/// A padded Initial carrying a PING: the packet a client sends when the server can answer
/// nothing until something arrives (RFC 9002 §6.2.2.1).
fn Client::build_probe(self : Client) -> Bytes {
let pn = self.core.next_packet_number(Level::Initial)
let payload = @packet.payload([@frame.Ping])
let build = body => {
seal_long(
Initial,
1U,
self.dcid,
self.scid,
pn,
4,
body,
self.initial_tx,
token=self.token,
)
}
pad(payload, build, min_datagram)
}
///|
/// Close the connection (RFC 9000 §10.2), in the shape `Session::close` has. An application's
/// close before 1-RTT keys exist cannot name its own error code, so it goes as a transport
/// close carrying APPLICATION_ERROR instead (RFC 9000 §10.2.3).
pub fn Client::close(
self : Client,
error_code : UInt64,
reason : Bytes,
now : Int64,
application~ : Bool = false,
) -> Unit {
guard self.phase == Active else { return }
self.at = now
let level = self.send_level()
let frame = if application && level == Level::Application {
@frame.ConnectionClose(error_code~, frame_type=None, reason~)
} else if application {
@frame.ConnectionClose(
error_code=application_error,
frame_type=Some(0UL),
reason~,
)
} else {
@frame.ConnectionClose(error_code~, frame_type=Some(0UL), reason~)
}
self.phase = Closing
self.close_frame = Some(frame)
self.close_level = level
self.close_at = now + self.cfg.close_period
self.events.push(Closed(error_code))
}
///|
/// The sending state of path `path` on this client. Path 0's lane shares the send loop's recovery
/// state, so a connection that never opens a second path behaves as it did before.
fn Client::lane(self : Client, path : UInt64) -> Lane[Unit] {
match self.lanes.get(path) {
Some(lane) => lane
None => {
let lane = Lane::new(
path,
(),
self.core.space_on(path, Level::Application),
@recovery.State::new(
policy=@recovery.Policy::new(
datagram=self.cfg.max_datagram,
ack_delay=self.cfg.max_ack_delay,
granularity=self.cfg.granularity,
),
),
self.cfg.max_datagram,
)
self.lanes[path] = lane
lane
}
}
}
///|
/// The sending state of path `path`, or `None` before anything has been sent on it.
pub fn Client::path_lane(self : Client, path : UInt64) -> Lane[Unit]? {
self.lanes.get(path)
}
///|
/// This connection's paths, path 0 being the one the handshake ran on.
pub fn Client::paths(self : Client) -> Paths {
self.paths
}
///|
/// Start a second path from this client (draft-ietf-quic-multipath §3.1): the caller names the
/// peer's connection id for it, the id this end will answer to on it, and eight bytes for the
/// PATH_CHALLENGE. The datagrams that follow are handed back by `poll_paths`, tagged with the
/// path they belong on, because which socket each goes out of is the caller's to know.
pub fn Client::add_path(
self : Client,
path : UInt64,
dcid : Bytes,
lcid : Bytes,
now : Int64,
nonce~ : Bytes,
token~ : Bytes,
) -> Bool {
guard self.phase == Active && self.tls.is_connected() else { return false }
guard self.paths.openable(path) else { return false }
guard dcid.length() > 0 && lcid.length() > 0 && nonce.length() == 8 else {
return false
}
self.paths.bind(path, dcid)
ignore(self.paths.bind_local(path, lcid))
// The challenge is what proves the path, so it is remembered before the packet that carries it:
// the answer is checked against what was sent, not against what can be recalled later.
ignore(self.paths.start(path, nonce))
self.sender.queue_control(
PathNewConnectionId(
path_id=path,
seq=0UL,
retire_prior_to=0UL,
conn_id=lcid,
reset_token=token,
),
)
self.send_on(path, [PathChallenge(nonce)], now)
}
///|
/// Put `frames` in one 1-RTT packet on `path`. Path 0 goes out the connection's own way and is
/// picked up by `poll_out`; any other path is numbered, paced and recovered by its own lane.
pub fn Client::send_on(
self : Client,
path : UInt64,
frames : Array[@frame.Frame],
now : Int64,
) -> Bool {
guard self.app_tx is Some(keys) else { return false }
guard frames.length() > 0 else { return false }
if path == 0UL {
for frame in frames {
self.sender.queue_control(frame)
}
return true
}
let lane = self.lane(path)
let dcid = self.paths.peer_cid(path)
guard dcid.length() > 0 else { return false }
let payload = @packet.payload(frames)
let size = (payload.length() + keys.suite.tag + 21 + self.scid.length()).to_int64()
guard lane.send(now, frames, size) is Some(pn) else { return false }
let datagram = send_short(
dcid,
pn,
4,
frames,
keys,
key_phase=self.key_phase,
path~,
)
self.lane_out.push((path, datagram))
true
}
///|
/// The datagrams the further paths have ready, each with the path it belongs on: what a lost
/// packet carried, then a probe a path owes. A client's caller sends each on the socket that path
/// runs over.
pub fn Client::poll_paths(self : Client, now : Int64) -> Array[(UInt64, Bytes)] {
let out : Array[(UInt64, Bytes)] = []
if self.phase != Active || !self.tls.is_connected() {
return out
}
for path in self.lanes.keys().collect() {
guard path != 0UL else { continue }
guard self.lanes.get(path) is Some(lane) else { continue }
while lane.due() is Some(frames) {
guard self.send_on(path, frames, now) else { break }
}
}
out.append(self.lane_out)
self.lane_out.clear()
if out.length() > 0 {
self.last_tx = now
}
out
}
///|
/// The probe timeouts of the further paths (RFC 9002 §6.2.4): a path whose timeout has come puts
/// its two oldest packets back in line for the next `poll_paths`.
fn Client::tick_paths(self : Client, now : Int64) -> Unit {
for path in self.lanes.keys().collect() {
guard path != 0UL else { continue }
guard self.lanes.get(path) is Some(lane) else { continue }
ignore(lane.on_pto(now))
}
}
///|
/// Act on an acknowledgement for path `path` (draft-ietf-quic-multipath §4.1).
fn Client::on_path_ack(
self : Client,
frame : @frame.Frame,
path : UInt64,
now : Int64,
) -> Unit {
let lane = self.lane(path)
lane.on_ack(frame, now, self.sender.ack_exponent) catch {
_ => return self.close(frame_encoding_error, b"", now)
}
}
///|
/// The acknowledgement owed on `path`: an ACK on the path the handshake ran on, a PATH_ACK on any
/// other, naming the path whose numbers it settles.
fn Client::ack_on(
self : Client,
path : UInt64,
delay : UInt64,
) -> @frame.Frame? {
let space = self.core.space_on(path, Level::Application)
guard space.ack_pending() else { return None }
let ack = space.build_ack(delay)
if path == 0UL {
return ack
}
match ack {
Some(Ack(largest~, delay~, first_range~, ranges~)) =>
Some(PathAck(path_id=path, largest~, delay~, first_range~, ranges~))
Some(AckEcn(largest~, delay~, first_range~, ranges~, ect0~, ect1~, ce~)) =>
Some(
PathAckEcn(
path_id=path,
largest~,
delay~,
first_range~,
ranges~,
ect0~,
ect1~,
ce~,
),
)
_ => None
}
}