diff --git a/docs/guide.fa.md b/docs/guide.fa.md index d0247453..6f5c685f 100644 --- a/docs/guide.fa.md +++ b/docs/guide.fa.md @@ -373,7 +373,7 @@ sni_hosts = ["www.google.com", "drive.google.com", "docs.google.com"] | ویژگی | چرا نه | |---|---| -| HTTP/2 multiplexing | state machine کریت `h2` (stream IDs، flow control، GOAWAY) موارد hang ظریف زیادی دارد؛ coalescing + pool ۲۰-conn بیشتر فایده را می‌گیرد | +| HTTP/2 multiplexing | مسیر سریع `h2` وقتی استفاده می‌شود که edge گوگل آن را با ALPN قبول کند. کلاینت تنظیمات explicit و browser-scale برای flow-control سطح stream و connection می‌فرستد و اگر ALPN آن را رد کند، connection گیر کند، یا deployment وضعیت ناسازگاری fronting برگرداند، به pool گرم HTTP/1.1 برمی‌گردد. | | Batch (`q:[...]` در apps_script) | connection pool + tokio async از قبل خوب موازی‌سازی می‌کند؛ batch ~۲۰۰ خط مدیریت state اضافه می‌کند با سود نامشخص | | Range-based parallel download | edge case‌های واقعی (سرورهای بدون Range، chunked وسط stream)؛ ویدیوی یوتیوب از قبل با تونل بازنویسی SNI، Apps Script را دور می‌زند | | حالت‌های `domain_fronting` / `google_fronting` / `custom_domain` | Cloudflare در ۲۰۲۴ domain fronting عمومی را کشت؛ Cloud Run پلن پولی می‌خواهد | diff --git a/docs/guide.md b/docs/guide.md index 679a35d0..ba6a2c5b 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -367,7 +367,7 @@ This port focuses on the **`apps_script` mode** — the only one that reliably w Intentionally **not** implemented: -- **HTTP/2 multiplexing** — `h2` crate state machine has too many subtle hang cases; coalescing + 20-conn pool gets most of the benefit +- **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. - **Request batching (`q:[...]` mode in apps_script mode)** — connection pool + tokio async already parallelizes well; batching adds ~200 lines of state for unclear gain - **Range-based parallel download** — edge cases real (non-Range servers, chunked mid-stream); YouTube already bypasses Apps Script via SNI-rewrite tunnel - **Other modes** (`domain_fronting`, `google_fronting`, `custom_domain`) — Cloudflare killed generic domain fronting in 2024; Cloud Run needs a paid plan diff --git a/src/domain_fronter.rs b/src/domain_fronter.rs index 0e11e764..4e146b6c 100644 --- a/src/domain_fronter.rs +++ b/src/domain_fronter.rs @@ -134,6 +134,19 @@ const H2_OPEN_TIMEOUT_SECS: u64 = 8; /// long. Prevents every concurrent caller during an h2 outage from /// paying its own full handshake-timeout cost in turn. const H2_OPEN_FAILURE_BACKOFF_SECS: u64 = 15; +/// Client-side HTTP/2 flow-control profile for the Apps Script h2 fast path. +/// +/// These values are intentionally browser-scale rather than the small h2 crate +/// defaults. Larger windows avoid frequent WINDOW_UPDATE churn while draining +/// Apps Script envelopes, and the explicit max-frame / stream-concurrency +/// settings keep the initial client SETTINGS frame stable across refactors. +/// This reduces anomalous h2 startup behavior without claiming byte-for-byte +/// browser fingerprint parity; TLS record packing and peer behavior still +/// affect the final wire shape. +const H2_CLIENT_INITIAL_STREAM_WINDOW_BYTES: u32 = 6 * 1024 * 1024; +const H2_CLIENT_INITIAL_CONNECTION_WINDOW_BYTES: u32 = 15 * 1024 * 1024; +const H2_CLIENT_MAX_FRAME_BYTES: u32 = 16 * 1024; +const H2_CLIENT_MAX_CONCURRENT_STREAMS: u32 = 1_000; /// Same idea as `H2_OPEN_TIMEOUT_SECS` but for the legacy h1 socket /// path. Without this, a stuck TCP connect or TLS handshake to a /// blackholed `connect_host:443` would block `acquire()` (and the @@ -288,6 +301,16 @@ impl From for FronterError { } } +fn configured_h2_client_builder() -> h2::client::Builder { + let mut builder = h2::client::Builder::new(); + builder + .initial_window_size(H2_CLIENT_INITIAL_STREAM_WINDOW_BYTES) + .initial_connection_window_size(H2_CLIENT_INITIAL_CONNECTION_WINDOW_BYTES) + .max_frame_size(H2_CLIENT_MAX_FRAME_BYTES) + .max_concurrent_streams(H2_CLIENT_MAX_CONCURRENT_STREAMS); + builder +} + pub struct DomainFronter { connect_host: String, /// Pool of SNI domains to rotate through per outbound connection. All of @@ -1350,14 +1373,7 @@ impl DomainFronter { if !alpn_h2 { return Err(OpenH2Error::AlpnRefused); } - // Larger initial windows mean we don't have to call - // `release_capacity` on every chunk for typical Apps Script - // payloads (usually < 1 MB; range chunks are 256 KB). We still - // release capacity in the body-read loop for safety on larger - // bodies. - let (send, conn) = h2::client::Builder::new() - .initial_window_size(4 * 1024 * 1024) - .initial_connection_window_size(8 * 1024 * 1024) + let (send, conn) = configured_h2_client_builder() .handshake(tls) .await .map_err(|e| OpenH2Error::Handshake(e.to_string()))?; @@ -7223,6 +7239,19 @@ hello"; } } + #[test] + fn h2_client_profile_uses_explicit_browser_scale_flow_control() { + assert_eq!(H2_CLIENT_INITIAL_STREAM_WINDOW_BYTES, 6 * 1024 * 1024); + assert_eq!( + H2_CLIENT_INITIAL_CONNECTION_WINDOW_BYTES, + 15 * 1024 * 1024 + ); + assert_eq!(H2_CLIENT_MAX_FRAME_BYTES, 16 * 1024); + assert_eq!(H2_CLIENT_MAX_CONCURRENT_STREAMS, 1_000); + + let _builder = configured_h2_client_builder(); + } + #[tokio::test(flavor = "current_thread")] async fn h2_handshake_post_tls_returns_alpn_refused_when_peer_picks_h1() { // Verify the OpenH2Error::AlpnRefused path: if the TLS layer