Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Qrux logo

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-Svc for browser HTTP/3 discovery
  • 0-RTT support for returning clients (understand the tradeoffs — see Security)

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

FieldDescription
listenQUIC / HTTP/3 listen address (e.g. 0.0.0.0:8443)
certPath to TLS certificate (PEM)
keyPath to TLS private key (PEM)
metrics_listenOptional. If set, exposes Prometheus metrics on this address
https_listenOptional. 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.

FieldDefaultDescription
upstream_connect_timeout_secs10TCP connect timeout to upstream
upstream_request_timeout_secs120End-to-end timeout per upstream request
max_request_body_bytes10485760 (10 MiB)Max HTTP/3 request body from clients
max_upstream_response_body_bytes52428800 (50 MiB)Max body read from upstream
max_idle_connections_per_upstream16Pooled idle TCP connections per upstream
graceful_shutdown_secs30Max 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 upstream host:port.
  • upstreams — List of host:port values; 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 (key in 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:

  1. Stop accepting new QUIC connections and notify the metrics / HTTPS sidecars to exit their accept loops.
  2. Wait for existing QUIC traffic to finish, up to limits.graceful_shutdown_secs (default 30s).
  3. 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 per host: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 (or debug). Default filter in the binary includes qrux=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.