QUIC transport for MoonBit.
// A protected Initial packet, from the connection ID up (RFC 9001 §5.2–§5.4).
let dcid : Bytes = b"\x83\x94\xc8\xf0\x3e\x51\x57\x08"
let (client, server) = @crypto.initial(dcid[:])
let wire = @conn.seal_long(
@packet.Kind::Initial, 1U, dcid, b"\xaa\xbb\xcc\xdd", 1L, 4,
@packet.payload([@frame.Ping]), server,
)
// And back, with the packet number recovered against what this space has seen.
let opened = @conn.recv_long(wire, client, largest=-1L) catch {
_ => abort("the packet did not open")
}
guard opened is Some((frames, n)) else { abort("the packet did not open") }
assert_eq(n, 1L)
assert_eq(frames, [@frame.Ping])moon run src/examples/02-frames moon run src/examples/05-ack moon run src/examples/08-streams
moon run src/examples/03-params moon run src/examples/06-flow moon run src/examples/09-sender
moon run src/examples/04-crypto moon run src/examples/07-recovery| What it covers | |
|---|---|
| frame | §19 every frame type, §19.3.1 the ACK range encoding both ways, and the multipath frames of draft-ietf-quic-multipath §4 with their own type codes |
| packet | §17 long and short headers, Retry, Version Negotiation, §17.1 packet numbers, §12.3 the three spaces |
| crypto | §5 packet protection, header protection and the Retry tag, §6 key update, and the path-and-packet-number nonce multipath needs |
| recovery | RFC 9002 the round-trip estimate, both loss thresholds, the probe timeout, and NewReno, CUBIC (RFC 9438) and BBR behind one controller trait, with persistent congestion; §7.7 pacing as a token bucket over bytes |
| stream | §2 stream IDs and reassembly, §3 both state machines, §4 data flow control and the stream quota, and a round-robin scheduler |
| conn | §18 transport parameters, §12.3 the three spaces and their CRYPTO streams, the client and server handshakes, the connection table and event loop with its timers and amplification limit (§8.1, §10, §14.1), §9 migration and path validation, §10.3 stateless reset, RFC 9221 datagrams, 0-RTT and resumption, §6 Retry and version negotiation, the frame and parameter checks §12.4 and §18 require, many client connections on one id space, and multipath: a packet-number space, a congestion window and a PATH_ACK for each path |
let server : @conn.Endpoint[Addr] = @conn.Endpoint::server(
@conn.Config::default(),
@tls.Server::new(chain, key, alpn=["h3"]),
rand=entropy,
)
server.recv(datagram, from, now)
for conn in server.take_busy() {
for event in conn.take_events() {
match event {
Readable(id) => {
let bytes = conn.read(id)
conn.write(id, reply(bytes), fin=conn.finished(id))
}
_ => ()
}
}
}
while server.poll_out(now) is Some((datagram, to)) {
send(datagram, to)
}let client = @conn.Client::new(
@tls.Client::new(roots, alpn=["h3"]),
"a.example",
@conn.encode(params),
rand=entropy,
dcid=Bytes::make(8, 1),
scid=Bytes::make(8, 2),
now,
)
for datagram in client.poll_out(now) { send(datagram, to) }
for datagram in receive() {
client.recv(datagram, now)
for answer in client.poll_out(now) { send(answer, to) }
}client.tick(now)
for datagram in client.poll_out(now) { send(datagram, to) }client.write(0UL, request, fin=true)
for datagram in client.poll_out(now) { send(datagram, to) }let kept = client.remembered().unwrap() // a ticket and the parameters it was issued under
let next = @conn.Client::new(
@tls.Client::new(roots, alpn=["h3"], sessions=store, early=true),
"a.example",
@conn.encode(params),
rand=entropy,
dcid=Bytes::make(8, 3),
scid=Bytes::make(8, 4),
now,
resumption=kept,
)let cfg = @conn.Config::default()
assert_eq(cfg.zero_rtt_data, 0UL)
assert_eq(cfg.zero_rtt_stream_data, 0UL)
// A caller that knows better than the last connection says so; §7.4.1 makes the
// remembered value the ceiling, so a larger override earns a flow-control error.
let small = { ..cfg, zero_rtt_data: 4096UL, zero_rtt_stream_data: 4096UL }
assert_eq(small.zero_rtt_data, 4096UL)let endpoint = @conn.ClientEndpoint::new(@conn.Config::default(), rand=entropy)
let id = endpoint.connect(
@tls.Client::new(roots, alpn=["h3"]),
"a.example",
params,
now,
)
endpoint.recv(datagram, now)
for answer in endpoint.poll_out(now) { send(answer, to) }
endpoint.tick(now)
let client = endpoint.client(id).unwrap()| What it does | |
|---|---|
| Paths::new(local_max=) | the paths of one connection, path 0 being the one the handshake ran on |
| Paths::bind, bind_local, path_of | the peer's id for a path, this end's id for it, and the path an id belongs to |
| Paths::openable, open, abandon | which path ids may be opened, which are carrying traffic, and giving one up |
| Paths::start, prove, validated | challenging a path and knowing when it has answered |
| Paths::on_max_path_id, on_status, next_status | the peer's word on the path count and on which paths are available |
| Endpoint::add_path, Session::add_path | opening a second path, with a connection id the endpoint makes |
| Session::send_on, poll_lane, path_lane | a packet on a named path, the datagrams that path has ready, its state |
| Client::add_path, send_on, recv_on, poll_paths | the same for a client, whose caller owns the sockets and says which path a datagram came in on |
| Lane | one path's packet numbers, congestion window, pacing, and what its packets carried |
let paths = @conn.Paths::new(local_max=2UL) // what this end will maintain
paths.on_max_path_id(1UL) // what the peer said it would
assert_true(paths.openable(1UL))
assert_false(paths.openable(2UL)) // the peer offered one path, not two
let spare : Bytes = b"\xaa\xbb\xcc\xdd"
paths.bind(1UL, spare) // the peer's id for the path
ignore(paths.bind_local(1UL, b"\x01\x02")) // this end's
assert_eq(paths.path_of(b"\x01\x02"), 1UL)
assert_false(paths.validated(1UL))
ignore(paths.start(1UL, b"\x10\x20\x30\x40\x50\x60\x70\x80"))
assert_true(paths.prove(1UL, b"\x10\x20\x30\x40\x50\x60\x70\x80"))
assert_true(paths.validated(1UL))let (_, keys) = @crypto.initial(b"\x83\x94\xc8\xf0\x3e\x51\x57\x08")
assert_eq(keys.nonce_at(0UL, 54321L), keys.nonce(54321L))
assert_true(keys.nonce_at(1UL, 54321L) != keys.nonce(54321L))let status = @frame.PathStatus(path_id=1UL, seq=1UL, available=true)
let wire = @frame.encode(status)
let (back, used) = @frame.decode(wire[:]) catch {
_ => abort("the frame did not decode")
}
assert_eq(used, wire.length())
assert_eq(back, status)
// PATH_ACK names the path whose packet numbers it settles.
let ack = @frame.PathAck(path_id=1, largest=9UL, delay=0UL, first_range=1UL, ranges=[])
assert_eq(@frame.encode(ack).length(), 4)let cfg = { ..@conn.Config::default(), max_datagram_frame_size: 1200UL }
assert_eq(cfg.max_datagram_frame_size, 1200UL)let dcid : Bytes = b"\x83\x94\xc8\xf0\x3e\x51\x57\x08"
let (initial, _) = @crypto.initial(dcid[:]) // AEAD_AES_128_GCM, RFC 9001 §5.2
let strong = @crypto.Keys::of(initial.key, suite=@crypto.aes256)
let others = @crypto.Keys::of(initial.key, suite=@crypto.chacha20)
assert_eq(strong.iv.length(), 12)
assert_eq(others.iv.length(), 12)let reno = @recovery.State::new()
let cubic = @recovery.State::new(control=@recovery.Cubic::new())
let bbr = @recovery.State::new(control=@recovery.Bbr::new())
assert_true(reno.window() > 0L)
assert_true(cubic.window() > 0L)
assert_true(bbr.window() > 0L)| h3spec case | RFC | code |
|---|---|---|
| STREAM offset past the stream's credit | 4.1 | FLOW_CONTROL_ERROR |
| stream id past the limit | 4.6 | STREAM_LIMIT_ERROR |
| initial_source_connection_id missing | 7.3 | TRANSPORT_PARAMETER_ERROR |
| a client declaring a server's parameter | 18.2 | TRANSPORT_PARAMETER_ERROR |
| a parameter whose value is not a varint | 18.1 | TRANSPORT_PARAMETER_ERROR |
| max_udp_payload_size below 1200 | 18.2 | TRANSPORT_PARAMETER_ERROR |
| ack_delay_exponent above 20 | 18.2 | TRANSPORT_PARAMETER_ERROR |
| max_ack_delay at or above 2^14 | 18.2 | TRANSPORT_PARAMETER_ERROR |
| initial_max_streams_* past 2^60 | 18.2 | TRANSPORT_PARAMETER_ERROR |
| ACK range below packet zero | 19.3.1 | FRAME_ENCODING_ERROR |
| ACK of a packet never sent | 13.1 | PROTOCOL_VIOLATION |
| a frame of unknown type | 12.4 | FRAME_ENCODING_ERROR |
| a packet with no frames | 12.4 | PROTOCOL_VIOLATION |
| reserved bits set in a Handshake or 1-RTT packet | 17.2 | PROTOCOL_VIOLATION |
| CRYPTO buffered past the limit | 7.5 | CRYPTO_BUFFER_EXCEEDED |
| PATH_CHALLENGE, DATA_BLOCKED or STREAM_DATA_BLOCKED in a Handshake packet | 12.4 | PROTOCOL_VIOLATION |
| RETIRE_CONNECTION_ID for an id never issued | 19.16 | PROTOCOL_VIOLATION |
| RESET_STREAM or STREAM on a send-only stream | 19.4, 19.8 | STREAM_STATE_ERROR |
| STREAM on a locally initiated stream not opened | 19.8 | STREAM_STATE_ERROR |
| STOP_SENDING or MAX_STREAM_DATA for a stream that does not exist | 19.5, 19.10 | STREAM_STATE_ERROR |
| MAX_STREAM_DATA for a receive-only stream | 19.10 | STREAM_STATE_ERROR |
| invalid MAX_STREAMS | 19.11 | FRAME_ENCODING_ERROR |
| invalid STREAMS_BLOCKED | 19.14 | STREAM_LIMIT_ERROR |
| NEW_CONNECTION_ID with a zero-length id or a bad retire_prior_to | 19.15 | FRAME_ENCODING_ERROR |
| NEW_TOKEN or HANDSHAKE_DONE from a client | 19.7, 19.20 | PROTOCOL_VIOLATION |
| KeyUpdate in Handshake or 1-RTT | RFC 9001 §6 | CRYPTO_ERROR with unexpected_message |
| EndOfEarlyData, or no ALPN overlap | RFC 8446 | CRYPTO_ERROR with the alert |
| a PATH_STATUS or PATH_ABANDON for a path id this end never offered | draft §3.1, §4.2 | PROTOCOL_VIOLATION |
| a path id above 2^32-1 in PATH_NEW_CONNECTION_ID, MAX_PATH_ID or PATHS_BLOCKED | draft §2.1, §4.6 | TRANSPORT_PARAMETER_ERROR, PROTOCOL_VIOLATION |
Install
Download zipQUIC transport for MoonBit.