Commit Graph

63 Commits

Author SHA1 Message Date
b0f83669c0 fix(proxy): raise connector timeout ceiling to 30s per ADR-026 2026-06-12 14:15:00 +00:00
e6d22bdcb8 Merge remote-tracking branch 'origin/fix/fix/rate-limiter-ip-source' 2026-06-12 14:01:16 +00:00
ad9b9b9b78 fix(rate_limit): use ConnectInfo as sole IP source, reject without it
The rate limiter previously extracted client IP from the X-Forwarded-For
header first, falling back to ConnectInfo. This allowed attackers to bypass
rate limits by sending spoofed X-Forwarded-For headers. Per ADR-025, the
rate limiter now uses ConnectInfo<SocketAddr> exclusively and rejects
requests with 429 when ConnectInfo is absent.
2026-06-12 14:00:31 +00:00
77ea1160de Merge remote-tracking branch 'origin/fix/fix/consolidate-config-types' 2026-06-12 14:00:10 +00:00
1ba1d2a4de Consolidate config types: remove RawConfig, use FullConfig in load_config
Delete the duplicate RawConfig struct and collect_sites helper from cli.rs.
Rewrite load_config to use FullConfig::parse + into_static_and_dynamic,
eliminating the redundant manual construction path.
2026-06-12 13:58:36 +00:00
05fea1a8e2 Fix InFlightCounter: increment in new(), use new() constructor, drain interval 100ms 2026-06-12 13:58:04 +00:00
0d54eba41e Update architecture specs to reflect live deployment findings and fix two bugs
Architecture updates based on gaps discovered during live deployment testing:

- ADR-023: HTTP/2 client-facing support via ALPN-based protocol detection.
  The spec previously said HTTP/2 was out of scope, but the deployment
  revealed that modern browsers negotiate HTTP/2 via ALPN. The proxy now
  correctly detects the negotiated ALPN protocol and uses the appropriate
  HTTP server builder (http2::Builder for h2, auto::Builder for http/1.1).
  Upstream connections remain HTTP/1.1. Host resolution now falls back to
  URI host for HTTP/2 :authority pseudo-headers.

- ADR-024: ANSI-disabled logging. All tracing-subscriber layers now use
  with_ansi(false) to prevent ANSI escape codes in log output, which broke
  fail2ban regex matching in Docker deployments. Also documents the fail2ban
  regex anchor fix (^RATE_LIMIT → RATE_LIMIT).

Bug fixes found by architecture review:

- Fix missing ALPN protocols in manual TLS mode. build_manual_server_config
  and build_multi_domain_server_config did not set alpn_protocols, meaning
  manual TLS mode could not support HTTP/2. Added h2 and http/1.1 ALPN
  entries to both functions (acme-tls/1 only in ACME mode).

- Fix missing with_ansi(false) in JSON log format. The init_json function
  with file output did not disable ANSI on stdout or file layers, which would
  break fail2ban in production JSON logging mode.

Other spec updates:

- All document statuses updated from draft to reviewed
- proxy.md: documented Server header removal, upstream HTTPS client,
  two-phase timeout enforcement, HTTP/2 host resolution, connect timeout
- tls.md: documented ALPN configuration differing by mode (ACME vs manual)
- overview.md: added HTTP/2 client-facing support to scope, updated crate
  deps (hyper-rustls, rustls-native-certs, hyper-util), clarified out-of-scope
- config.md: fixed http_port type (u16→u32) to match implementation, added
  ANSI-disabled note for LoggingConfig
- operations.md: documented ANSI-disabled logging, fail2ban regex anchor
- open-questions.md: updated OQ-09 resolution (connect timeout fully
  implemented), OQ-10 (C2 bug is fixed)
2026-06-12 11:28:31 +00:00
c2eefddb4f Disable ANSI colors in logs and fix fail2ban regex
- Add with_ansi(false) to all tracing_subscriber fmt layers so log
  output (both stdout and file) is plain text without escape codes.
  This is critical for Docker deployments and fail2ban log parsing.

