WebTransport
A raw QUIC session helper over quinn for server-to-server tunnels — tokio-only, behind the webtransport feature, TLS required.
WebTransport
The webtransport module exposes a thin wrapper over a quinn QUIC server
endpoint. It accepts QUIC connections, enables datagrams, and surfaces
bidirectional and unidirectional streams plus unreliable datagrams.
This is not W3C WebTransport. The module does not implement the W3C draft
handshake (CONNECT :protocol = webtransport over HTTP/3,
SETTINGS_ENABLE_WEBTRANSPORT, per-session demultiplexing). Browsers cannot
reach this server through the WebTransport API — only QUIC peers that speak the
same private framing can. The W3C-compliant CONNECT handshake is a roadmap item.
Use this module when you want a private QUIC tunnel between Tako-aware peers
(server-to-server, or a custom client you control). Do not advertise the
endpoint as WebTransport to browsers; they will reject it.
WebTransport is tokio-only and lives behind the webtransport feature.
Enabling http3 enables webtransport automatically. Like all QUIC transports,
TLS is required — you pass PEM cert and key paths at spawn time.
Serving sessions
serve_webtransport(addr, cert_path, key_path, handler) binds a QUIC endpoint
and dispatches each accepted connection to your handler, wrapped in a
RawQuicSession. The handler returns a boxed, pinned future.
use tako::webtransport::{RawQuicSession, serve_webtransport};
async fn handle_session(session: RawQuicSession) {
let remote = session.remote_address();
println!("New session from {remote}");
loop {
tokio::select! {
result = session.accept_bi() => {
match result {
Ok((mut send, mut recv)) => {
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
while let Ok(Some(n)) = recv.read(&mut buf).await {
if send.write_all(&buf[..n]).await.is_err() {
break;
}
}
});
}
Err(_) => break,
}
}
result = session.read_datagram() => {
match result {
Ok(data) => {
let _ = session.send_datagram(data);
}
Err(_) => break,
}
}
}
}
}
#[tokio::main]
async fn main() {
serve_webtransport("[::]:4433", "cert.pem", "key.pem", |session| {
Box::pin(handle_session(session))
})
.await;
}serve_webtransport_with_shutdown(addr, cert, key, handler, signal) is the same
entry point with a graceful-shutdown future. On shutdown the endpoint stops
accepting, drains in-flight sessions up to a 30 s timeout, then aborts the
remainder.
use tako::webtransport::{RawQuicSession, serve_webtransport};
async fn handle_session(session: RawQuicSession) {
let remote = session.remote_address();
println!("New WebTransport session from {remote}");
// Handle bidirectional streams (echo server)
loop {
tokio::select! {
// Accept bidirectional streams
result = session.accept_bi() => {
match result {
Ok((mut send, mut recv)) => {
println!("[{remote}] New bidirectional stream");
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
loop {
match recv.read(&mut buf).await {
Ok(Some(n)) => {
println!("[stream] Echoing {n} bytes");
if send.write_all(&buf[..n]).await.is_err() {
break;
}
}
_ => break,
}
}
});
}
Err(e) => {
println!("[{remote}] Connection closed: {e}");
break;
}
}
}
// Read unreliable datagrams
result = session.read_datagram() => {
match result {
Ok(data) => {
println!("[{remote}] Datagram: {} bytes", data.len());
// Echo datagram back
let _ = session.send_datagram(data);
}
Err(e) => {
println!("[{remote}] Datagram error: {e}");
break;
}
}
}
}
}
println!("Session closed: {remote}");
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
println!("WebTransport echo server on [::]:4433");
println!("Requires TLS certificates: cert.pem and key.pem");
println!();
println!("Generate self-signed certs for testing:");
println!(" openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \\");
println!(" -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'");
serve_webtransport("[::]:4433", "cert.pem", "key.pem", |session| {
Box::pin(handle_session(session))
})
.await;
}
RawQuicSession
RawQuicSession wraps a quinn::Connection and exposes its stream and datagram
surface directly:
| Method | Purpose |
|---|---|
remote_address() | Peer socket address. |
accept_bi() | Accept an incoming bidirectional stream (SendStream, RecvStream). |
accept_uni() | Accept an incoming unidirectional stream (RecvStream). |
open_bi() | Open a new bidirectional stream. |
open_uni() | Open a new unidirectional stream. |
read_datagram() | Read an unreliable datagram. |
send_datagram(bytes) | Send an unreliable datagram. |
close(code, reason) | Close the session with a QUIC error code. |
connection() | Borrow the underlying quinn::Connection for advanced use. |
The stream and datagram types (quinn::SendStream, quinn::RecvStream,
bytes::Bytes) are quinn's own, so anything quinn supports on a connection is
reachable through connection().
Certificate provider note
The endpoint installs the ring rustls CryptoProvider on startup and
advertises the h3 ALPN protocol. If your process also serves plain TLS and you
need a specific provider, pin it before constructing any server — see the same
note on the HTTP/3 page.
Related
- HTTP/3 — the QUIC transport WebTransport builds on;
enabling
http3enableswebtransport. - Feature flags — the
webtransportfeature.