Files
reverse-proxy/AGENTS.md
glm-5.1 0f4e9d596f Update docs: reviews #005/#006 resolved status, AGENTS.md admin refactor
- Review #005: update W2/W5/W6 rows to show resolved status
- Review #005: update reviewed_code to reference new admin files
- Review #006: update status from draft to resolved
- Review #006: update Category 4 (SIGHUP/admin reload) for mtime check
- Review #006: update Category 6 (admin interface) for HTTP API
- Review #006: update P4 and reviewed_code for new file paths
- AGENTS.md: update project structure (admin/, config/mod.rs)
- AGENTS.md: update architecture concepts (admin HTTP, TOCTOU, wildcard)
- AGENTS.md: update config format (admin_key_path)
- AGENTS.md: update testing (admin HTTP tests)
- AGENTS.md: update common modifications (curl instead of socat)
2026-06-15 06:18:12 +00:00

7.6 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/
│   ├── mod.rs            # ReloadError, read_and_validate_config(), FullConfig
│   ├── 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/
│   ├── auth.rs         # Bearer token auth middleware (subtle, SHA-256)
│   ├── handler.rs      # HTTP handlers for /admin/reload, /status, /rotate-key
│   └── mod.rs          # Re-exports
├── 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 HTTP API, 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.
  • Admin HTTP API: Authenticated Bearer token endpoints on the health check port (admin_key_path). Empty admin_key_path disables admin endpoints (returns 404). Key file contains plaintext token, read once at startup.
  • 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 reload TOCTOU: Both SIGHUP and admin HTTP reload paths use read_and_validate_config() which checks file mtime before and after reading. If mtime changed, reload is rejected with a retry message.
  • Wildcard bind consistency: cli_allow_wildcard_bind is stored in ConfigReloadHandle and used for both startup and reload validation.

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
  • admin_key_path must be an absolute path or empty (empty disables admin); path traversal (..) is rejected; default is /etc/reverse-proxy/admin-key

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/. Admin endpoint tests use reqwest HTTP client with Bearer token authentication.

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:

curl -X POST -H "Authorization: Bearer $ADMIN_KEY" http://127.0.0.1:9900/admin/reload

Checking proxy status

curl -H "Authorization: Bearer $ADMIN_KEY" http://127.0.0.1:9900/admin/status

Rotating the admin key

curl -X POST -H "Authorization: Bearer $ADMIN_KEY" http://127.0.0.1:9900/admin/rotate-key

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.