Implement two-phase timeout in proxy_handler:
- Inner timeout uses per-site upstream_connect_timeout_secs (default 5s)
for the connect + first-byte phase
- Outer timeout uses upstream_request_timeout_secs (default 60s) for the
full request/response cycle
- Set connect_timeout on HttpConnector for both HTTP and HTTPS clients
(default 5s) to enforce TCP-level connect timeouts
- Use wrap_connector for HTTPS client to apply connect_timeout on the
underlying HttpConnector
- Add Ok(Err(_)) handler for connect timeout returning 504 Gateway Timeout
- Replace handle.abort() for HTTPS server tasks with timeout-based join,
allowing in-flight requests to drain before forceful shutdown
- Add shutdown_rx to start_admin_socket with tokio::select! for clean
accept loop exit and Unix socket file cleanup on shutdown
- Add shutdown_rx to start_eviction_task with tokio::select! for
cancellable eviction loop
- Add shutdown channel to spawn_acme_state for cancellable ACME state
machine via tokio::select!
- Pass Arc<GracefulShutdown> through setup_tls to ACME state machine
- Move GracefulShutdown creation before admin socket and TLS setup
- Update integration test for new start_eviction_task signature
The main code changes were already committed (3f2550f), but test config
TOML strings in cli.rs, admin/socket.rs, shutdown.rs, and
integration_test.rs still needed the new acme_contact field to pass
validation rule 19.
Add log_request! calls for every proxied request (success, 4xx/5xx from
upstream, 502/504 errors) and log_upstream_error! calls for upstream
connection failures and timeouts. Duration is tracked from request entry
to response using std::time::Instant.
- Remove health_handler and /health early return from proxy_handler
- Remove /health route from proxy_router (now just fallback)
- Remove is_https field from ProxyState struct
- Remove is_https parameter from inject_proxy_headers, hardcode https
- Add comment explaining why X-Forwarded-Proto is always https
- Remove health_path_returns_200 and health_with_unknown_host tests
- Update all inject_proxy_headers test calls to remove is_https param
- Remove inject_proxy_headers_sets_x_forwarded_proto_http test
Extract strip_port_from_host into shared utils module and update normalize_host to properly strip brackets from IPv6 addresses like [::1]:443 -> ::1 instead of incorrectly using split(':').next().
Change ConfigReloadHandle.static_config from StaticConfig to ArcSwap<StaticConfig>
so that after each reload, the stored static config is updated with the new value.
This prevents repeated stale warnings about the same static config fields on
every reload.
The init_creates_log_directory_and_file test called init() which sets a
global tracing subscriber. When tests run in parallel, other tests may
have already set the subscriber, causing init() to return an error and
the test to fail. Now the test tolerates the 'already set' error while
still asserting the log file is created.
Change http_port type from u16 to u32 to allow out-of-range values to be
caught by validation. Add HttpPortInvalid error variant and validation check
for http_port > 65535. Add test for http_port=65536 producing HttpPortInvalid.
http_port=0 (disabled) remains valid per existing test.
- Replace determine_if_https() with ProxyState.is_https field so X-Forwarded-Proto
reflects the listener's protocol instead of guessing from the Host header
- Return ProxyError::BadGateway with host/upstream context for non-connect upstream
errors instead of bare StatusCode::BAD_GATEWAY
- Implement InFlightCounter with RAII guard for tracking in-flight connections
- Add drain_in_flight() to wait for connections to complete on shutdown, with
configurable timeout before forcing exit
- Mark review/core-components and review/integration-readiness as complete
- Add server module that orchestrates the full startup sequence:
parse config, init dynamic config, init shared state, bind health
check, bind admin socket, bind all listener ports, load TLS config,
start TCP listeners, start background tasks, signal readiness
- For each ListenerConfig: bind TCP listener, construct appropriate
ServerConfig (manual or ACME via TlsMode), create TlsAcceptor
- ConnectInfo<SocketAddr> populated from TcpStream::peer_addr() BEFORE
TLS wrapping via ConnectInfoService wrapper that inserts ConnectInfo
into request extensions for each connection
- Per-listener axum::Router instances sharing Arc<ProxyState> via State
- Fail-fast: if any bind or TLS load fails, exit with non-zero code
- All ports bound before any connections accepted
- /health endpoint available on HTTPS listener(s) as fallback
(proxy_router already includes /health route)
- sd_notify(READY=1) sent after all listeners started
- Use hyper_util for TLS connection serving with TowerToHyperService
and ConnectInfoService to bridge ConnectInfo from pre-TLS peer_addr
- Add sd-notify dependency for systemd readiness notification
- Add ProxyError enum with IntoResponse for error handling (400, 404, 502, 504)
- Implement proxy header injection: X-Real-IP, X-Forwarded-For (replaced, not appended), X-Forwarded-Proto
- Implement hop-by-hop header removal for both request and response headers
- Implement request forwarding via shared hyper::Client with HTTP and HTTPS support
- Add ProxyState with http_client and https_client instances shared via axum State
- Add per-site timeout overrides using tokio::time::timeout
- Add HTTPS upstream support with system native TLS root certificates
- No Server or Via headers added to responses
- Host header preserved as-is
- Add unit tests for header injection, hop-by-hop removal, and URI building
- Add integration tests for proxy forwarding, hop-by-hop removal, and 502 on unreachable upstream
Adds the HTTP redirect listener that redirects all plain HTTP requests to
the HTTPS equivalent URL. Each listener with http_port > 0 runs its own
redirect server on bind_addr:http_port.
- build_redirect_url: constructs https://{host}:{port}/{path}?{query},
omitting port 443 and stripping the host port from the Host header
- redirect_handler: axum handler returning 301 with Location header,
400 for missing/empty Host, 404 for ACME challenge paths
- redirect_router: creates axum Router with fallback handler
- start_http_redirect_listener: binds TCP and spawns redirect server
- ACME HTTP-01 challenge path returns 404 (placeholder for future)
- 19 unit tests for URL construction and host parsing
- 8 integration tests covering 301 redirect, 400 on missing Host,
port 443 omission, non-443 port inclusion, query preservation,
ACME challenge 404
Add admin socket module that binds to a configurable Unix domain socket
path (default /run/reverse-proxy/admin.sock) supporting reload and status
commands. Reload re-reads config and swaps DynamicConfig via ArcSwap with
serialized access using the same Mutex as SIGHUP. Status returns uptime
and site count. Unknown commands and invalid input return structured
JSON error responses. Stale socket files are removed at startup; if the
socket is occupied by another process, a warning is logged and the socket
is disabled. Empty admin_socket_path disables the socket entirely.
Also adds FullConfig struct to config module for parsing complete config
files during reload, and adds serde_json dependency for JSON responses.
- Add Cli struct with clap derive macros for --config, --validate, --allow-wildcard-bind flags
- Config loading: reads TOML, deserializes into StaticConfig + DynamicConfig, validates
- --validate: load, validate, print success/errors, exit 0 or 1
- --allow-wildcard-bind is OR'd with config allow_wildcard_bind field
- Default config path: /etc/reverse-proxy/config.toml
- Version from Cargo.toml via clap
- Unit tests for CLI argument parsing and config loading
- Integration tests for --validate with valid/invalid config and --allow-wildcard-bind
Add body_limit middleware that reads limit from ArcSwap<DynamicConfig>
on each request, enabling runtime config changes without restart.
Uses Content-Length header check for fast rejection and http_body_util::Limited
for streaming body enforcement. Default limit: 100 MB (104,857,600 bytes).
Returns 413 Payload Too Large when exceeded.
- Add TokenBucket with nodelay semantics (nginx limit_req burst nodelay)
- Per-IP rate limiting: IPv4 /32, IPv6 /64 prefix normalization
- DashMap for concurrent access, ArcSwap for lock-free config reads
- Background eviction task for stale entry cleanup
- 429 response with plain text body, RATE_LIMIT log prefix
- Config reload adopts new rate/burst on next request without clearing state
- Unit tests for bucket algorithm and IPv6 normalization
- Integration tests for 429 responses and per-IP independence
Add routing table (HashMap<String, SiteConfig>) to DynamicConfig for
O(1) host lookup. Implement normalize_host (lowercase + strip port)
per RFC 7230 §2.7.3. Add proxy_handler that routes /health to 200,
missing Host to 400, unknown host to 404, and known host to 200.
Routing table updates atomically via ArcSwap.
- Add logging::init() with dual output (file + stdout) via tracing-subscriber Layer composition
- Support configurable log level via LoggingConfig.level and JSON/text format via LoggingConfig.format
- Create log file and parent directories when log_file_path is configured
- Add KvVisitor for custom key=value event field formatting
- Add log_request!, log_rate_limit!, log_upstream_error!, log_config_reload! macros
with REQUEST, RATE_LIMIT, UPSTREAM_ERROR, CONFIG_RELOAD prefixes
- Add format_event_fields() for extracting structured fields from tracing events
- Add tracing-subscriber env-filter and json features to Cargo.toml
- Add unit tests for KvVisitor formatting, log macros, and init function
- Apply cargo fmt to existing tls/config.rs tests
Add ConfigReloadHandle with Arc<ArcSwap<DynamicConfig>> for lock-free reads
on the request hot path and tokio::sync::Mutex-serialized reload. Add static
config change detection via diff_static_config(). Add DynamicConfig validation
(rate_limit, body_limit, site checks). Add PartialEq derives to config types.
Include unit tests for ArcSwap swap visibility, invalid config rejection, and
concurrent reload serialization.
- Add health.rs module with start_health_check_listener() that binds to
127.0.0.1:{health_check_port} and serves GET /health returning 200 OK
with empty body
- Add health_route() in proxy/handler.rs for HTTPS listener fallback
- Add port conflict detection in config validation: health_check_port
must not conflict with listener ports on 127.0.0.1/localhost/0.0.0.0
- health_check_port = 0 disables the separate listener (handled at call
site)
- Add unit and integration tests for health check functionality
Add ACME TLS module with automatic Let's Encrypt certificate provisioning
and renewal using rustls-acme 0.12. Each listener creates its own AcmeConfig
with domain list, cache directory, and Let's Encrypt directory URL. The ACME
state machine runs as a background tokio task per listener, and
ResolvesServerCertAcme serves the provisioned certificate. Certificate
failure behavior: fail to start without valid cert, continue serving if one
exists. TLS-ALPN-01 is the default challenge type with acme-tls/1 ALPN
registered. Cipher suites restricted to 4 TLS 1.2 + all TLS 1.3 suites.
Also implements manual TLS mode with PEM file loading, SNI-based cert
resolution, and shared CryptoProvider with restricted cipher suites.
- Add [lib] target to enable integration test imports
- Add rcgen and reqwest dev-dependencies for TLS and HTTP test helpers
- Create src/config/test_fixtures.rs with test_static_config() and test_dynamic_config()
- Create tests/ with integration tests, HTTP test helper (TestUpstream), and TLS test helper (SelfSignedCert)
- Add Clone derives to StaticConfig and related structs for test fixture construction
- All existing tests continue to pass
Add Clone derive to StaticConfig, ListenerConfig, TlsConfig, and
LoggingConfig to support immutable-after-startup pattern. Add unit
tests verifying TOML deserialization for multi-config (dedicated-IP)
and shared-IP (SAN certificate) deployment formats, default value
application, logging config, and site defaults.