Skip to content

Releases: therealaleph/MasterHttpRelayVPN-RUST

v1.9.24

13 May 11:18

Choose a tag to compare

Fix Full mode timeout cascade — `batch header read honors request_timeout_secs` (#1088, PR #1108 by @dazzling-no-more). در Full mode، یک Apps Script edge کند، تمام تونل sessionهای hot-and-flowing رو cascade-kill می‌کرد. کاربرها روی v1.9.21+ مرتب 10s "batch timeout" می‌دیدن و download progress تلگرام/browser رو از دست می‌دادن. Root cause: `read_http_response` در `domain_fronter.rs` یک hardcoded 10s header-read timeout داشت که داخل `tunnel_batch_request_to` اجرا می‌شد — مستقل از و کوتاه‌تر از outer `tokio::time::timeout(batch_timeout, ...)` در `fire_batch`. Apps Script cold starts معمولاً 8-12s طول می‌کشن (PR #1040's A/B 4/30 H1 batches رو ثبت کرد که دقیقاً 10s timeout می‌شدن)، پس inner cliff به‌عنوان false-positive batch timeout قبل از اینکه `request_timeout_secs` (default 30s) trigger بشه fire می‌شد. Fix: (1) `tunnel_batch_request_to` حالا `batch_timeout` رو به header read pass می‌کنه via new `read_http_response_with_header_timeout` helper. (2) Header read یک absolute deadline استفاده می‌کنه (`timeout_at`) به جای per-read `timeout()` — slow drip-feed peer دیگه نمی‌تونه silently extend بزنه. (3) Bonus: `TunnelMux::reply_timeout` با `batch_timeout` co-vary می‌کنه (`batch_timeout + 5s slack`). ۲۰۹ → ۲۳۱ lib test (+22 regression).
Docker: cargo-chef برای build بدون BuildKit (#620, PR #1117 by @dazzling-no-more). `tunnel-node/Dockerfile` از BuildKit-only `RUN --mount=type=cache` استفاده می‌کرد که روی Cloud Run's `gcloud run deploy --source .` path شکست می‌خورد (underlying `gcr.io/cloud-builders/docker` builder BuildKit رو enable نمی‌کنه). cargo-chef pattern: `recipe.json` planner stage + `cargo chef cook` deps stage + final build with `src/` on top. Docker's regular layer cache حالا dependency reuse رو handle می‌کنه — warm rebuilds تنها `src/` رو compile می‌کنن. Base bump `rust:1.85-slim` → `rust:1.90-slim` (cargo-chef نیاز به rustc 1.86+ داره).

Fix Full mode timeout cascade — batch header read honors request_timeout_secs (#1088, PR #1108 by @dazzling-no-more). Under Full mode, a single slow Apps Script edge cascade-killed every in-flight tunnel session sharing its batch. Users on v1.9.21+ saw frequent 10s "batch timeout" errors and lost download progress on Telegram / browser sessions.

Root cause: read_http_response in domain_fronter.rs had a hardcoded 10s header-read timeout that ran inside tunnel_batch_request_to — independent of and shorter than the outer tokio::time::timeout(batch_timeout, …) in fire_batch. Apps Script cold starts routinely land in the 8-12s range (PR #1040's A/B recorded 4/30 H1 batches timing out at exactly 10s after the H2→H1 switch), so the inner cliff fired as a false-positive batch timeout well before request_timeout_secs (default 30s) could.

Fix (in domain_fronter.rs + tunnel_client.rs):

  1. tunnel_batch_request_to passes batch_timeout to the header read via new read_http_response_with_header_timeout helper. Config::request_timeout_secs is now the only knob controlling how long we wait for an Apps Script edge to start responding. Other callers (relay path, exit-node) keep the historical 10s value.
  2. Header read uses a single absolute deadline (timeout_at) instead of per-read timeout(). Total elapsed across all header reads is bounded regardless of read cadence — a slow drip-feed peer can no longer silently extend.
  3. TunnelMux::reply_timeout co-varies with batch_timeout (computed at construction as fronter.batch_timeout() + 5s slack instead of fixed 35s const). Operators raising request_timeout_secs no longer have sessions abandon reply_rx just before fire_batch's HTTP round-trip would complete.

209 → 231 lib tests (+22 regression covering the deadline/co-variance behavior).

Docker: cargo-chef so tunnel-node builds without BuildKit (#620, PR #1117 by @dazzling-no-more). tunnel-node/Dockerfile used BuildKit-only RUN --mount=type=cache directives, breaking on Cloud Run's gcloud run deploy --source . path (the underlying gcr.io/cloud-builders/docker builder doesn't enable BuildKit, and --set-build-env-vars DOCKER_BUILDKIT=1 doesn't flip it on either).

Reworked to use cargo-chef: a dedicated planner stage emits recipe.json for dependency metadata, a cargo chef cook stage builds just the deps in their own Docker layer, the final build stage adds src/ on top. Docker's regular layer cache handles dependency reuse — warm rebuilds where only src/ changes still skip the slow crate compile.

Base bump rust:1.85-slimrust:1.90-slim (cargo-chef's transitive deps require rustc 1.86+; tunnel-node's Cargo.toml has no rust-version pin so the bump is internal-only).

Action for Cloud Run users blocked on #620: pull v1.9.24 of the tunnel-node Docker image (ghcr.io/therealaleph/mhrv-tunnel-node:v1.9.24 or :latest) — your gcloud run deploy --source . should now succeed without BuildKit.

Followup: issue #1131 (BuffOvrFlw) reports h1 open timed out after 8s — that's the H1_OPEN_TIMEOUT_SECS = 8 from PR #1029 firing on open() (TCP+TLS handshake), separate from the header-read timeout this release fixes. Worth a follow-up PR to make H1_OPEN_TIMEOUT_SECS parameterized via request_timeout_secs too.

What's Changed

Full Changelog: v1.9.23...v1.9.24

v1.9.23

12 May 13:14

Choose a tag to compare

Fix: stream range-parallel downloads larger than Apps Script's 50 MiB cap (#1042 + PR #1085 by @dazzling-no-more). دانلودهای range-capable بزرگ‌تر از ~۵۰ MiB از طریق Apps Script relay با `504 Relay timeout — Apps Script unresponsive` fail می‌شد. v2rayN DMG 104 MiB در reported logs canonical repro بود. روت کاز: `relay_parallel_range` در 64 MiB ceiling داشت و برای بالاتر به single `relay()` fallback می‌کرد که از 50 MiB Apps Script ceiling عبور می‌کرد، Apps Script script رو mid-execution می‌کشت، و 25s timeout. Fix: `relay_parallel_range` به writer-based API تبدیل شد که large files رو chunk-by-chunk (هر chunk ≤256 KiB، خوب زیر 50 MiB cap) به client socket stream می‌کنه. ۴-way dispatch: Buffered (≤40 MiB)، Stream (40 MiB-16 GiB)، FallbackSingleGet (wrapper 40-64 MiB)، RejectTooLarge (>16 GiB، quota guard). Lazy range planning با `saturating_*` — O(1) memory حتی برای `u64::MAX` total (قبل ~6 GB Vec allocation می‌داد). MITM HTTPS + plain HTTP call sites + CORS-aware `transform_head` همه updated. ۲۰۹ → ۲۲۷ lib test (+۱۸ new: dispatch enum، lazy planning، head assembly، head transform، streaming writer، flush behavior، CORS-into-streaming integration).

Fix: stream range-parallel downloads larger than Apps Script's 50 MiB cap (#1042 + PR #1085 by @dazzling-no-more).

Range-capable downloads larger than ~50 MiB through the Apps Script relay returned 504 Relay timeout — Apps Script unresponsive instead of the file. The 104 MiB v2rayN DMG in the reported logs was the canonical repro (also fixes @Paymanonline's #1077 report).

Root cause: relay_parallel_range capped the stitched response at 64 MiB and fell back to a single relay() for anything larger. Single-GET routes through Apps Script's ~50 MiB response ceiling, so Apps Script killed the script mid-execution and we hung for the full 25s relay timeout before returning 504.

Fix: convert relay_parallel_range into a writer-based API that streams large files chunk-by-chunk to the client socket. Each chunk is still one ≤256 KiB Apps Script call (well under the 50 MiB cap); only the host-side buffering changes. Backward-compatible Vec<u8> wrapper preserves the pre-v1.9.23 API surface for external library consumers.

Three-way dispatch via RangeDispatch { Buffered, Stream, FallbackSingleGet, RejectTooLarge } and the pure dispatch_range_response(total, streaming_allowed) predicate:

  • Bufferedtotal ≤ APPS_SCRIPT_BODY_MAX_BYTES (40 MiB) on either surface. Existing stitch + single-GET fallback path; fully recovers on chunk failure.
  • Stream — writer API above 40 MiB. Streams; chunk failure flushes the committed prefix and returns Err so the Content-Length mismatch tells download clients to resume via Range.
  • FallbackSingleGet — wrapper above 64 MiB. Falls back to self.relay(), matching the pre-v1.9.23 cliff for external library consumers stuck on the old API.
  • RejectTooLarge — writer API above 16 GiB. Refuses with 502; bounds worst-case Apps Script quota drain from a hostile origin advertising an absurd Content-Range total.

Memory bounds: Lazy plan_remaining_ranges via std::iter::from_fn + saturating_*. Range planning is O(1) memory regardless of advertised total — even a u64::MAX total no longer drives a ~6 GB Vec<(u64, u64)> allocation.

CORS interaction: MITM HTTPS and plain-HTTP call sites updated to use relay_parallel_range_to with a CORS-aware transform_head closure. New inject_cors_into_head (head-only variant of inject_cors_response_headers) lets the streaming path rewrite ACL headers before the body has been assembled.

209 → 227 lib tests (+18 new: RangeDispatch enum coverage, lazy range planning under u64::MAX, assemble_200_head correctness, transform_head closure invocation, streaming writer chunk-by-chunk semantics, head-then-flush-before-body ordering, CORS-into-streaming cross-module integration).

User impact: GitHub release downloads, large CDN binaries, ROM-hack distributions, anything in the 50 MiB – 16 GiB range now downloads successfully through apps_script mode. Previously these required Full mode, an Iran-mirror proxy (#1077), or a friend-with-VPS workaround.

What's Changed

Full Changelog: v1.9.22...v1.9.23

v1.9.22

11 May 13:24

Choose a tag to compare

Fix: skip H2 برای `tunnel_request` (single ops) — completes #1040 (PR #1041 by @yyoyoian-pixel). v1.9.21's PR #1040 H2 رو از `tunnel_batch_request_to` skip کرد ولی `tunnel_request` (single-op path برای plain `connect` ops) جا موند. کاربرانی که session‌های full-tunnel با single-op path داشتند هنوز ۱۶-۱۷s long-poll stalls می‌گرفتن. این PR fix رو complete می‌کنه — same shape: حذف H2 try/fallback/NonRetryable block، مستقیم H1 pool `acquire()`. همه ۵ تا call site `h2_relay_request` audit شدن (جدول در PR description) — relay-mode paths H2 رو نگه می‌دارن (apps_script users بدون change)، همه full-tunnel paths حالا H1-only. ۲۰۹ lib test still pass.

Fix: skip H2 for tunnel_request (single ops) — completes #1040 (PR #1041 by @yyoyoian-pixel).

v1.9.21's PR #1040 skipped H2 for tunnel_batch_request_to but missed tunnel_request — the single-op path used for plain connect ops. Users on full-tunnel sessions that went through the single-op path still saw 16-17s long-poll stalls. This PR completes the fix: same shape, remove the H2 try/fallback/NonRetryable block from tunnel_request, go straight to H1 pool acquire().

All 5 h2_relay_request call sites audited:

Call site Function Mode H2 skipped?
do_relay_once_with relay Relay No (correct — relay benefits from H2)
relay() exit-node relay Relay No (correct)
tunnel_request tunnel single op Full tunnel Yes (this release)
tunnel_batch_request_to tunnel batch Full tunnel Yes (v1.9.21)
tunnel_batch_request_with_timeout tunnel batch Full tunnel Yes (v1.9.21)

No other full-tunnel paths use H2 after this fix. Relay-mode H2 stays — r0ar's controlled A/B in #962 confirmed h2 is strictly better for apps_script-mode users, and that path is unchanged.

209 lib tests still pass. domain_fronter.rs-only, -41 net lines.

What's Changed

Full Changelog: v1.9.21...v1.9.22

v1.9.21

11 May 00:01

Choose a tag to compare

Perf: skip H2 برای Full-tunnel batch requests (PR #1040 by @yyoyoian-pixel). Full mode tunnel batches قبلاً N op رو در یک HTTP request coalesce می‌کنند — H2 stream multiplexing چیزی برای multiplex کردن نداره. H2 try/fallback path در این مسیر خاص سه regression از v1.9.14 معرفی کرد: (1) long-poll stallها در ۱۶-۱۷s به جای 10s timeout روی H1 — هر poll ~۶۰٪ بیشتر slot Apps Script رو نگه می‌داشت، (2) silent batch drops via `RequestSent::Maybe` بدون retry، (3) pool starvation از `POOL_MIN_H2_FALLBACK = 2` که از 8 → 2 trim می‌کرد. H2 multiplexing برای relay mode (apps_script) فعال می‌مونه — اونجا واقعاً به‌درد می‌خوره (r0ar در #962 confirmed). A/B روی Pixel 6 Pro: 0/30 vs 8-10/30 long-poll stalls. ۲۰۹ lib test still pass. v1.9.14 tunnel performance بازگشت + همه v1.9.15+ improvements حفظ شد (relay mode h2، zero-copy mux، block DoH/QUIC، PR #1029 warm-race fix).

Perf: skip H2 for Full-tunnel batch requests (PR #1040 by @yyoyoian-pixel). Tunnel batches already coalesce N ops into one HTTP request — H2 stream multiplexing has nothing to multiplex on this code path. The H2 try/fallback block introduced three regressions vs v1.9.14:

  1. Long-poll stalls: idle polls completed at 16-17s (LONGPOLL_DEADLINE + network latency) instead of timing out at 10s on H1. Each poll held an Apps Script execution slot ~60% longer.
  2. Silent batch drops: RequestSent::Maybe failures dropped the entire batch with no retry — a failure mode H1 doesn't have.
  3. Pool starvation: POOL_MIN_H2_FALLBACK = 2 trimmed the H1 pool from 8 → 2 once H2 connected, but tunnel batches still used H1 and needed the full pool.

H2 multiplexing stays active for relay mode (non-full) where each browser request is a separate HTTP call that genuinely benefits from stream multiplexing — r0ar's controlled A/B test in #962 confirmed h2 is strictly better than force_http1: true for apps_script-mode users, and that path is unchanged here.

Changes (domain_fronter.rs-only, -54/-12 lines, +12 net)

  • tunnel_batch_request_to: remove H2 try/fallback/NonRetryable block, go straight to H1 pool acquire().
  • run_pool_refill: always maintain POOL_MIN = 8. Remove the POOL_MIN_H2_FALLBACK = 2 trim.

A/B results (Pixel 6 Pro, 30 batch samples each)

Metric H2 (stock v1.9.20) H1 (this release) v1.9.14 (baseline)
16-17s batches 8-10/30 0/30 0/30
10s timeouts 0 4/30 5/30
Active RTTs 1.4-2.4s 1.3-2.2s 1.4-2.3s

Restores v1.9.14 tunnel performance while keeping all v1.9.15+ improvements (H2 for relay, zero-copy mux from PR #881, block DoH/QUIC defaults from v1.9.13/14, PR #1029's warm-race fix from v1.9.20).

Interaction with v1.9.20 (PR #1029)

PR #1029 added H2Cell.dead: Arc<AtomicBool> for synchronous dead-cell detection. With this release removing the H2 path for tunnel batches, the dead-cell flag scopes to relay mode only — that's intentional (the flag was protecting the relay path in practice). No regression.

209 lib tests still pass (no test changes — the affected paths are exercised by integration probes which the PR reporter ran on Pixel 6 Pro).

What's Changed

Full Changelog: v1.9.20...v1.9.21

v1.9.20

10 May 21:56

Choose a tag to compare

Fix Full mode regression از v1.9.15 (#924 — یک ۳-هفته‌ای tracking thread با ۱۸+ duplicate report، fixed by @rezaisrad in PR #1029). علامت: `batch timed out after 30s` در Full mode، در حالی که apps_script mode normal کار می‌کرد. فقط workaround موجود `"force_http1": true` kill switch بود. Bisect دقیق این رو به `0e678630a` (PR #799 که h2 multiplexing رو اضافه کرد) رساند. روت کاز یک‌ line ordering: `warm()` در v1.9.15 h1 prewarm loop رو پشت `ensure_h2().await` گذاشت — وقتی h2 handshake کند بود (تا 8s)، pool h1 خالی می‌موند. اگر در آن window یک request می‌آمد، h1 fallback یک TCP+TLS handshake cold می‌زد که خود stall می‌شد، outside the 30s batch_timeout. Fix: h1 prewarm parallel با h2 handshake (v1.9.14 ordering restored)، plus بستنک‌های پیرامون با `H1_OPEN_TIMEOUT_SECS = 8` و `H2Cell.dead` AtomicBool. ۲۰۸ → ۲۰۹ lib test (+1 regression: `ensure_h2_rejects_dead_cell_within_ttl`). تأیید end-to-end: 5/5 cold restarts pass (9.6-22.5s)، 5/5 concurrent SOCKS5 burst.

Fix Full mode regression since v1.9.15 (#924, PR #1029 by @rezaisrad). #924 was the canonical tracking thread for an 18+ duplicate cluster spanning ~3 weeks; affected users saw batch timed out after 30s on every Full-mode request while apps_script mode kept working. The only available workaround was the "force_http1": true kill switch.

Root cause (rigorously bisected to 0e678630a — PR #799 which added HTTP/2 multiplexing): PR #799 gated the h1 socket-pool prewarm behind ensure_h2().await. ensure_h2() is bounded by H2_OPEN_TIMEOUT_SECS = 8s but can take the full window on a cold first connection. During that window the h1 fallback pool was empty, so any request that arrived would:

  1. Get Err((Relay("h2 unavailable"), No)) immediately → fall back to h1
  2. Empty pool → cold open() → fresh TCP+TLS to connect_host:443
  3. Same network conditions that stalled h2 also stalled h1; cold open exceeded the 30s batch_timeout
  4. User saw batch timed out after 30s that "works on apps_script" couldn't explain

Fix (two commits, domain_fronter.rs-only):

  1. warm h1 pool in parallel with h2: spawn h2 prewarm in a separate task so the h1 prewarm loop runs concurrently. Full n h1 sockets are warm before user traffic, even when h2 stalls. run_pool_refill trims back to POOL_MIN_H2_FALLBACK = 2 within 5s once h2 lands as the fast path.

  2. bound h1 open() + detect dead h2 cells synchronously: H1_OPEN_TIMEOUT_SECS = 8 wraps the TCP+TLS handshake in open() so a stuck handshake doesn't block acquire() until the outer batch budget elapses. H2Cell.dead: Arc<AtomicBool> flipped by the connection driver task when Connection::await ends — known-dead cells are rejected within ≤5s instead of waiting for H2_CONN_TTL_SECS = 540s to expire.

API impact: h2_handshake_post_tls return type changes to (SendRequest, Arc<AtomicBool>). One existing test (h2_handshake_post_tls_returns_alpn_refused_when_peer_picks_h1) tweaks its Ok arm to match — no panic message change.

208 → 209 lib tests (+1 regression: ensure_h2_rejects_dead_cell_within_ttl). Live end-to-end (per PR notes): 5/5 cold restarts pass in 9.6-22.5s, 5/5 concurrent SOCKS5 burst, default full.json baseline 200 OK in 13.3s.

Action for affected users: update to v1.9.20, drop the "force_http1": true workaround from config.json if you had it set. Full mode should work reliably on cold restart again.

What's Changed

New Contributors

Full Changelog: v1.9.19...v1.9.20

v1.9.19

10 May 14:20

Choose a tag to compare

• UI accessibility — screen reader labels for NVDA / Narrator (#1015 by @brightening-eyes, fixes #916). `accesskit` در Cargo.toml از قبل فعال بود ولی هیچ widget label-association نداشت — وقتی focus به یک text input یا combobox می‌رفت، NVDA فقط نوع control رو می‌گفت ("edit", "combobox") نه نام field رو. حالا `form_row` پلامبینگ `egui::Id` رو به widget می‌فرسته و هر widget با `.labelled_by(label_id)` به label visible خود معرفی می‌شه. تست شد توسط کاربر نابینایی که issue رو گزارش داد. ۲۰۸ lib test همه pass. (also includes c437598 fix for exit_node Content-Encoding + Content-Length stripping — ChatGPT / Claude / Reddit through exit-node now work without Content Encoding Error.)

UI accessibility — proper screen-reader labels for NVDA / Narrator (#1015 by @brightening-eyes, fixes #916). The accesskit feature was already enabled in Cargo.toml via eframe, but no widget had an explicit label association — so when focus moved to a text input or combobox, NVDA / Narrator only announced the control type ("edit", "combobox") instead of the field name. The fix plumbs egui::Id through form_row so each widget can call .labelled_by(label_id) to associate with its visible label. Tested by the blind user who originally reported the issue with their actual NVDA setup. 208/208 lib tests still pass.

form_row's signature changes from widget: impl FnOnce(&mut egui::Ui) to widget: impl FnOnce(&mut egui::Ui, egui::Id). Two existing call sites that don't need the label id (the Mode combobox, Share on LAN checkbox) bind it as _label_id — no functional change there.

• Also rolling up the exit_node Content-Encoding fix (#964): fetch() (Deno / Bun / Node) auto-decompresses gzip / br / deflate response bodies, but the destination's Content-Encoding: gzip header was forwarded verbatim — telling the browser the body was gzipped when it was already plain. Browsers raised Content Encoding Error: invalid or unsupported form of compression. Strip both Content-Encoding and Content-Length from the forwarded headers (the Apps Script + Rust transport reframes the wire body anyway, so neither is meaningful end-to-end). Affects every compressed-response destination through exit-node: ChatGPT, Claude, Reddit, X, etc.

Action for exit-node users: pull the latest assets/exit_node/exit_node.ts and redeploy your Deno Deploy / VPS exit-node. The Rust binary side has nothing new for this fix — it's purely on the exit-node script.

What's Changed

Full Changelog: v1.9.18...v1.9.19

v1.9.18

08 May 03:15

Choose a tag to compare

• Performance refactor of full-tunnel mux hot path (#881 by @dazzling-no-more) — zero-copy reads via Bytes/BytesMut و base64 encoding از روی single mux thread برداشته شد. هیچ wire-protocol change نداره — فقط internal data flow. (1) tunnel_loop و SOCKS5 UDP receive loop دیگه per-iteration Vec::to_vec() copy ندارن. MuxMsg::{ConnectData,Data,UdpOpen,UdpData} حالا Bytes (Arc-backed) carry می‌کنن به جای Vec<u8>/Arc<Vec<u8>>. TCP path threshold-based: ≥32 KB → BytesMut::split().freeze() (saves 64 KB memcpy on hot downloads); <32 KB → Bytes::copy_from_slice + buf.clear() (payload-sized retention). UDP path: fixed Vec<u8> recv buffer + size-guarded copy. (2) base64 encoding (تا ~3 MB per batch) از mux thread رفت به spawned task تو fire_batch بعد از per-deployment semaphore — single mux task دیگه serialize نمی‌شه. (3) Code quality: BatchAccum::push_or_fire (۴ match arm به ۱ کلپس)، should_fire() predicate با saturating_add، encode_pending() free function. ۲۰۰ → ۲۰۸ lib test (+۸ regression: encode_pending × ۴، should_fire × ۳، batch_accum_reindexes_after_flush). API change: TunnelMux::udp_open/udp_data حالا impl Into<Bytes> می‌گیرن — existing callers با Vec/Bytes/BytesMut بدون تغییر کار می‌کنن.

• Performance refactor of the full-tunnel mux hot data path (#881 by @dazzling-no-more). No wire-protocol changes — internal data flow only.

1. Zero-copy reads via Bytes/BytesMut. tunnel_loop and the SOCKS5 UDP receive loop drop per-iteration Vec::to_vec() copies. MuxMsg::{ConnectData,Data,UdpOpen,UdpData} now carry Bytes (Arc-backed internally) instead of Vec<u8>/Arc<Vec<u8>>; the Arc::try_unwrap dance for pending_client_data is gone. TCP path is threshold-based to avoid memory regressions:

  • n ≥ 32 KB: BytesMut::split().freeze() — saves the 64 KB memcpy on hot downloads.
  • n < 32 KB: Bytes::copy_from_slice + buf.clear() — payload-sized retention. Without this split, bytes 1.x's whole-allocation refcount would pin a full 64 KB per queued tiny read under semaphore stall (worst case ~96 MB on a backpressured tunnel).

UDP path: fixed Vec<u8> recv buffer + Bytes::copy_from_slice after the 9 KB MAX_UDP_PAYLOAD_BYTES guard. parse_socks5_udp_packet split into _offsets + &[u8] wrapper so callers stay on the reusable buffer.

2. Base64 encoding moved off the single mux thread. New internal PendingOp { data: Option<Bytes>, encode_empty: bool } flows through mux_loop with raw bytes. Actual B64.encode(...) runs in fire_batch's spawned task, after the per-deployment semaphore. Up to ~3 MB of encoding per batch (50 ops × 64 KB) no longer serializes the single mux task.

3. Code quality (drive-bys). BatchAccum::push_or_fire collapses 4× ~25-line match arms into ~10 lines each. should_fire(pending_len, payload_bytes, op_bytes) predicate extracted with saturating_add. encode_pending(p) -> BatchOp extracted as a free function for direct test coverage.

Public API change: TunnelMux::udp_open and udp_data now take data: impl Into<Bytes> instead of Vec<u8> — existing in-tree callers passing Vec<u8>, &'static [u8], Bytes, or BytesMut all keep compiling.

200 → 208 lib tests (+8 regression: encode_pending_* × 4, should_fire_* × 3, batch_accum_reindexes_after_flush).

What's Changed

Full Changelog: v1.9.16...v1.9.18

v1.9.17

07 May 18:15

Choose a tag to compare

• Inject CORS response headers after relay — اضافه شد به‌جای فقط preflight short-circuit. مرورگرها در درخواست‌های cross-origin (مثل YouTube’s youtubei/v1/next / youtubei/v1/comments که از script context fire می‌شه) responseـی نیاز دارن با Access-Control-Allow-Origin که با origin درخواست match کنه + Allow-Credentials: true. Apps Script's UrlFetchApp.fetch() گاهی header‌های ACL مقصد رو preserve نمی‌کنه، یا destination با Allow-Origin: * پاسخ می‌ده که با credentialed request ناسازگاره. mhrv-rs حالا header‌های Access-Control-* پاسخ relay رو strip می‌کنه + permissive set تزریق می‌کنه که با origin درخواست echo می‌شه. علت ریشه‌ای: YouTube comments نمی‌اومدن load بشن + گاهی restricted-mode error به همین دلیل ظاهر می‌شد. ایده credit: ThisIsDara/mhr-cfw-go (Go rewrite of upstream Python). فقط برای درخواست‌هایی با Origin header اعمال می‌شه — non-CORS traffic (curl، apps native) دست‌نخورده می‌مونه. ۱۹۷ → ۲۰۰ lib test (+۳ regression test for CORS injection edge cases).

• Inject CORS response headers after relay (in addition to the existing preflight short-circuit). When browsers issue cross-origin fetches from script contexts — e.g. YouTube's youtubei/v1/next / youtubei/v1/comments calls, which fire from the player JS — they require the response to carry Access-Control-Allow-Origin matching the request's origin AND Allow-Credentials: true. Apps Script's UrlFetchApp.fetch() sometimes doesn't preserve the destination's ACL headers, or the destination returns Allow-Origin: * which is incompatible with credentialed requests. mhrv-rs now strips any Access-Control-* headers from the relay response and injects a permissive set keyed on the request's Origin. Root cause: YouTube comments not loading + the "restricted mode" error sometimes surfacing on cross-origin XHR responses the browser silently dropped. Idea credit: ThisIsDara/mhr-cfw-go (Go rewrite of upstream Python's CFW variant). Only applies when the original request had an Origin header — non-CORS traffic (curl, app-level HTTP clients) passes through byte-for-byte unchanged. 197 → 200 lib tests (+3 regression tests for CORS injection edge cases: wildcard-origin replacement, non-ACL header preservation, malformed-response passthrough).

Full Changelog: v1.9.16...v1.9.17

v1.9.16

07 May 16:47

Choose a tag to compare

• Fix Full mode large-download truncation at exactly 50 MiB (#863). Apps Script's response body cap is ~50 MiB; tunnel-node had a TCP_DRAIN_MAX_BYTES = 16 MiB per-session cap to stay under it, but multiple sessions in the same batch each contributed up to 16 MiB raw, summing past 50 MiB on busy VPS (Steam/CDN downloads with N≥4 concurrent sessions). Symptom: batch JSON parse error: EOF while parsing a string at line 1 column 52428630 (body_len=52428630) followed by session abort + download restart from 0. Fix: new BATCH_RESPONSE_BUDGET = 32 MiB total-batch cap; the drain loop tracks remaining budget across sessions and stops one short of the cliff. Sessions deferred this batch keep their buffered data and drain on the next poll (no data loss). New regression test drain_now_respects_caller_budget_below_per_session_cap. ۳۶ tunnel-node test (was 35) همه pass + ۱۹۷ lib test همه pass.

• Fix Full mode large-download truncation at exactly 50 MiB (#863). Apps Script's response body cap is ~50 MiB; tunnel-node had a TCP_DRAIN_MAX_BYTES = 16 MiB per-session cap to stay under it, but multiple sessions in the same batch each contributed up to 16 MiB raw, summing past 50 MiB on busy VPS (Steam / CDN downloads with N≥4 concurrent sessions). Symptom: batch JSON parse error: EOF while parsing a string at line 1 column 52428630 (body_len=52428630) followed by session abort + download restart from 0. Fix: new BATCH_RESPONSE_BUDGET = 32 MiB total-batch cap; the drain loop tracks remaining budget across sessions and stops one short of the cliff. Sessions deferred this batch keep their buffered data and drain on the next poll (no data loss). New regression test drain_now_respects_caller_budget_below_per_session_cap. 36 tunnel-node tests (was 35) + 197 lib tests all green.

Full Changelog: v1.9.15...v1.9.16

v1.9.15

06 May 22:06

Choose a tag to compare

• HTTP/2 multiplexing روی relay leg (PR #799 از @dazzling-no-more): ALPN از Google edge h2 رو negotiate می‌کنه؛ اگه peer قبول کنه، تمام relay traffic (Apps Script direct، exit-node outer call، full-mode tunnel single ops، full-mode tunnel batches) روی یک TCP/TLS connection با ~۱۰۰ stream همزمان multiplex می‌شه به‌جای pool 8-80 socket. اگر h2 negotiate نشه، fallback خودکار به HTTP/1.1 keep-alive path قبلی. مزیت اصلی: یک Apps Script call کند دیگه head-of-line بقیه‌ی queue روی همان socket رو block نمی‌کنه — مهم‌ترین اثر روی streaming sites (YouTube/googlevideo) و concurrent fan-out (range-parallel downloads). Idempotency-safe retry: RequestSent::{No, Maybe}No (قبل از send_request موفق) safely retried، Maybe فقط برای متدهای idempotent. Kill switch: "force_http1": true در config.json تمام h2 path رو bypass می‌کنه و دقیقاً behavior pre-PR رو می‌ده. استراتژیک، این architectural fix برای regression #781 / #773 — h2 multiplexing pool tuning مسئله‌ی بسیار کم‌اهمیت می‌کنه. ۱۸۰→۱۹۷ test (+۱۷ test جدید).
• Block QUIC default true (PR #805 از @yyoyoian-pixel): QUIC روی tunnel TCP-based منجر به TCP-over-TCP meltdown (<1 Mbps) می‌شد. مرورگرها با drop UDP/443 خاموش، در ثانیه‌ها به TCP/HTTPS برمی‌گردن — نتیجه: page load و YouTube video load ابتدایی در Full mode به‌طرز محسوسی سریع‌تر. UI toggle در Android Advanced + Desktop checkbox (قبلاً config-only از #213). Android serialization همیشه block_quic رو emit می‌کنه تا default Rust silently override نشه.
• Accessibility: accesskit feature برای eframe فعال شد (PR #819 از @brightening-eyes — drop به نفع #750). screen reader users (NVDA / JAWS / VoiceOver / Orca) حالا کامل می‌تونن از UI استفاده کنن. tested with real screen reader by author.
• GitHub Actions Full tunnel docs (PR #783 از @euvel): ۳ مسیر برای کاربرانی که نمی‌توانن VPS بخرن — cloudflared Quick (zero accounts beyond GitHub، URL موقت)، ngrok (free account، URL موقت)، cloudflared Named (CF domain، URL دایم). در assets/github-actions-tunnel/. هر runner GitHub Actions ۶ ساعت timeout داره — repeatable ولی persistent نه. برای daily traffic سنگین همچنان VPS توصیه می‌شه.
• تست: ۱۹۷ lib + ۳۵ tunnel-node test همه pass.

• HTTP/2 multiplexing on relay leg (PR #799 from @dazzling-no-more): ALPN-negotiates h2 against the Google edge; if the peer agrees, all relay traffic (Apps Script direct, exit-node outer call, full-mode tunnel single ops, full-mode tunnel batches) rides one TCP/TLS connection multiplexing ~100 concurrent streams instead of the legacy 8-80-socket pool. Falls back to the existing HTTP/1.1 keep-alive path automatically when h2 isn't viable. Big win: a slow Apps Script call no longer head-of-line-blocks the rest of the queue on the same socket — most user-visible on streaming sites (YouTube/googlevideo) and concurrent fan-out (range-parallel downloads). Idempotency-safe retry via RequestSent::{No, Maybe}No (anything before send_request succeeds) is safely retried, Maybe only retries for idempotent methods. Kill switch: "force_http1": true in config.json bypasses the entire h2 path and gives back exactly the pre-PR behaviour. Strategically this is the architectural fix for the perceived-slowness regression in #781 / #773 — h2 multiplexing makes the pool tuning machinery much less load-bearing because one connection serves all requests. 180 → 197 lib tests (+17 covering ALPN selection, sticky disable, RequestSent classification on RST_STREAM, 421 handling, gzip parity, POST body transmission, redirect chain, force_http1 round-trip).
• Block QUIC by default (PR #805 from @yyoyoian-pixel): QUIC over the TCP-based tunnel was causing TCP-over-TCP meltdown (<1 Mbps). With UDP/443 silently dropped, browsers detect the failure and fall back to TCP/HTTPS within seconds — significantly faster initial page and YouTube video load times in Full mode. UI: "Block QUIC" toggle in Android Advanced settings, "Block QUIC (UDP/443)" checkbox in desktop UI (was config-only since #213). Android serialization always emits block_quic so the Rust default can't silently override the user's choice.
• UI accessibility: enabled the accesskit feature on eframe (PR #819 from @brightening-eyes — closes #750). Screen reader users (NVDA / JAWS / VoiceOver / Orca) can now navigate the desktop UI cleanly. Tested with a real screen reader by the contributor.
• GitHub Actions Full tunnel docs (PR #783 from @euvel): 3 paths for users who can't or won't buy a VPS — cloudflared Quick (zero accounts beyond GitHub, temporary URL), ngrok (free account, temporary URL), cloudflared Named (CF domain, permanent URL). Lives in assets/github-actions-tunnel/. Each GitHub Actions runner has a 6-hour timeout — repeatable but not persistent; serious daily traffic should still go VPS.
• Tests: 197 lib + 35 tunnel-node tests passing.

What's Changed

  • added accessibility for the ui by @brightening-eyes in #819
  • feat: block QUIC by default — faster initial page/video loads in full tunnel mode by @yyoyoian-pixel in #805
  • feat: HTTP/2 multiplexing on relay leg with idempotency-safe h1 fallback by @dazzling-no-more in #799
  • docs: add GitHub Actions Full tunnel documentation and workflows by @euvel in #783

New Contributors

Full Changelog: v1.9.13...v1.9.15