Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide.fa.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 پلن پولی می‌خواهد |
Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 37 additions & 8 deletions src/domain_fronter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -288,6 +301,16 @@ impl From<OpenH2Error> 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
Expand Down Expand Up @@ -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()))?;
Expand Down Expand Up @@ -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
Expand Down
Loading