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, and RFC 9369 §3.2 version 2's own type codes |
| crypto | §5 packet protection, header protection and the Retry tag, §6 key update, RFC 9369 §3.3 version 2's salt, labels and Retry key, 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, version 2 and RFC 9368's version_information on both ends, 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 |
| Client::open_stream | says a stream this end initiates exists, for a caller that puts its frames on a path itself |
| 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 scid : Bytes = b"\x01\x02\x03\x04\x05\x06\x07\x08"
let params = @conn.Limits::new(idle_timeout=30_000_000UL, streams_bidi=8UL).params()
params.push({ kind: @conn.Kind::InitialScid, value: scid, })
let client = @conn.Client::new(
@tls.Client::new(@x509.Roots::new(), alpn=["h3"]),
"a.example",
@conn.encode(params[:]),
n => Bytes::make(n, b'\x00'),
b"\x08\x07\x06\x05\x04\x03\x02\x01",
scid,
0L,
) catch {
_ => abort("the client did not start")
}
// Once the handshake is done, the request goes on stream 4, on whichever path the caller chooses.
ignore(client.open_stream(4UL))
ignore(
client.send_on(
1UL,
[@frame.Stream(id=4UL, offset=0UL, fin=true, data=b"GET /\r\n")],
0L,
),
)let cfg = { ..@conn.Config::default(), max_datagram_frame_size: 1200UL }
assert_eq(cfg.max_datagram_frame_size, 1200UL)assert_eq(@packet.Kind::Initial.code(@packet.version1), 0)
assert_eq(@packet.Kind::Initial.code(@packet.version2), 1)
assert_eq(@packet.Kind::of(@packet.version2, 1), @packet.Kind::Initial)
// The constants that differ, by the number a long header carries.
assert_eq(@crypto.Version::of(@packet.version2).unwrap().salt[0].to_int(), 0x0d)
assert_eq(@crypto.Version::of(@packet.version2).unwrap().key, b"quicv2 key")let only_v2 = { ..@conn.Config::default(), versions: [@packet.version2], }
assert_eq(only_v2.versions, [@packet.version2])
let both = { ..@conn.Config::default(), versions: [@packet.version1, @packet.version2], }
assert_eq(both.versions.length(), 2)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 |
| run | peer | what it checks | result |
|---|---|---|---|
| scripts/quic/check_aioquic_early_data.py | aioquic 1.3.0 | a resumed connection writes before its handshake, with neither Config credit field set, and the peer reads those bytes before its handshake completes | passes: one 0-RTT datagram of 67 bytes, early_accepted true, the peer's first read recorded ahead of its own handshake |
| scripts/quic/check_picoquic_multipath.py | picoquic 7fa9c85 | a second path is opened from a second socket against the connection id the peer offers, challenged and validated, and then carries a request of its own | passes: both paths answer the same file, a PATH_ACK naming path 1 is in the peer's log in both directions, and the path's flight settles to zero |
| scripts/quic/check_versions.py | quic-go v0.55.0 | both directions, in version 1 and version 2, with the default key share; a Version Negotiation packet and a restart; and this library's own client against a server that prefers version 2, which converts a version 1 first flight | passes: twenty checks, the version read off both ends each time, the peer naming X25519MLKEM768 as the group it ran, and the client reporting that it adopted version 2 |
| scripts/quic/check_ngtcp2.py | ngtcp2 v1.16.0 | its example client opens a stream against this library's server; integration/ngtcp2-peer — a small server built on ngtcp2_conn_server_new that echoes a raw stream under any ALPN — answers this library's client. Both directions in version 1 and version 2, plus a run where ngtcp2 converts a version 1 first flight into version 2 | passes: twenty checks, the version named by each end, the peer reporting every echoed byte, and the client completing under the converted version |
| integration/ngtcp2-peer | ngtcp2 v1.16.0, OpenSSL | a raw bidirectional stream echo, because ngtcp2's own examples are HTTP/3 servers and its one raw-stream example is not a CMake target and hard-codes the h3 ALPN | built and run by CI; accepts the client's first ALPN offer, finishes the stream, and prints the version it spoke |
Install
Download zipQUIC transport for MoonBit.