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)
3.1 KiB
ADR-023: HTTP/2 Client-Facing Support
Status
Accepted
Context
The original architecture spec excluded HTTP/2 proxying from scope, stating "HTTP/2 or HTTP/3 proxying (services that need these run their own native Rust servers)." This was interpreted as excluding HTTP/2 entirely — both for client connections and upstream connections.
During deployment testing, we discovered that modern browsers and HTTP clients
negotiate HTTP/2 via ALPN during the TLS handshake. The initial implementation
used hyper_util::server::conn::auto::Builder which failed to properly detect
HTTP/2 over TLS connections because its ReadVersion mechanism doesn't work
reliably with tokio-rustls TlsStream wrappers.
This caused two problems:
- HTTP/2 clients received degraded performance (no multiplexing) or connection failures
- In HTTP/2, the host is conveyed via the
:authoritypseudo-header, which hyper represents as the URI host rather than aHostheader — causing 400 errors for HTTP/2 clients
Decision
The proxy now supports HTTP/2 on the client-facing side (between the client and the proxy). This is distinct from HTTP/2 proxying to upstream services, which remains out of scope.
Implementation:
-
ALPN-based protocol detection: After the TLS handshake, the proxy reads the negotiated ALPN protocol from
tls_stream.get_ref().1.alpn_protocol(). If the ALPN ish2, the connection useshyper::server::conn::http2::Builder; otherwise, it useshyper_util::server::conn::auto::Builderwith HTTP/1.1 + upgrade support. -
Host header fallback: The proxy handler now falls back to
req.uri().host()when theHostheader is absent. In HTTP/2, the:authoritypseudo-header is represented as the URI host in hyper, so this correctly handles both HTTP/1.1 (whereHostis always present) and HTTP/2 (where:authoritymaps to URI host). -
ALPN advertisement: The TLS
ServerConfigadvertisesh2andhttp/1.1as ALPN protocols, plusacme-tls/1for ACME challenges.
Upstream connections remain HTTP/1.1. The proxy communicates with upstream
services over HTTP/1.1 (or HTTPS/1.1 when upstream_scheme = "https"). HTTP/2
to upstreams is out of scope for Phase 1.
Consequences
Positive:
- Modern browsers and HTTP/2 clients work correctly with the proxy
- HTTP/2 multiplexing improves client-facing performance (multiple requests over a single connection)
- ALPN-based detection is the standard mechanism for HTTP/2 negotiation over TLS
- Host header fallback correctly handles both HTTP/1.1 and HTTP/2
Negative:
- Slightly more complex TLS listener code (ALPN protocol detection, dual builder paths)
- The distinction between "HTTP/2 to the proxy" and "HTTP/2 to upstream" must be clearly documented to avoid confusion
ConnectInfoServiceis typed toRequest<Incoming>rather than the genericRequest<B>, which is a correct but slightly less flexible implementation
References
- proxy.md — request flow and host-based routing
- tls.md — TLS termination and ALPN configuration
- overview.md — scope and out-of-scope items