QUIC server
using [Nursery] does not compileEvery listing on this page opens with using [Nursery], and that line
does not work today. Measured:
error<E605>: undefined context: Nursery
Nursery is declared as a type — public type Nursery is { … } in
core/async/nursery.vr — and using [...] takes a context, which
the grammar spells public context Name { … }. core/context/ declares
five of those (Random, Logger, Database, Auth, Config);
Nursery is not among them.
Mounting the type is not the fix. It quiets the checker and leaves the
program wrong: with mount core.async.nursery.{Nursery}; added, the
listing type-checks, and running it warns
WARN Context 'Nursery' in function 'main' has no matching context
declaration
before hanging. The loud E605 is the honest answer, so the listings
are left as they are until structured concurrency has a real context to
name.
Below the using line the QUIC surface has its own gap: QuicConnection
is backed entirely by @intrinsic("verum.quic.…"), and all seventeen of
those keys sit in the frozen unimplemented set, so a dial would stop
there with a panic naming the key — the form measured on sibling
families, for example
pq. Read rather than run here: the
using line stops the listing before the dial is reached.
This note stops being true when Nursery becomes a context, or when the
listings stop claiming one.
Warp's high-level QUIC server lives at core.net.quic.api.QuicServer.
It wraps the TLS 1.3 server-side handshake, per-connection state
machine, and UDP packet dispatch behind an async accept loop.
Minimum viable server
using [Nursery]
mount core.*;
mount core.net.quic.api.{QuicServer, QuicServerOptions, QuicAccepted};
pub async fn main() -> Result<(), core.net.quic.api.QuicServerError> {
let cert_chain: List<List<Byte>> = load_cert_chain_der();
let signer = load_private_key_signer();
// ^ YOURS TO WRITE. Neither helper exists in `core/` — there is no
// certificate loader in the standard library yet. Read the DER
// bytes with `core.io.file` and build the signer from them.
let opts = QuicServerOptions.from_cert(cert_chain, signer)
.with_alpn([b"h3".to_list(), b"echo-v1".to_list()]);
let server = QuicServer.bind(&f"0.0.0.0:443", opts).await?;
print(f"listening on {server.local_addr()}");
// accept_loop spawns one task per peer into the caller's nursery.
server.accept_loop(|accepted: QuicAccepted| async move {
handle_peer(accepted).await;
}).await?;
Ok(())
}
async fn handle_peer(mut conn: QuicAccepted) {
match conn.next_bidi_stream().await {
Ok(mut stream) => {
let mut buf: List<Byte> = [];
let _ = stream.recv.read_to_end(&mut buf).await;
let _ = stream.send.write_all(&buf).await; // echo
let _ = stream.send.close().await;
},
Err(_) => {},
}
}
accept_loop is the structured-concurrency variant. Each connection
runs inside its own nursery; when the outer main is cancelled the
whole tree tears down deterministically.
Server options
let opts = QuicServerOptions.from_cert(cert_chain, signer)
.with_alpn([b"h3".to_list()]);
// Tune per-connection flow control.
opts.params.initial_max_data = 64_u64 * 1024_u64 * 1024_u64; // 64 MiB
opts.params.initial_max_streams_bidi = 1_024_u64;
// Cap server-wide concurrent connections.
opts.max_connections = 10_000_u64;
// Extend idle timeout for long-lived services.
opts.idle_timeout = Duration.from_secs(120);
opts.params.max_idle_timeout_ms = 120_000_u64;
Defaults per QuicServerOptions.from_cert:
| Field | Default |
|---|---|
params.initial_max_data | 10 MiB |
params.initial_max_streams_bidi | 128 |
params.initial_max_streams_uni | 128 |
params.max_idle_timeout_ms | 30 000 ms |
suites | AES_128_GCM_SHA256, CHACHA20_POLY1305_SHA256, AES_256_GCM_SHA384 |
accepted_groups | X25519, Secp256r1 |
dhe_pool | LocalDhePool.new() (in-process, 128-entry LRU) |
Certificate + signer
cert_chain is the server's DER-encoded certificate chain (leaf first,
root last). signer is any implementation of the CertSigner
protocol — for most production setups this is
core.security.x509.sign.FileSigner or an HSM-backed variant.
// DOES NOT COMPILE — kept as the shape the API wants, not as a recipe.
// Both mounts name modules that do not exist, and the signer half has
// no substitute at all. See the note directly below.
mount core.security.x509.parse.{parse_cert_chain_pem};
// ^ NEITHER MODULE EXISTS — there is no `parse.vr` and no `sign.vr`
// under core/security/x509/, and neither name is declared anywhere.
// The table below says so too; this marker is here because a reader
// copies the block, not the table.
mount core.security.x509.sign.{FileSigner};
let chain = parse_cert_chain_pem(&fs.read_text("/etc/letsencrypt/live/example.com/fullchain.pem").await?)?;
let signer = FileSigner.from_pem_path("/etc/letsencrypt/live/example.com/privkey.pem").await?;
let opts = QuicServerOptions.from_cert(chain, Heap(signer));
Checked against core/:
| written here | reality |
|---|---|
core.security.x509.parse.{parse_cert_chain_pem} | no parse.vr module, and no parse_cert_chain_pem anywhere |
core.security.x509.sign.{FileSigner} | no sign.vr module, and no FileSigner anywhere |
ServerOptions.from_cert(chain, Heap(signer)) | exists — core/net/h3/server.vr:85 |
The PEM parsing that does exist is Certificate.from_pem_chain(&Text) -> Result<List<Certificate>, LegacyTlsError> in
core/security/x509/credential.vr, and a trust bundle is
TrustStore.from_pem_bundle.
The signer half has no substitute. from_cert wants a
Heap<dyn CertSigner>, and the protocol is declared once with no
implementation anywhere — one command shows both halves:
grep -rn 'type CertSigner' core/ --include='*.vr'
# core/net/tls13/handshake/server_sm.vr — the protocol
grep -rl 'implement CertSigner' core/ --include='*.vr' | wc -l # 0
So this walkthrough cannot be written today — not with different spellings, not at all — until a concrete signer lands.
Handling individual streams
QuicAccepted exposes four async operations:
next_bidi_stream()— wait for the peer to open a bidi stream.next_uni_stream()— wait for a peer-initiated uni stream.next_datagram()— pop the next inbound RFC 9221 datagram.send_datagram(data)— emit a datagram; best-effort.
Peer-initiated stream IDs arrive in allocation order from the peer; the server doesn't choose them.
Graceful shutdown
// Send CONNECTION_CLOSE with application error 0x0100 (H3_NO_ERROR).
accepted.close(0x0100_u64, &f"going away").await?;
Connection-level close triggers the RFC 9000 §10.2 three-PTO draining window; subsequent datagrams on the same peer IP are ignored.
Stateless reset
The server holds a long-lived StatelessResetKey and issues reset
tokens per-CID via StatelessResetKey.token_for_cid(cid). A datagram
with no matching connection ends with a trailing 16-byte token
that the client recognises — see
stateless_reset_surface discussion.
Address validation
First-flight Initial packets from a new peer IP are anti-amplification limited to 3× received bytes (§8.1). For high-traffic deployments, use Retry to cheaply bounce through an address-validation round-trip:
opts.require_retry = true; // every new 4-tuple gets a Retry first
The server keeps a symmetric-key sealed Retry token so follow-up
Initials carry the proof of liveness.
Observability
Per-connection stats expose pacing rate, loss counters, cwnd, and key-phase generation:
accepted.stats().snapshot().pretty();
Prometheus rendering lives in core.net.quic.stats_prometheus, which
exports render_connection(&QuicStats, &Text) and
render_endpoint(&EndpointStats) — both return the exposition text.
Serving it at /metrics is yours to wire; there is no
listener_exporter, and no exporter that takes a server.
See also
core.net.quic— module reference.- Transport parameters — what gets advertised in the server's EncryptedExtensions.
- TLS 1.3 handshake — server typed-state machine driving each handshake.