Introduction
Qrux is a QUIC / HTTP/3–terminating proxy that forwards traffic to plain TCP / HTTP/1.1 backends.
Client (HTTP/3 over QUIC) ──→ [Qrux] ──→ Backend (HTTP/1.1 over TCP)
Features
- QUIC with TLS 1.3 (quinn + rustls)
- HTTP/3 support
- SNI / Host–based routing to multiple backends
- Round-robin load balancing across multiple upstreams
- Upstream TCP connection pooling
- Prometheus metrics (
/metrics) - HTTPS fallback with
Alt-Svcfor browser HTTP/3 discovery - 0-RTT support for returning clients (understand the tradeoffs — see Security)
Links
- Crates.io (releases, metadata): crates.io/crates/qrux
- Source code & issues: github.com/dedsecrattle/Qrux
- This book (GitHub Pages): dedsecrattle.github.io/Qrux
License
MIT.
Quick start
Install from crates.io
Qrux is on crates.io. With the Rust toolchain installed:
cargo install qrux
This installs the qrux binary (usually under ~/.cargo/bin). Use the same qrux --config … commands as below.
1. TLS certificates
For local development, mkcert is convenient:
brew install mkcert # or your OS package manager
mkcert -install
cd certs && mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
Alternatively, use the script in the repo:
./scripts/gen-certs.sh
2. Configuration
Create proxy.toml:
[server]
listen = "0.0.0.0:8443" # QUIC/HTTP3 port
cert = "./certs/cert.pem"
key = "./certs/key.pem"
metrics_listen = "127.0.0.1:9090" # Prometheus metrics
https_listen = "0.0.0.0:8444" # HTTPS fallback (Alt-Svc)
[[routes]]
match = "*"
upstream = "example.com:80"
# Or load balancing:
# upstreams = ["server1:80", "server2:80", "server3:80"]
3. Run the proxy
cargo run --release -- --config proxy.toml
4. Smoke test
# HTTP/3 (needs curl with HTTP/3)
curl --http3-only -k https://localhost:8443/
# Or HTTPS fallback (any curl)
curl -k https://localhost:8444/
Build from source
cargo build --release
The release binary is at target/release/qrux.
Configuration
Configuration is TOML, passed with --config <file> (see the example proxy.toml in the repo).
Server block
| Field | Description |
|---|---|
listen | QUIC / HTTP/3 listen address (e.g. 0.0.0.0:8443) |
cert | Path to TLS certificate (PEM) |
key | Path to TLS private key (PEM) |
metrics_listen | Optional. If set, exposes Prometheus metrics on this address |
https_listen | Optional. If set, listens for HTTPS (TCP/TLS) and adds Alt-Svc for HTTP/3 discovery |
Optional: [server.limits]
Omitted keys use defaults (safe for typical deployments). See Production.
| Field | Default | Description |
|---|---|---|
upstream_connect_timeout_secs | 10 | TCP connect timeout to upstream |
upstream_request_timeout_secs | 120 | End-to-end timeout per upstream request |
max_request_body_bytes | 10485760 (10 MiB) | Max HTTP/3 request body from clients |
max_upstream_response_body_bytes | 52428800 (50 MiB) | Max body read from upstream |
max_idle_connections_per_upstream | 16 | Pooled idle TCP connections per upstream |
graceful_shutdown_secs | 30 | Max wait after SIGINT/SIGTERM for QUIC to drain |
Example:
[server.limits]
upstream_connect_timeout_secs = 5
upstream_request_timeout_secs = 60
max_request_body_bytes = 2097152
Routes
Each [[routes]] entry maps hostnames to upstreams:
match— Hostname from TLS SNI or HTTP:authority/Host. Use"*"as a catch-all.upstream— Single upstreamhost:port.upstreams— List ofhost:portvalues; requests are distributed round-robin.
Use either upstream or upstreams, not both for the same route.
Example
[server]
listen = "0.0.0.0:8443"
cert = "./certs/cert.pem"
key = "./certs/key.pem"
metrics_listen = "127.0.0.1:9090"
https_listen = "0.0.0.0:8444"
[[routes]]
match = "api.example.com"
upstream = "127.0.0.1:8080"
[[routes]]
match = "app.example.com"
upstreams = [
"server1.internal:8080",
"server2.internal:8080",
"server3.internal:8080",
]
[[routes]]
match = "*"
upstream = "127.0.0.1:8080"
Prometheus metrics
When metrics_listen is set, Qrux serves Prometheus text format at /metrics on that address.
Metric names use the qrux_ prefix.
Example:
http://127.0.0.1:9090/metrics
Typical metrics
Request counts (labels: method, status, upstream):
qrux_http_requests_total{method="GET",status="200",upstream="example.com:80"} 42
Latency (histogram, labels: method, upstream):
qrux_http_request_duration_seconds_bucket{method="GET",upstream="example.com:80",le="0.1"} 40
Active QUIC connections:
qrux_active_connections 5
Pooled upstream TCP connections (label: upstream):
qrux_upstream_pool_connections{upstream="example.com:80"} 3
Upstream request timeouts (whole request exceeded upstream_request_timeout_secs):
qrux_upstream_timeouts_total 2
Scrape this endpoint with Prometheus or inspect it with curl while debugging.
Architecture
High-level view of how listeners and routing fit together:
┌─────────────────────────────────────────────────────┐
│ Qrux │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ QUIC/HTTP3 │ │ HTTPS │ ◄── Alt-Svc │
│ │ (listen) │ │ (https_listen)│ header │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Router │ ◄── SNI / Host │
│ │ (round-robin) │ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Upstream pool │ ◄── TCP reuse │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Backends │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────────┘
HTTPS fallback and Alt-Svc
The optional HTTPS listener responds over TLS (HTTP/1.1) and sends an Alt-Svc header pointing at the QUIC port so clients that speak HTTP/3 can discover and upgrade (browser behavior varies; curl is reliable for verifying HTTP/3).
Example header shape:
Alt-Svc: h3=":8443"; ma=86400, h3-29=":8443"; ma=86400
Adjust ports to match your listen / https_listen configuration.
Security
0-RTT (early data)
0-RTT lets returning clients send application data in the first flight, which improves latency but data can be replayed by an attacker who captures the encrypted packets.
Generally reasonable: idempotent reads (e.g. GET) where replay is acceptable.
Avoid: anything that must run exactly once — payments, POST / PUT / DELETE, or sensitive state changes without replay protection at the application layer.
Further reading: Cloudflare — Introducing 0-RTT.
TLS and certificates
- Use real certificates from a public CA in production.
- Protect private keys (
keyin config); never commit them to git.
Upstream trust
The proxy terminates TLS from clients and opens new TCP connections to backends. Ensure network access to upstreams is restricted (firewall / VPC) to what the proxy actually needs.
Production operations
Graceful shutdown
Send SIGINT (Ctrl+C) or SIGTERM (e.g. Kubernetes, systemd stop). Qrux will:
- Stop accepting new QUIC connections and notify the metrics / HTTPS sidecars to exit their accept loops.
- Wait for existing QUIC traffic to finish, up to
limits.graceful_shutdown_secs(default 30s). - Exit.
If shutdown exceeds that window, the process still exits after the timeout (logged as a warning).
Timeouts and limits
Tune [server.limits] in your TOML for your backends and SLAs:
upstream_connect_timeout_secs— Fail fast if a TCP connect to an upstream hangs (default 10s).upstream_request_timeout_secs— Cap for connect + request + full response body from the upstream (default 120s). Must be ≥ connect timeout.max_request_body_bytes— Rejects oversized HTTP/3 request bodies with 413 (default 10 MiB).max_upstream_response_body_bytes— Protects memory if an upstream sends a huge body (default 50 MiB).max_idle_connections_per_upstream— TCP pool size perhost:port(default 16).
When the upstream request timeout fires, the counter qrux_upstream_timeouts_total increments.
Startup validation
The config must include at least one [[routes]] row with upstream or upstreams, and limit fields must be positive and consistent (see defaults in the Configuration table). Invalid configs fail at startup with a clear error.
Observability
- Logs:
RUST_LOG=qrux=info(ordebug). Default filter in the binary includesqrux=info. - Metrics: Prometheus scrape on
metrics_listen; see Metrics.
Docker
The repository includes a Dockerfile, docker-compose.yml, and docker/proxy.toml.example. Publish QUIC with UDP (-p 8443:8443/udp). CI pushes to GitHub Container Registry (ghcr.io/<owner>/qrux); optionally configure DOCKERHUB_USERNAME (variable) and DOCKERHUB_TOKEN (secret) to push the same tags to Docker Hub. See the README for docker build, docker compose, docker pull, and manual docker push.