Skip to content

Commit 280128c

Browse files
committed
fix(relay): make h2 client startup settings explicit
The Apps Script relay h2 fast path already depends on explicit client-side flow-control tuning, but those values were embedded directly in the handshake path and only covered stream and connection windows. That made the startup profile harder to audit and left max-frame and local stream-concurrency behavior implicit in the h2 crate defaults. Define a named h2 client profile for the relay connection and route h2 handshakes through a single builder helper. The profile advertises a 6 MiB initial stream window, a 15 MiB initial connection window, a 16 KiB maximum frame size, and a 1000-stream local concurrency setting. These values keep the client startup SETTINGS stable across refactors and reduce small-window churn while Apps Script envelopes are drained over the h2 connection. This does not add a new transport backend, alter ALPN policy, change retry behavior, or claim byte-for-byte browser fingerprint parity. The existing fallback model remains intact: peers that refuse h2, stalled h2 connections, and fronting-incompatibility responses still fall back to the warmed HTTP/1.1 pool. Add focused unit coverage for the h2 profile constants and document the current h2 behavior in the English and Persian guides. The documentation now describes h2 as an active fast path with explicit flow-control tuning rather than an unimplemented roadmap item.
1 parent 40b5386 commit 280128c

3 files changed

Lines changed: 39 additions & 10 deletions

File tree

docs/guide.fa.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ sni_hosts = ["www.google.com", "drive.google.com", "docs.google.com"]
373373

374374
| ویژگی | چرا نه |
375375
|---|---|
376-
| HTTP/2 multiplexing | state machine کریت `h2` (stream IDs، flow control، GOAWAY) موارد hang ظریف زیادی دارد؛ coalescing + pool ۲۰-conn بیشتر فایده را می‌گیرد |
376+
| HTTP/2 multiplexing | مسیر سریع `h2` وقتی استفاده می‌شود که edge گوگل آن را با ALPN قبول کند. کلاینت تنظیمات explicit و browser-scale برای flow-control سطح stream و connection می‌فرستد و اگر ALPN آن را رد کند، connection گیر کند، یا deployment وضعیت ناسازگاری fronting برگرداند، به pool گرم HTTP/1.1 برمی‌گردد. |
377377
| Batch (`q:[...]` در apps_script) | connection pool + tokio async از قبل خوب موازی‌سازی می‌کند؛ batch ~۲۰۰ خط مدیریت state اضافه می‌کند با سود نامشخص |
378378
| Range-based parallel download | edge case‌های واقعی (سرورهای بدون Range، chunked وسط stream)؛ ویدیوی یوتیوب از قبل با تونل بازنویسی SNI، Apps Script را دور می‌زند |
379379
| حالت‌های `domain_fronting` / `google_fronting` / `custom_domain` | Cloudflare در ۲۰۲۴ domain fronting عمومی را کشت؛ Cloud Run پلن پولی می‌خواهد |

docs/guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ This port focuses on the **`apps_script` mode** — the only one that reliably w
367367

368368
Intentionally **not** implemented:
369369

370-
- **HTTP/2 multiplexing**`h2` crate state machine has too many subtle hang cases; coalescing + 20-conn pool gets most of the benefit
370+
- **HTTP/2 multiplexing**the relay can use an `h2` fast path when the Google edge negotiates it. The client advertises explicit browser-scale stream and connection flow-control settings, then falls back to the warmed HTTP/1.1 pool when ALPN refuses h2, the h2 connection stalls, or the deployment returns a fronting-incompatibility status.
371371
- **Request batching (`q:[...]` mode in apps_script mode)** — connection pool + tokio async already parallelizes well; batching adds ~200 lines of state for unclear gain
372372
- **Range-based parallel download** — edge cases real (non-Range servers, chunked mid-stream); YouTube already bypasses Apps Script via SNI-rewrite tunnel
373373
- **Other modes** (`domain_fronting`, `google_fronting`, `custom_domain`) — Cloudflare killed generic domain fronting in 2024; Cloud Run needs a paid plan

