This is a high-performance relay server built with WebTransport and Rust. It enables browser-based RISC-V VMs to communicate with each other and access the external internet via a User-Space NAT gateway.
- WebTransport/QUIC: Uses modern HTTP/3-based transport for low-latency, secure connections over UDP port 4433.
- Virtual Switch: Broadcasts Ethernet frames between all connected clients (VMs), effectively placing them on the same virtual LAN.
- User-Space NAT Gateway:
- Gateway IP:
10.0.2.2(responds to ARP and Ping) - External Access: Allows VMs to ping external hosts (e.g.,
8.8.8.8) and perform UDP queries (e.g., DNS) by proxying traffic through the container's network stack. - No Privileges Needed: Uses standard UDP sockets and the
pingcommand installed in the container.
- Gateway IP:
- Flexible TLS: Supports both CA-signed certificates (production) and self-signed certificates (development).
For production, use Let's Encrypt (or any CA) certificates. Browsers trust these natively — no hash pinning or serverCertificateHashes needed.
docker compose up -dEdit docker-compose.yml to uncomment and configure the certificate volume mount:
services:
relay:
build: .
ports:
- "4433:4433/udp"
- "4433:4433/tcp"
environment:
RELAY_CERT_PEM: /certs/fullchain.pem
RELAY_KEY_PEM: /certs/privkey.pem
CERT_POLL_INTERVAL: "60"
volumes:
- /etc/letsencrypt/live/relay.yourdomain.com:/certs:roThe entrypoint watches the certificate files and automatically restarts the relay when they change (e.g., certbot renewal).
If your relay sits behind Nginx with HTTP/3 enabled, ensure your Nginx config exposes QUIC on the relay's domain:
server {
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
http3 on;
server_name relay.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/relay.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relay.yourdomain.com/privkey.pem;
# Forward QUIC/WebTransport to the relay
location / {
proxy_pass https://relay-container:4433;
}
}Note: For direct WebTransport proxying, Nginx needs HTTP/3 upstream support, which is still maturing. An alternative is to expose the relay's UDP port directly and point the domain at it, using the same Let's Encrypt certs mounted into the relay container.
With a CA-signed certificate, no hash pinning is needed:
async function connectToRelay() {
const transport = new WebTransport("https://relay.yourdomain.com:4433");
await transport.ready;
console.log("Connected to relay!");
const reader = transport.datagrams.readable.getReader();
const writer = transport.datagrams.writable.getWriter();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log("Received packet:", value);
}
}- Install Rust: Ensure you have the latest stable Rust toolchain installed.
- Run the Server:
cargo run --releaseThe server starts on port 4433 (UDP) with an ephemeral self-signed certificate. It will print a certificate hash you can use for browser connections:
Certificate SHA-256 Hash: e7...3f
Use this hash with serverCertificateHashes when connecting (dev mode)
Listening on https://0.0.0.0:4433
Generate short-lived certificates (valid 10 days, Chrome requires ≤14):
./scripts/generate-certs.sh
cargo run --release -- --cert-pem certs/relay-cert.pem --key-pem certs/relay-key.pemdocker build -t riscv-relay .
docker run -d -p 4433:4433/udp --name relay riscv-relay
docker logs relay # grab the certificate hashSelf-signed certificates require serverCertificateHashes:
const RELAY_CERT_HASH = "YOUR_CERTIFICATE_HASH_HERE";
const certHashBytes = new Uint8Array(
RELAY_CERT_HASH.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
);
async function connectToRelay() {
const transport = new WebTransport("https://127.0.0.1:4433", {
serverCertificateHashes: [
{ algorithm: "sha-256", value: certHashBytes },
],
});
await transport.ready;
console.log("Connected to relay!");
const reader = transport.datagrams.readable.getReader();
const writer = transport.datagrams.writable.getWriter();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log("Received packet:", value);
}
}⚠ Browser Limitation: Chrome/Chromium requires self-signed certificates used with
serverCertificateHashesto have a maximum validity of 14 days. The cert hash changes on every regeneration, so your app must fetch it dynamically.
- Connection: The browser initiates a WebTransport session over QUIC.
- Verification: The browser verifies the server's certificate (via CA trust chain in production, or hash pinning in dev).
- Data Exchange:
- The VM encapsulates Ethernet frames into WebTransport datagrams.
- The Relay receives these datagrams.
- Routing:
- Broadcast: If the packet is internal (e.g., ARP, or destined for another VM), the relay broadcasts it to all other connected clients.
- NAT: If the packet is destined for the internet (e.g., Google DNS
8.8.8.8), the relay performs NAT, sends it out via the host's UDP socket, and forwards the response back to the specific client.
cargo run --release -- --bind 0.0.0.0 --port 4433| Flag / Env Var | Default | Description |
|---|---|---|
--port |
4433 |
UDP/QUIC listen port |
--bind |
0.0.0.0 |
Bind address |
--cert-pem / RELAY_CERT_PEM |
— | Path to TLS certificate PEM |
--key-pem / RELAY_KEY_PEM |
— | Path to TLS private key PEM |
--heartbeat-interval |
30 |
Heartbeat interval (seconds) |
--peer-timeout |
150 |
Peer timeout (seconds) |
CERT_POLL_INTERVAL |
60 |
Cert file poll interval (seconds, entrypoint only) |