Files
RustProxy/docs/HTTP3.md
T
2026-10-07 22:22:54 +02:00

9.7 KiB

HTTP/3 transport

Uncompiled development source in this environment. This release adds an actual QUIC/HTTP/3 adapter, not only an Alt-Svc header. It does not claim independent full-RFC conformance, interoperability or production performance until the acceptance tests run.

Stack and configuration

The adapter delegates QUIC transport/TLS to Quinn 0.11.12 and HTTP/3/QPACK plus the Quinn adapter to hpx-h3 2.5.19 with its quinn feature. The crate is imported as h3 in source. These are Cargo dependencies, not bundled binaries. Rustls provides TLS 1.3 for QUIC; SNI certificates come from the existing inventory. HTTP/1.1 and optional HTTP/2 remain on TCP at the same port. HTTP/3 is on UDP. There is no HTTP/3 upstream client in this revision: origins still use HTTP/1.1 or HTTP/2.

Each vhost has http3_enabled (default true). When false, RustProxy removes Alt-Svc from responses for that vhost and rejects HTTP/3 requests for it with 421, while HTTP/1.1 and any permitted HTTP/2 traffic on the selected TLS listener keep working. The UDP listener is still a listener-level resource; the vhost switch narrows use of that shared capability rather than hot-unbinding the UDP socket.

HTTP/2 follows the same UI model through http2_enabled (default true). A vhost may only opt into H2 when one of its selected named listeners enables H2. For TLS, RustProxy reads ClientHello/SNI before finishing the handshake and selects H2+HTTP/1.1 or HTTP/1.1-only ALPN for that vhost. The listener remains the capability ceiling.

Since 0.5.0, an omitted http3 enables QUIC with defaults on TLS bindings. Explicit http3: null disables it and is preserved when serialized. An object enables it with chosen limits. Plain HTTP defaults to no QUIC. In 0.24.3 the Enable HTTP/3 over QUIC (UDP) switch is edited with the TLS endpoint under Settings -> Listeners. The listener row links to Settings -> HTTP connections -> HTTP/3, which contains only advertisement, connection, stream and idle-timeout tuning for listeners where H3 is enabled. Both enablement and tuning remain restart-required. Configure this only on named listeners.endpoints[].binding:

{
  "address": "127.0.0.1:8443",
  "tls": true,
  "http2": true,
  "http3": {
    "alt_svc_max_age": 3600,
    "max_concurrent_connections": 512,
    "max_concurrent_bidistream": 100,
    "max_concurrent_unistream": 100,
    "max_idle_timeout": 10
  }
}
Field Meaning and accepted limits
alt_svc_max_age Advertisement lifetime, seconds, 0..604800
max_concurrent_connections Established/pending accepted connection slots per endpoint, 1..4096
max_concurrent_bidistream Peer bidirectional streams per connection, 1..1024
max_concurrent_unistream Peer unidirectional streams per connection, 3..1024
max_idle_timeout QUIC idle timeout, seconds, 0..86400; zero means no local QUIC idle timeout

Three unidirectional streams are required for the peer control and QPACK streams. These stream limits are NOT global to all listeners. The existing global in-flight request and application body/header deadlines still apply. Setting QUIC idle to zero does not disable application deadlines, and the peer can negotiate its own idle timeout. All changes to socket/ALPN/QUIC transport parameters require restart.

Transport behavior and safety

  • Bind UDP before announcing readiness; failure to bind is a startup failure.
  • Advertise Alt-Svc: h3=":port"; ma=N only on an ACTUAL active TLS+H3 endpoint. Origin Alt-Svc is stripped so it cannot advertise the origin or a disabled service.
  • UDP address validation/Retry, bounded pending handshakes, a 10-second handshake deadline, per-endpoint permits, stream flow control and bounded application concurrency.
  • 32 KiB decoded HTTP field section limit; request content-length checks and rejection of forbidden connection/framing headers; streamed bodies and trailers.
  • Shared routing pipeline: real local socket -> validated authority -> trusted client address -> ACL -> Tinyauth -> cache -> origin -> response/security/Server policies.
  • HTTP cache semantics are shared with HTTP/1.1 and HTTP/2. In 0.27.3, body-less HEAD requests use the same bounded empty-body probe as GET, so an initially unpolled HTTP/3 receive stream does not cause a false request_body cache bypass.
  • 0-RTT is disabled to avoid replaying writes; no retry to another origin or transport.
  • Endpoint active migration is disabled. Missing real local destination data on a wildcard QUIC connection fails closed rather than substituting a forwarded IP.
  • Configuration reload changes SNI certificates for new QUIC handshakes without changing active socket limits. Graceful shutdown requests GOAWAY then bounded drain.
  • Counters: http3_requests, http3_connections, http3_active_connections and http3_rejected_connections in status/Prometheus; H3 request count also in dashboard.