- Remove ^ anchor from fail2ban failregex since log lines have a
  timestamp/level prefix before RATE_LIMIT.
2026-06-12 10:15:50 +00:00
9ebb8ee7a8 Fix HTTP/2 support: use ALPN-based protocol detection and fallback to URI host
Two changes to properly support HTTP/2 clients:

1. server.rs: Detect ALPN protocol after TLS handshake and use
   hyper::server::conn::http2::Builder for H2 connections instead
   of the auto::Builder which failed to detect HTTP/2 over TLS.
   The auto::Builder's ReadVersion mechanism doesn't work reliably
   with tokio-rustls TlsStreams. For H1 connections, continue using
   auto::Builder with upgrade support.

2. handler.rs: Fallback to URI host when Host header is missing.
   In HTTP/2, the host is conveyed via :authority pseudo-header which
   hyper represents as the URI host, not a Host header.
2026-06-12 06:14:46 +00:00
cfba7491ae Merge branch 'fix/fix/clean-dead-code' 2026-06-12 05:12:47 +00:00
cbcd746c9f Remove dead_code annotations and add #[non_exhaustive] to public enums
All #[allow(dead_code)] annotations on now-used items have been removed
(acceptor.rs, acme.rs, config.rs, static_config.rs). #[non_exhaustive]
added to TlsMode, ProxyError, AdminSocketError, and ValidationError
with wildcard match arms in main.rs for the non-exhaustive enums.
2026-06-12 05:12:32 +00:00
9b3fe23499 Add clarifying comments for correct-but-non-obvious behaviors (C3, W8, W10, W11, S9) 2026-06-12 05:05:10 +00:00
0c769e682e Wire upstream_connect_timeout_secs to enforce separate connect timeout
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
2026-06-12 05:01:54 +00:00
6cb0f8e6fe Merge branch 'fix/fix/graceful-shutdown' into fix/acme-contact-and-challenge 2026-06-12 04:59:32 +00:00
280fe782a1 Implement graceful shutdown for listeners, admin socket, eviction task, and ACME
- 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
2026-06-12 04:59:18 +00:00
9bdc2b72af Add acme_contact to test config TOML strings
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.
2026-06-12 04:48:25 +00:00
3f20c9d01f Add request timeout scope comment (fix/request-timeout-scope) 2026-06-12 04:47:06 +00:00
5529cf2add Merge branch 'fix/fix/access-logging'
# Conflicts:
#	src/proxy/handler.rs
2026-06-12 04:46:26 +00:00
4cdc3aa0b8 Merge branch 'fix/fix/remove-health-and-hardcode-https' 2026-06-12 04:44:54 +00:00
3f2550fa20 Fix ACME contact email wiring and remove unused challenge config 2026-06-12 04:44:41 +00:00
23c8b74058 Wire up access logging in proxy handler
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.
2026-06-12 04:43:59 +00:00
a826106673 Remove /health route from main listener and hardcode X-Forwarded-Proto to https
- 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
2026-06-12 04:43:59 +00:00
f59a86a8cf Merge branch 'fix/fix/normalize-host-ipv6' 2026-06-12 04:41:20 +00:00
42c721e954 fix: normalize_host handles IPv6 bracket notation
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().
2026-06-12 04:40:43 +00:00
a78e3bf374 Fix ConfigReloadHandle static config drift causing stale diff warnings
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.
2026-06-12 04:35:20 +00:00
634ceb365a Merge branch 'fix/fix/logging-test-global-subscriber' 2026-06-12 04:29:40 +00:00
667495cf43 fix(logging): handle global subscriber conflict in test
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.
2026-06-12 04:29:28 +00:00
d24148dae9 Add http_port range validation (0 or 1-65535)
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.
2026-06-12 04:28:35 +00:00
8f9e3b639d Merge branch 'fix/fix/fragile-error-detection' 2026-06-12 04:25:35 +00:00
067f8a9012 fix: use typed hyper::Error::is_incomplete_message() instead of fragile string matching 2026-06-12 04:25:11 +00:00
a701c82c90 fix: use nanosecond precision in token bucket refill calculation 2026-06-12 04:21:53 +00:00
cf002cc40f Fix spec deviations and implement graceful shutdown drain
- 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
2026-06-11 14:01:55 +00:00
7bed7db615 Wire startup orchestration: correct sequence, middleware order, TLS, ConnectInfo, sd_notify
Consolidate startup logic into main.rs following operations.md sequence:
1. Parse/validate config, 2. Init DynamicConfig ArcSwap, 3. Init shared state
(rate limiter, clients, logging), 4. Bind health check port, 5. Bind admin
socket, 6. Bind all listener ports (HTTP+HTTPS), 7. Load TLS config,
8. Start TCP listeners, 9. Start background tasks, 10. Signal readiness

