189 lines
13 KiB
Markdown
189 lines
13 KiB
Markdown
# 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.20` 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`:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
## H3 full-page navigation to HTTP/1.1 backend (0.31.6)
|
|
|
|
The RustProxy logo remains a normal `href="/"` link, not a client-side panel change. On an H3-enabled frontend it produces a new document GET through the proxy. When forwarding an H3 GET/HEAD to an HTTP/1.1 backend, RustProxy now probes for the stream's end for at most 35 ms per initial frame. A finished empty request is passed to H1 with a confirmed complete receive half rather than an unknown body state. Hyper already special-cases H1 GET/HEAD framing; no claim is made that ordinary GET requests were chunked. A nonempty frame and all subsequent bytes are replayed without buffering the whole request. The slow-peer case falls back to regular streaming.
|
|
|
|
If `h3.accept()` fails, existing request tasks get the normal bounded drain instead of immediate mass abort. The new diagnostics use status=0 for cancellations (not a real HTTP response), and persist interrupted-response details including `phase=`, `stream_id=`, `method=`, `path=`, `response_status=`, `body_bytes_sent=` and `error=` in the bounded, redacted `error_detail`. These are stored per vhost when file logging is enabled; the normal access logs remain available for successful document and asset GETs.
|
|
|
|
**iOS validation procedure:** keep H3 on, click the logo repeatedly on an authenticated panel behind the real RustProxy vhost, and collect timestamps for `http3_peer_cancelled_before_headers`, `http3_peer_cancelled_after_headers`, `http3_response_write_error`, `http3_accept_error`, and the GET `/` and asset access records. Collect browser Web Inspector Network/QUIC trace if a blank document persists. Correlate the **new GET/** status and transmitted bytes with older cancelled streams. Mere cancellation of an obsolete API or SSE stream is not proof that the new document failed. This implementation is not proven to resolve an iOS issue until live tests pass.
|
|
|
|
## Browser refresh and stream cancellation (0.31.5)
|
|
|
|
A browser may cancel an obsolete H3 stream when the user reloads or leaves a
|
|
page, sometimes before its HEADERS arrive. RFC 9114 `H3_REQUEST_CANCELLED`
|
|
(and a peer-originated `H3_NO_ERROR` reset) is not itself a server-side HTTP 400.
|
|
RustProxy therefore omits that fabricated 400 access-log entry and avoids
|
|
adding `H3_INTERNAL_ERROR` to a directly identified cancelled response stream.
|
|
Malformed or forbidden headers still yield a protocol-error 400.
|
|
|
|
This correction does **not** establish the cause of a blank document after F5.
|
|
If the new navigation fails, compare its actual document/asset statuses over H3
|
|
and H2, the configured reverse-proxy Host/Origin/forwarded-proto chain, and any
|
|
connection-level QUIC errors. An error record without HTTP method, path and
|
|
request headers cannot identify the browser document which failed. Capture
|
|
browser network traces and QUIC qlogs with timestamps for live acceptance.
|
|
|
|
Per-vhost `http2_enabled`/`http3_enabled` toggles also no longer rotate the global
|
|
HOT/COLD cache identity. Origin representation policy and cache invalidation are
|
|
unchanged; the frontend protocol remains part of live request routing.
|
|
|
|
## Verification gate
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
- HTTP/3 RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html
|
|
- QUIC RFC 9000: https://www.rfc-editor.org/rfc/rfc9000.html
|
|
- QPACK RFC 9204: https://www.rfc-editor.org/rfc/rfc9204.html
|
|
- Quinn: https://docs.rs/quinn/latest/quinn/
|
|
- RFC 8441 Extended CONNECT: https://www.rfc-editor.org/rfc/rfc8441.html
|
|
- RFC 9220 WebSocket over HTTP/3: https://www.rfc-editor.org/rfc/rfc9220.html
|
|
- hpx-h3 2.5.19: https://docs.rs/hpx-h3/2.5.19/h3/
|