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)
This commit is contained in:
2026-06-12 11:28:31 +00:00
parent c2eefddb4f
commit 0d54eba41e
11 changed files with 313 additions and 66 deletions

View File

@@ -0,0 +1,75 @@
# 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:
1. HTTP/2 clients received degraded performance (no multiplexing) or connection
failures
2. In HTTP/2, the host is conveyed via the `:authority` pseudo-header, which
hyper represents as the URI host rather than a `Host` header — 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:**
1. **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 is `h2`, the connection uses
`hyper::server::conn::http2::Builder`; otherwise, it uses
`hyper_util::server::conn::auto::Builder` with HTTP/1.1 + upgrade support.
2. **Host header fallback**: The proxy handler now falls back to
`req.uri().host()` when the `Host` header is absent. In HTTP/2, the
`:authority` pseudo-header is represented as the URI host in hyper, so this
correctly handles both HTTP/1.1 (where `Host` is always present) and HTTP/2
(where `:authority` maps to URI host).
3. **ALPN advertisement**: The TLS `ServerConfig` advertises `h2` and
`http/1.1` as ALPN protocols, plus `acme-tls/1` for 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
- `ConnectInfoService` is typed to `Request<Incoming>` rather than the generic
`Request<B>`, which is a correct but slightly less flexible implementation
## References
- [proxy.md](../proxy.md) — request flow and host-based routing
- [tls.md](../tls.md) — TLS termination and ALPN configuration
- [overview.md](../overview.md) — scope and out-of-scope items