🐙 tako
Transports

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:

MethodPurpose
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.

  • HTTP/3 — the QUIC transport WebTransport builds on; enabling http3 enables webtransport.
  • Feature flags — the webtransport feature.

On this page