src/domain_fronter.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ const H2_OPEN_TIMEOUT_SECS: u64 = 8;
134134
/// long. Prevents every concurrent caller during an h2 outage from
135135
/// paying its own full handshake-timeout cost in turn.
136136
const H2_OPEN_FAILURE_BACKOFF_SECS: u64 = 15;
137+
/// Client-side HTTP/2 flow-control profile for the Apps Script h2 fast path.
138+
///
139+
/// These values are intentionally browser-scale rather than the small h2 crate
140+
/// defaults. Larger windows avoid frequent WINDOW_UPDATE churn while draining
141+
/// Apps Script envelopes, and the explicit max-frame / stream-concurrency
142+
/// settings keep the initial client SETTINGS frame stable across refactors.
143+
/// This reduces anomalous h2 startup behavior without claiming byte-for-byte
144+
/// browser fingerprint parity; TLS record packing and peer behavior still
145+
/// affect the final wire shape.
146+
const H2_CLIENT_INITIAL_STREAM_WINDOW_BYTES: u32 = 6 * 1024 * 1024;
147+
const H2_CLIENT_INITIAL_CONNECTION_WINDOW_BYTES: u32 = 15 * 1024 * 1024;
148+
const H2_CLIENT_MAX_FRAME_BYTES: u32 = 16 * 1024;
149+
const H2_CLIENT_MAX_CONCURRENT_STREAMS: u32 = 1_000;
137150
/// Same idea as `H2_OPEN_TIMEOUT_SECS` but for the legacy h1 socket
138151
/// path. Without this, a stuck TCP connect or TLS handshake to a
139152
/// blackholed `connect_host:443` would block `acquire()` (and the
@@ -288,6 +301,16 @@ impl From<OpenH2Error> for FronterError {
288301
}
289302
}
290303

304+
fn configured_h2_client_builder() -> h2::client::Builder {
305+
let mut builder = h2::client::Builder::new();
306+
builder
307+
.initial_window_size(H2_CLIENT_INITIAL_STREAM_WINDOW_BYTES)
308+
.initial_connection_window_size(H2_CLIENT_INITIAL_CONNECTION_WINDOW_BYTES)
309+
.max_frame_size(H2_CLIENT_MAX_FRAME_BYTES)
310+
.max_concurrent_streams(H2_CLIENT_MAX_CONCURRENT_STREAMS);
311+
builder
312+
}
313+
291314
pub struct DomainFronter {
292315
connect_host: String,
293316
/// Pool of SNI domains to rotate through per outbound connection. All of
@@ -1350,14 +1373,7 @@ impl DomainFronter {
13501373
if !alpn_h2 {
13511374
return Err(OpenH2Error::AlpnRefused);
13521375
}
1353-
// Larger initial windows mean we don't have to call
1354-
// `release_capacity` on every chunk for typical Apps Script
1355-
// payloads (usually < 1 MB; range chunks are 256 KB). We still
1356-
// release capacity in the body-read loop for safety on larger
1357-
// bodies.
1358-
let (send, conn) = h2::client::Builder::new()
1359-
.initial_window_size(4 * 1024 * 1024)
1360-
.initial_connection_window_size(8 * 1024 * 1024)
1376+
let (send, conn) = configured_h2_client_builder()
13611377
.handshake(tls)
13621378
.await
13631379
.map_err(|e| OpenH2Error::Handshake(e.to_string()))?;
@@ -7223,6 +7239,19 @@ hello";
72237239
}
72247240
}
72257241

7242+
#[test]
7243+
fn h2_client_profile_uses_explicit_browser_scale_flow_control() {
7244+
assert_eq!(H2_CLIENT_INITIAL_STREAM_WINDOW_BYTES, 6 * 1024 * 1024);
7245+
assert_eq!(
7246+
H2_CLIENT_INITIAL_CONNECTION_WINDOW_BYTES,
7247+
15 * 1024 * 1024
7248+
);
7249+
assert_eq!(H2_CLIENT_MAX_FRAME_BYTES, 16 * 1024);
7250+
assert_eq!(H2_CLIENT_MAX_CONCURRENT_STREAMS, 1_000);
7251+
7252+
let _builder = configured_h2_client_builder();
7253+
}
7254+
72267255
#[tokio::test(flavor = "current_thread")]
72277256
async fn h2_handshake_post_tls_returns_alpn_refused_when_peer_picks_h1() {
72287257
// Verify the OpenH2Error::AlpnRefused path: if the TLS layer

0 commit comments

Comments
 (0)