fix(core): gate h2 extended CONNECT on backend support and send GOAWAY on upstream failure - #3814
Open
helix-nine wants to merge 2 commits into
Open
fix(core): gate h2 extended CONNECT on backend support and send GOAWAY on upstream failure#3814helix-nine wants to merge 2 commits into
helix-nine wants to merge 2 commits into
Conversation
…Y on upstream failure Two defects in run_http2_proxy (#3775), both reachable for every protocol: 'https' | 'wss' binding once the client-facing ALPN is reflected (#3777): - The client-facing h2 server advertised SETTINGS_ENABLE_CONNECT_PROTOCOL unconditionally, so a browser opening wss:// as an RFC 8441 extended CONNECT had its :protocol forwarded to backends that reject it (nginx), killing the WebSocket. The backend's opening SETTINGS is now read off the wire before the handshake (RFC 9113 requires it first) and replayed into it; the advertisement mirrors what was found. hyper's is_extended_connect_protocol_enabled() can't be used for this: with a spawning executor nothing ever wakes a manual poll of the Connection, and absence of the param would mean waiting out a timeout on every connection. - An upstream error returned early, dropping the client leg without GOAWAY and aborting every in-flight stream — the symptom #3731 fixed for HTTP/1, which the h2 path never got. Upstream errors are now logged and answered with graceful_shutdown like a clean end, and a handshake that fails outright serves the client just long enough to deliver GOAWAY.
Review round 1 on #3814: - A sniff that timed out or errored mid-frame dropped the bytes already consumed and handed hyper an empty replay, corrupting the stream; any incomplete opening SETTINGS now goes to goaway_only like a failed handshake instead. - goaway_only's stub answered a request that slipped in during the drain with a fabricated empty 200; it now answers 502. - Replay::poll_read returned Ready(Ok(())) with zero bytes filled when polled with a zero-capacity buffer, which reads as EOF; empty-buffer polls delegate to the inner stream. - take_initial_settings builds its replay buffer in one allocation and shares parse_settings_entries with the tests, which also stop redefining the module's frame constants. - Changelog: split the combined entry into one bullet per behavior.
Contributor
Author
|
Review round 1 found six things worth acting on; all fixed and pushed in d9df0a6:
Not acted on, for the record: the vectored-write downgrade through |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3775.
What
Two fixes to
run_http2_proxy(shared-libs/crates/start-core/src/net/http.rs), both pre-existing defects that become reachable for everyprotocol: 'https' | 'wss'binding once the client-facing ALPN is reflected (#3777).1. Extended CONNECT is now advertised only when the backend supports it
The client-facing h2 server used to set
SETTINGS_ENABLE_CONNECT_PROTOCOLunconditionally. A browser openingwss://…then uses an RFC 8441 extended CONNECT, whose:protocolwe forward verbatim — against a backend without RFC 8441 support (nginx) the stream is reset and the WebSocket dies. Now the advertisement mirrors the backend.How the decision is made: hyper's
SendRequest/Connection::is_extended_connect_protocol_enabled()turns out to be unusable here for two reasons, both measured:handshake().awaitresolves (h2's client handshake only writes our preface + SETTINGS), so it readsfalse.TokioExecutor) the connection runs on its own spawned task; manually polling the returnedConnectionregisters a waker nothing ever signals, so a "wait until the setting flips" loop hangs to its timeout — and absence of the param would mean waiting out that timeout on every proxied connection to an nginx-class backend. Unacceptable latency.Instead the proxy reads the backend's opening SETTINGS frame directly off the wire before handing the stream to hyper — RFC 9113 requires it to be the first frame — decides from its entries, and replays the consumed bytes into the real handshake via a small
AsyncReadwrapper (Replay). Cost on conformant backends: one round trip, zero added delay either way. If no settings arrive within 5s the connection is broken anyway; we proceed without the advertisement.2. An upstream error no longer aborts the client connection
res?in the select loop returned early, sograceful_shutdown()ran only when the upstream ended cleanly; any error — the common "container restarted" case — dropped the browser's leg with no GOAWAY, aborting every in-flight stream. This was the exact symptom #3731 fixed for HTTP/1; the h2 path never got the counterpart. Upstream errors are now logged and answered withgraceful_shutdown(), so in-flight streams drain behind GOAWAY. A handshake that fails outright (backend died before completing it) serves the client just long enough to deliver GOAWAY too.Tests
extended_connect_is_advertised_only_when_the_backend_advertises_it— parses the proxy's client-facing SETTINGS frames against backends with and without the param.an_upstream_error_reaches_the_client_as_goaway_not_a_bare_abort— a backend that answers its handshake and dies mid-connection reaches the browser as GOAWAY.All 12
net::httptests pass, fullstart-corelib suite green (329 passed,export_skipped per run-tests.sh).