4 releases (2 breaking)
Uses new Rust 2024
| new 0.3.0 | May 13, 2026 |
|---|---|
| 0.2.0 | May 13, 2026 |
| 0.1.1 | Feb 25, 2026 |
| 0.1.0 | Feb 13, 2026 |
#254 in HTTP server
610KB
14K
SLoC
pyx
A high-performance reverse proxy written in Rust, designed as a drop-in replacement for h2o with configuration compatibility.
Features
- Protocol Support: HTTP/1.1, HTTP/2 (end-to-end via ALPN), HTTP/3 (QUIC), TLS 1.2/1.3
- Reverse Proxy: Connection pooling, header manipulation, X-Forwarded-* injection, streaming
- TCP Proxy: Layer 4 proxying with weighted load balancing and health checks
- Static Files: Directory listing, gzip pre-compression, range requests, ETag support
- TLS: SNI-based certificate selection, wildcard certificates, session resumption, transparent upgrade
- Load Balancing: Weighted distribution, latency-aware routing, active health monitoring
- Security: Request body limits, directory traversal protection, rate limiting
Installation
cargo build --release
Binary will be at target/release/pyx.
Usage
pyx [OPTIONS]
Options:
-c, --config <CONFIG> Configuration file [default: /etc/pyx/pyx.yaml]
-l, --log-level <LEVEL> Log level: trace, debug, info, warn, error [default: info]
--json-logs Enable JSON structured logging
-t, --test Test configuration and exit
-w, --workers <WORKERS> Worker threads [default: auto-detect]
-h, --help Print help
-V, --version Print version
Configuration
pyx uses h2o-compatible YAML configuration.
Minimal Example
hosts:
"example.com:80":
listen:
host: 0.0.0.0
port: 80
paths:
"/":
proxy.reverse.url: "http://localhost:8080"
Full Example
# Global settings
num-threads: 4
pid-file: /var/run/pyx.pid
# Protocol settings
http2-enabled: ON
http2-idle-timeout: 180
http2-max-concurrent-streams: 256
http3-enabled: OFF
# Request limits
limit-request-body: 10485760
# Proxy defaults
proxy.preserve-host: ON
proxy.timeout.io: 30000
# Static file settings
file.send-gzip: ON
hosts:
"example.com:443":
listen:
host: 0.0.0.0
port: 443
ssl:
certificate-file: /etc/ssl/certs/example.pem
key-file: /etc/ssl/private/example.key
minimum-version: "TLSv1.2"
paths:
"/":
proxy.reverse.url: "http://backend:8080"
header.set:
- "X-Forwarded-Proto: https"
"/static/":
file.dir: /var/www/static
file.index:
- index.html
expires: "7 days"
"/api/":
proxy.reverse.url: "http://api-backend:3000"
proxy.preserve-host: OFF
header.set:
- "Cache-Control: no-store"
TCP Proxy with Load Balancing
hosts:
"tcp.example.com:3306":
listen:
host: 0.0.0.0
port: 3306
type: tcp
backends:
- host: "db1.internal"
port: 3306
weight: 100
- host: "db2.internal"
port: 3306
weight: 50
health:
interval: 5000
timeout: 2000
unhealthy-threshold: 3
healthy-threshold: 3
latency-aware: ON
Header Manipulation
# Set header (overwrite if exists)
header.set:
- "X-Custom: value"
# Set only if header doesn't exist
header.setifempty:
- "X-Powered-By: pyx"
# Append to existing header
header.merge:
- "Cache-Control: public"
# Remove header
header.unset:
- "Server"
- "X-Powered-By"
Configuration Reference
| Setting | Default | Description |
|---|---|---|
num-threads |
CPU count | Worker thread count |
pid-file |
(none) | PID file path |
limit-request-body |
10485760 | Max request body size (bytes) |
file.send-gzip |
OFF | Serve pre-compressed .gz files |
proxy.preserve-host |
OFF | Preserve Host header when proxying |
proxy.timeout.io |
30000 | Proxy I/O timeout (ms) |
http2-enabled |
ON | Enable HTTP/2 |
http2-idle-timeout |
180 | HTTP/2 idle timeout (seconds) |
http2-max-concurrent-streams |
256 | Max streams per connection |
http2-initial-stream-window |
1048576 | HTTP/2 stream window size (bytes) |
http2-initial-connection-window |
2097152 | HTTP/2 connection window size (bytes) |
http2-max-frame-size |
16384 | HTTP/2 max frame size (bytes) |
http3-enabled |
OFF | Enable HTTP/3 (QUIC) |
http3-idle-timeout |
30 | HTTP/3 idle timeout (seconds) |
http3-max-concurrent-streams |
256 | HTTP/3 max streams per connection |
Routing
Routes use longest-prefix matching. More specific paths take priority:
paths:
"/": # Catch-all
"/api": # Matches /api, /api/users, /api/v1
"/api/v2": # Matches /api/v2, /api/v2/users (takes priority over /api)
"/static/": # Only matches paths starting with /static/
Path matching prevents false prefixes: /api does not match /apikey.
TLS
Basic TLS
listen:
host: 0.0.0.0
port: 443
ssl:
certificate-file: /path/to/cert.pem
key-file: /path/to/key.pem
SNI with Multiple Certificates
Configure multiple hosts on the same port. pyx selects certificates based on SNI:
hosts:
"site-a.com:443":
listen:
port: 443
ssl:
certificate-file: /certs/site-a.pem
key-file: /keys/site-a.key
paths:
"/":
proxy.reverse.url: "http://backend-a:8080"
"site-b.com:443":
listen:
port: 443
ssl:
certificate-file: /certs/site-b.pem
key-file: /keys/site-b.key
paths:
"/":
proxy.reverse.url: "http://backend-b:8080"
Automatic Let's Encrypt Certificates
Enable ACME globally, then opt in per TLS host with ssl.letsencrypt: ON.
styx serves HTTP-01 challenges internally from /.well-known/acme-challenge/,
stores certificates under cache-dir, loads cached certificates on startup,
and renews them before expiry.
letsencrypt:
enabled: ON
contact:
- mailto:admin@example.com
cache-dir: /var/lib/styx/letsencrypt
terms-of-service-agreed: ON
staging: OFF
renew-before-days: 30
check-interval-seconds: 43200
hosts:
"example.com:80":
listen:
port: 80
paths:
"/":
redirect: "https://example.com/"
"example.com:443":
listen:
port: 443
ssl:
letsencrypt: ON
paths:
"/":
proxy.reverse.url: "http://backend:8080"
certificate-file and key-file remain supported. When omitted for a
Let's Encrypt host, styx writes to cache-dir/live/<domain>/fullchain.pem
and cache-dir/live/<domain>/privkey.pem. If provisioning or renewal fails,
the error is logged and styx keeps running with any currently loaded
certificate.
Transparent TLS Upgrade (TCP Proxy)
Accept both plain and TLS connections on the same port for TCP proxy:
hosts:
"tcp.example.com:8080":
listen:
host: 0.0.0.0
port: 8080
type: tcp
tls:
certificate-file: /path/to/cert.pem
key-file: /path/to/key.pem
transparent-upgrade: ON
backends:
- host: "backend.internal"
port: 8080
Static File Serving
paths:
"/static/":
file.dir: /var/www/static
file.index:
- index.html
- index.htm
file.dirlisting: ON
expires: "30 days"
Features:
- Automatic MIME type detection
- Serves
.gzfiles whenAccept-Encoding: gzipis present - ETag and If-None-Match support
- Range request support (partial content)
- Directory traversal protection
Health Checks
For TCP proxy backends:
health:
interval: 5000 # Check every 5 seconds
timeout: 2000 # 2 second timeout per check
unhealthy-threshold: 3 # 3 failures to mark unhealthy
healthy-threshold: 3 # 3 successes to mark healthy
latency-aware: ON # Route less traffic to slow backends
sigma-threshold: 2.0 # Latency std deviation threshold
Development
# Build
cargo build # Debug
cargo build --release # Release
# Test
cargo test # All tests
cargo test --lib # Unit tests
cargo test --test integration_tests
# Lint
cargo clippy
cargo fmt
# Benchmark
cargo bench
cargo bench --bench routing
cargo bench --bench proxy
License
See LICENSE file.
Dependencies
~131MB
~3M SLoC