Key changes:
- main.rs: Complete startup orchestration with proper sequence, TLS handling,
  ConnectInfo propagation, sd_notify, graceful shutdown
- server.rs: Simplified to just serve_https_listener with shutdown support
- proxy/mod.rs: Added build_router() with correct middleware order
  (rate limiting → body limit → routing → proxy handler)
2026-06-11 13:45:39 +00:00
e0f7e100d9 Merge feat/tls/tls-listener-setup into main 2026-06-11 13:40:04 +00:00
7ccb2ae64f feat: implement multi-listener TLS setup with ConnectInfo propagation
- 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
2026-06-11 13:38:39 +00:00
78a518acd4 Implement signal handling and graceful shutdown
- Add GracefulShutdown struct with watch channel for shutdown signaling
- Handle SIGTERM/SIGINT via signal-hook to trigger graceful shutdown
- Handle SIGHUP via signal-hook for config reload (same code path as admin socket)
- Implement graceful shutdown sequence: stop accepting -> drain -> force-close -> cancel tasks -> exit 0
- Wire up main.rs with full server startup (health check, admin socket, HTTP redirect, HTTPS proxy)
- Add integration tests for GracefulShutdown and SIGHUP reload
- shutdown_timeout_secs configurable in StaticConfig (default 30)
2026-06-11 13:33:26 +00:00
388523d6fe Merge feat/proxy/headers-and-forwarding into main 2026-06-11 13:24:40 +00:00
9abae2093b Merge feat/ops/admin-socket into main 2026-06-11 13:20:23 +00:00
b9126a96f4 Implement proxy header injection, hop-by-hop removal, and request forwarding
- 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
2026-06-11 13:18:56 +00:00
c25d19c63f Merge feat/tls/http-redirect into main 2026-06-11 13:18:46 +00:00
f280a04d4b Remove accidentally staged worktree dirs 2026-06-11 13:16:45 +00:00
d893187c40 Implement HTTP to HTTPS redirect with per-listener binding
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
2026-06-11 13:14:27 +00:00
56eda4e47c feat: implement Unix domain socket admin API for config reload and status
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.
2026-06-11 13:13:15 +00:00
fe5b32b25c Merge remote-tracking branch 'origin/feat/proxy/error-responses' 2026-06-11 13:13:12 +00:00
d89ab71f85 Implement CLI argument parsing with clap and config file loading
- 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
2026-06-11 13:12:28 +00:00
24a7f9ed86 Merge feat/ops/body-size-limit into main 2026-06-11 13:12:27 +00:00
23ed5cde27 Implement ProxyError enum with plain text error responses and logging 2026-06-11 13:10:32 +00:00
5fa0fc600e Implement body size limit middleware with dynamic config reload
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.
2026-06-11 13:02:59 +00:00
2791070971 Implement token bucket rate limiting with IPv6 /64 normalization
- 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
2026-06-11 13:01:25 +00:00
d5f5713deb Implement host-based routing with global routing table
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.
2026-06-11 12:57:31 +00:00