moq-uring
Experimental Linux io_uring support for the native MoQ stack: a
thread-per-core Worker that owns a SINGLE_ISSUER | DEFER_TASKRUN | COOP_TASKRUN ring, a userspace timer heap, a local (!Send) task set, and
the UDP sockets bound through it.
- Receive: one persistent multishot
recvmsgper socket, fed from a registered provided-buffer ring of worst-case-sized buffers (one per completion), withUDP_GROcoalescing. Received packets borrow the pool and hand the space back on drop, which is also the receive-side backpressure. Incremental consumption (IOU_PBUF_RING_INC) cannot back a multishotrecvmsg: the kernel faults the receive once a buffer's leftover tail is smaller than the recvmsg header. - Send:
sendmsgwith an explicitUDP_SEGMENTcontrol message per call, staged in a fixed pool of buffers owned by id and released on completion (the shape a laterSENDMSG_ZCneeds). - Timers: a heap the worker sweeps; the earliest deadline rides
io_uring_enteras an absolute timeout. Zero timeout SQEs. The worker'sHandle::rundrives MoQ with the worker clock and a single timer. - Parking: a futex word per worker. Remote wakes are an atomic store, plus
one
futex(2)wake only while the worker is actually parked (aFUTEX_WAITSQE armed on the word). - QUIC: a sans-IO QUIC stack over that UDP path. A
quic::Endpointserves many connections on one socket, demuxed by connection id (dials share the socket with accepts, ids rotate as peers consume them, unknown versions get a version negotiation packet). Native peers speak raw QUIC: the ALPN carries the application protocol. - WebTransport: browsers negotiate
h3andquic::web::Requestruns the HTTP/3 CONNECT handshake (SETTINGS, subprotocol selection, capsule close) over the same adapter viaweb-transport-proto.quic::web::Sessionis a raw or web transport (Session::raw).connect_lite/accept_litereturn the session and its driver; poll the driver or await it inside aHandle::spawntask to run it on the worker. Web mode maps stream and close codes through the HTTP/3 error space. - qlog:
quic::qlog::Sinkpoints a group of workers at a directory andquic::Transport::qlogturns capture on. The pinned worker never writes to the file: the QUIC stacks want aSend + Syncwriter, which cannot hold the worker's!Sendring handle, so a trace is staged in memory and handed to one background thread for every worker sharing the sink. Behind theqlogfeature, so a production build compiles none of it. - Metrics: a set of relaxed atomic counters per worker, read from any
thread through
metrics::Metrics::snapshot. Buffer-pool health (ENOBUFS, provided-buffer exhaustion, TX-pool stalls), batch effectiveness (datagrams per receive and per send), ring traffic (submissions, completions,io_uring_entercalls), and scheduling (parks, remote futex wakes, timer churn). Pass ametrics::MetricstoConfig::metricsto hold a copy on the thread that spawned the worker, or read the worker's own withHandle::metrics.moq-relaypublishes them at/metricson its internal listener. - Identity: the socket names its worker.
Handle::udpadopts a loneUdpSocketor a member of a completedmoq-socksteeredSO_REUSEPORTgroup (udp::Bound), and aquic::Endpointbuilt on it runs its demux and every connection driver on that worker, whichever handle built it. A member brings its slot along, so every issued connection id leads with the group's steering byte and the kernel keeps a connection (and a cluster dial's responses) on the worker that owns it. An endpoint on a dropped worker is refused.
Requires Linux 6.12; Worker::new refuses older kernels with a legible
error rather than degrading (note that default container seccomp policies
block io_uring entirely). There is no fallback here: older kernels keep using
the tokio stack.
Backends
The quic module uses the sans-IO moq-noq-proto
stack with rustls. The noq feature is enabled by default and remains optional
so the worker, timers, and UDP socket can be built without QUIC.
| Feature | Stack | TLS |
|---|---|---|
noq (default) |
moq-noq-proto | rustls |
Building without default features leaves the quic module out entirely.
moq-relay enables it with --features io-uring.
The qlog feature turns on noq's capture support. It writes one file per
connection.
Validation
tests/echo.rs runs an echo against a tokio noq peer over the worker:
handshake, half a megabyte each way, and timers driven by noq's timeout.
tests/session.rs runs full moq-lite sessions through
quic::Endpoint (including two clients demuxed on one server socket), and
tests/endpoint.rs covers the endpoint mechanics (dial+accept on one socket,
version negotiation, the dial-only refusal), tests/workers.rs runs a
steered two-worker reuseport group serving one port across threads, and
tests/web.rs is WebTransport interop against web-transport-moq: stream and
datagram echo through the H3 framing,
close codes through the capsule, and a full moq-lite session over
WebTransport. All of them skip (loudly) below the kernel floor, which
includes GitHub-hosted CI runners.
Benchmarks
udp_tokio and udp_uring are the disposable syscall-level matrices from the
first spike (recv batching x GRO x GSO, epoll vs io_uring); see git history
for their methodology. echo_noq is the ablation matrix over the real
worker: the same noq echo with receive batching, GRO, and GSO toggled one
at a time.