Files
reverse-proxy/AGENTS.md
glm-5.1 c8ab794ef3 Add LICENSE, README, AGENTS.md, and deployment setup guide
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.
2026-06-12 11:42:08 +00:00

6.4 KiB
Raw Blame History

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 ArcSwap for 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 /health on 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_addr must be explicit (no 0.0.0.0) unless allow_wildcard_bind is enabled via config or --allow-wildcard-bind CLI flag (OR logic)
  • Site host values must be unique across all listeners
  • upstream must be in host:port format (e.g., gitea:3000, 127.0.0.1:3000)
  • ACME mode requires acme_domains (non-empty) and acme_contact (valid mailto: URI)
  • Manual mode requires cert_path and key_path pointing to readable files
  • rate_limit.requests_per_second and rate_limit.burst must be > 0
  • body.limit_bytes must be > 0
  • http_port must be 0 (disabled) or 165535; https_port must be 165535
  • health_check_port must 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 anyhow for application code and thiserror for library error types
  • Structured logging with tracing — always with_ansi(false)
  • Config types implement serde::Deserialize for TOML parsing
  • All network operations use tokio async runtime

Deployment Files

deploy/ contains production-ready deployment configs:

  • Dockerfile — multi-stage build (rust:alpine → alpine)
  • docker-compose.yml — complete setup with Gitea example
  • reverse-proxy.service — systemd unit file with security hardening
  • fail2ban/ — 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.