A connection receives a maximum aggregate 8 MiB QUIC window and 256 KiB per-stream window; these limits are not process memory guarantees. Many simultaneous connections can use substantial memory. Size connection/stream limits to the machine and measure. No active path migration, 0-RTT, server push, WebTransport or HTTP datagrams are enabled. RFC 9220 WebSocket Extended CONNECT is experimental and disabled by default per vhost. RustProxy advertises SETTINGS_ENABLE_CONNECT_PROTOCOL on a QUIC listener only when at least one vhost bound to that listener explicitly enables websocket_rfc9220; request routing still rejects RFC 9220 for vhosts that did not opt in. Generic CONNECT and other Extended CONNECT protocols remain unsupported and are answered with 501. The downstream H3 request must not carry HTTP/1.1-only Connection, Upgrade, Sec-WebSocket-Key or Sec-WebSocket-Accept fields. RustProxy creates a private RFC 6455 key for the HTTP/1.1 backend handshake; a valid backend 101 becomes downstream 200, after which H3 DATA bytes are the WebSocket byte stream. Client FIN half-closes the backend write side; backend EOF finishes the H3 send side; abnormal/forced termination uses H3_REQUEST_CANCELLED. No side TCP listener or protocol bypass is used.

WebSocket bootstrap ordering and diagnostics in 0.16.0

For RFC 9220, a validated backend 101 is no longer enough to send the client-facing H3 success immediately. RustProxy first awaits Hyper's backend OnUpgrade and obtains the actual upgraded stream. Only then is downstream 200 returned and the H3 DATA bridge started. If backend takeover is cancelled or fails, the client receives a normal 503/502 response instead of 200 followed by an immediate H3_REQUEST_CANCELLED reset.

Access logging can now distinguish this path with http_version, transport, websocket_bootstrap, websocket_phase, websocket_error, h3_reset_code and upstream_status. This makes Safari/iOS interoperability failures distinguishable from backend Socket.IO responses and from H3 tunnel resets. Query-string logging remains subject to the normal privacy policy; do not use access logging to capture session secrets.

The advertised port is the real listener port. Behind port-rewriting NAT, use a matching externally reachable UDP/TCP port or do not enable this advertisement; there is no separate external-port override. Open UDP as well as TCP in your firewall/load balancer. Scope labels do not perform those operations. Clients lacking QUIC or blocked UDP can still connect using TCP; no guarantee is made about browser protocol preference.

Verification gate

bash dev.sh check
cargo test --locked --test http3 -- --nocapture
cargo test --locked --test integrations --test listeners
bash dev.sh test

tests/http3.rs creates a real loopback UDP endpoint, a trusted generated test certificate, a real origin and a Quinn+h3 client. It checks MISS/HIT, body preservation, Server/diagnostic policy, connection capacity and ALPN HTTP/2/HTTP/1.1 compatibility. The same integration test explicitly enables websocket_rfc9220, then opens an RFC 9220 Extended CONNECT stream and translates it to a real HTTP/1.1 backend Upgrade, sends a masked WebSocket frame as H3 DATA, half-closes the client send side and verifies byte-transparent DATA return plus tunnel metrics. Unit regressions separately reject HTTP/1.1-only proof/framing headers on the H3 side. It is provided but was NOT run in the packaging environment. The same-stack client is a regression test, not independent interoperability evidence.

After applying examples/http3-vhost.json on a disposable test instance and starting bash dev.sh mock --port 9001, use an independently built curl with HTTP3 support:

curl --version
curl --http3-only --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http3-only --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http2 --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http1.1 --cacert data/certs/default.pem -D - https://localhost:8443/cache

Do not substitute --http3 fallback or -k for strict protocol/certificate validation. Also test independent clients, malformed-frame handling, stream exhaustion, slow uploads/downloads, IPv6 wildcard destination metadata, cancellation, graceful drain, certificate rotation, and memory/CPU at configured maxima before exposure. No full RFC conformance or load-test result is asserted by this source package.

Primary references