Status: Work in progress. The relay server works and forwards traffic, but end-to-end QUIC-over-relay is not yet verified. See Known Issues.
Lightweight UDP relay server for NAT traversal fallback. When direct STUN + UDP hole punching fails (e.g. symmetric NATs, corporate firewalls), both peers register with this relay and all QUIC traffic is forwarded through it transparently.
- Each peer sends
REG:<session_uuid>\nto the relay - Relay responds
ACK\n - Once two peers register for the same session, all subsequent UDP datagrams are forwarded bidirectionally
- Sessions are cleaned up after 5 minutes of inactivity (configurable)
The relay handles NAT port rebinding -- if a peer's external port changes (e.g. socket close + rebind), the relay detects the same IP and updates the session automatically.
cargo build --releaseThe release binary is ~2MB stripped. A GitHub Actions workflow builds the Linux x86_64 binary on every push to main.
# Default: listen on UDP port 4433, 5-minute session timeout
./quic-relay
# Custom port and timeout
./quic-relay --port 5000 --session-timeout-secs 600Set RUST_LOG for log verbosity:
RUST_LOG=debug ./quic-relay- Launch a
t3.microinstance (e.g.ap-southeast-1for Modal'sapregion) - Assign an Elastic IP for a stable address
- Security group: allow inbound UDP on port 4433
- Download the binary from GitHub Actions artifacts:
# From your local machine
gh run download --name quic-relay-linux-x86_64 --dir /tmp/quic-relay-artifact
scp -i ~/.ssh/your-key.pem /tmp/quic-relay-artifact/quic-relay ubuntu@your-ec2:~/- Start it on EC2:
ssh -i ~/.ssh/your-key.pem ubuntu@your-ec2
chmod +x ~/quic-relay
nohup ~/quic-relay --port 4433 > ~/quic-relay.log 2>&1 &The relay requires a patched version of quic-portal that adds SO_REUSEPORT to Quinn's sockets, allowing the keepalive socket and Quinn to coexist on the same port.
Set QUIC_RELAY_IP in hosting/.env:
QUIC_RELAY_IP=your-elastic-ip
To skip hole punching and always use the relay:
QUIC_RELAY_IP=your-elastic-ip
QUIC_RELAY_ONLY=true
The hosting layer reads these via modal.Secret.from_dotenv() and injects them into the Modal container. The client discovers the relay address from the Modal Dict automatically.
- Server registers with relay (keepalive socket maintains NAT mapping)
- Quinn binds same port (
SO_REUSEPORTallows coexistence) - Client discovers relay info from Modal Dict, registers, connects through relay
- Relay forwards QUIC datagrams bidirectionally
- Keepalive stops once QUIC connection is established
- End-to-end not yet verified. The relay correctly forwards traffic between peers, but the full QUIC handshake through the relay has not completed successfully yet. The main challenge was NAT mapping expiration between the keepalive socket close and Quinn socket bind, which the
SO_REUSEPORTchange to quic-portal is intended to fix. - Requires quic-portal fork. Upstream quic-portal does not set
SO_REUSEPORTon Quinn's sockets, which is needed for the keepalive + Quinn coexistence.
MIT License