Dual MIT/Apache-2.0 license, public-facing README with quick start and config reference, step-by-step deploy/README.md for Docker and systemd setups, and AGENTS.md for LLM-assisted development.
6.4 KiB
AGENTS.md
Guidance for LLM agents (and humans) working on this project.
Project Overview
A memory-safe reverse proxy built with Rust/axum that replaces vulnerable nginx
installations. Terminates TLS, routes requests by Host header to upstream
services, enforces rate limits, and injects proxy headers. See README.md and
docs/architecture/ for full details.
Build & Run
cargo build # debug build
cargo build --release # release build
cargo test # run all tests (unit + integration)
cargo test -- --nocapture # run tests with stdout visible
cargo clippy # lint
reverse-proxy --config config.toml # run (defaults to /etc/reverse-proxy/config.toml)
reverse-proxy --validate --config config.toml # validate config only
For a static binary with no libc dependency:
cargo build --release --target x86_64-unknown-linux-musl
Project Structure
src/
├── main.rs # Entry point, server startup, listener binding
├── cli.rs # CLI parsing (clap), config loading, validation
├── lib.rs # Library root, module declarations
├── config/
│ ├── static_config.rs # StaticConfig — immutable, requires restart
│ ├── dynamic_config.rs# DynamicConfig — hot-reloadable via ArcSwap
│ ├── validation.rs # Config validation rules (called at startup and reload)
│ └── test_fixtures.rs # Test config generation helpers
├── proxy/
│ ├── handler.rs # Core reverse proxy handler (forward requests to upstream)
│ ├── headers.rs # Proxy header injection (X-Real-IP, X-Forwarded-For, etc.)
│ ├── body_limit.rs # Request body size limiting middleware
│ ├── error.rs # Error response types (502, 504, 429, etc.)
│ └── mod.rs # Router construction, client creation
├── tls/
│ ├── acceptor.rs # TLS acceptor setup (manual + ACME)
│ ├── acme.rs # ACME certificate provisioning via rustls-acme
│ ├── config.rs # TLS ServerConfig construction, cipher suites
│ └── redirect.rs # HTTP → HTTPS 301 redirect listener
├── rate_limit/
│ ├── mod.rs # Rate limiting middleware, eviction task
│ └── bucket.rs # Token bucket implementation (IPv4 /32, IPv6 /64)
├── admin/
│ ├── socket.rs # Unix domain socket admin API (reload, status)
│ └── mod.rs
├── health.rs # Health check endpoint on localhost:9900
├── logging/
│ ├── mod.rs # Logging init (file + stdout, ANSI disabled)
│ └── format.rs # Structured log format (REQUEST, RATE_LIMIT, etc.)
├── server.rs # HTTPS listener serving with ALPN detection
├── shutdown.rs # Graceful shutdown (SIGTERM, SIGINT) + SIGHUP reload
└── utils.rs # Shared utilities
Key Architecture Concepts
- StaticConfig vs DynamicConfig: Static config (bind addresses, TLS,
ports) requires a restart. Dynamic config (sites, rate limits, body limits)
can be reloaded at runtime via SIGHUP or admin socket, using
ArcSwapfor lock-free reads. - Multi-listener:
[[listeners]]in TOML — each listener has its own bind address, TLS config, and site routing. Sites are collected into a global routing table at runtime. - Edge proxy model: The proxy is the edge — X-Forwarded-For is replaced (not appended), X-Real-IP is set from the connection's remote address.
- No
/healthon public listener: Health checking is localhost:9900 only. The main listener does not intercept any paths. - HTTP/2 client-facing only: ALPN detects h2 vs http/1.1. Upstream connections are always HTTP/1.1.
- IPv6 rate limiting: IPv6 addresses are normalized to /64 prefixes so addresses within the same /64 share a token bucket.
Config Format
TOML. See docs/architecture/config.md for full schema. Key validation rules:
bind_addrmust be explicit (no0.0.0.0) unlessallow_wildcard_bindis enabled via config or--allow-wildcard-bindCLI flag (OR logic)- Site
hostvalues must be unique across all listeners upstreammust be inhost:portformat (e.g.,gitea:3000,127.0.0.1:3000)- ACME mode requires
acme_domains(non-empty) andacme_contact(validmailto:URI) - Manual mode requires
cert_pathandkey_pathpointing to readable files rate_limit.requests_per_secondandrate_limit.burstmust be > 0body.limit_bytesmust be > 0http_portmust be 0 (disabled) or 1–65535;https_portmust be 1–65535health_check_portmust not conflict with any listener's http_port or https_port on the same bind address
Testing
Tests use rcgen for self-signed certificate generation and reqwest for
HTTP client requests. Integration tests are in tests/integration_test.rs
with helpers in tests/helpers/.
cargo test # all tests
cargo test --test integration # integration tests only
cargo test --lib # unit tests only
Code Style
- No comments unless explicitly requested
- Error handling uses
anyhowfor application code andthiserrorfor library error types - Structured logging with
tracing— alwayswith_ansi(false) - Config types implement
serde::Deserializefor TOML parsing - All network operations use
tokioasync runtime
Deployment Files
deploy/ contains production-ready deployment configs:
Dockerfile— multi-stage build (rust:alpine → alpine)docker-compose.yml— complete setup with Gitea examplereverse-proxy.service— systemd unit file with security hardeningfail2ban/— filter and jail config for rate limit log parsing
See deploy/README.md for step-by-step setup instructions.
Common Modifications
Adding a new site
Add a [[listeners.sites]] entry to config and reload:
echo "reload" | socat - UNIX-CONNECT:/run/reverse-proxy/admin.sock
Changing rate limits
Update [rate_limit] in config and reload (no restart needed).
Changing bind address or TLS config
These are in StaticConfig — require a full process restart.
Adding per-site timeouts
Set upstream_connect_timeout_secs and upstream_request_timeout_secs on a
site definition. Defaults are 5s connect, 60s request.