Releases: therealaleph/MasterHttpRelayVPN-RUST
v1.9.18
• 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,bytes1.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
- feat: zero-copy full-tunnel mux + base64 off mux thread by @dazzling-no-more in #881
Full Changelog: v1.9.16...v1.9.18
v1.9.17
• 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
• 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
• 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
- @brightening-eyes made their first contribution in #819
Full Changelog: v1.9.13...v1.9.15
v1.9.14
• Fix v1.9.13 regression — کاربران v1.9.10 → v1.9.13 upgrade میکردن و حس میکردن کندتره (#773). علت: block_doh در Rust با #[serde(default)] برای فیلد bool به false resolve میشد (default trait از Rust)، نه true که PR #763 قصد داشت. کاربران existing با config.json بدون فیلد block_doh و tunnel_doh = true (default جدید از #468)، هر DNS lookup رو از مسیر Apps Script میفرستادن — ~۱.۵ ثانیه overhead هر page load. حالا block_doh با named-default function به true resolve میشه — مرورگر DoH reject میشه + system DNS via tun2proxy فوراً پاسخ میده + هیچ tunnel round-trip دیگه. کاربران power که عمداً DoH از تونل میخوان، میتونن block_doh: false صریح بگذارن. تست: 180 lib + 35 tunnel-node + UI release-mode build همه green.
• Fix v1.9.13 perceived-slowness regression on upgrade (#773): block_doh was using #[serde(default)] on a bool, which resolves to Rust's Default::default() = false rather than the true PR #763 intended. Existing configs upgrading from v1.9.10 had no block_doh field, so they got false paired with tunnel_doh = true (the new default from #468) — every browser DoH lookup got tunneled through Apps Script, adding ~1.5s overhead per page load. Now block_doh uses a named-default function that returns true — DoH is rejected at the SOCKS5 listener so the browser falls back to system DNS (instant, via tun2proxy's virtual DNS) and no tunnel round-trip happens. Power users who specifically want DoH-through-tunnel can opt back in with block_doh: false. Tests: 180 lib + 35 tunnel-node + UI release-mode build all green.
Full Changelog: v1.9.13...v1.9.14
v1.9.13
• Hotfix v1.9.11 / v1.9.12 build failure: PR #763 فیلد جدید block_doh به Config اضافه کرد ولی src/bin/ui.rs::FormState (که Config رو با struct literal میسازه) بهروز نشد، در نتیجه mhrv-rs-ui در CI با error[E0063]: missing field 'block_doh' کامپایل نشد. هر دو release CI v1.9.11 و v1.9.12 fail شدن — هیچ binary منتشر نشد. این release همان تغییرات رو با fix UI ship میکنه. پیامد محصول: v1.9.13 = v1.9.11 + v1.9.12 + UI compile fix. تست: 180 lib + 35 tunnel-node + UI release-mode build همه green.
• Hotfix v1.9.11 / v1.9.12 build failure: PR #763 added a new block_doh field to Config but didn't update src/bin/ui.rs::FormState (which constructs Config via a struct literal), so mhrv-rs-ui failed to compile in CI with error[E0063]: missing field 'block_doh'. Both v1.9.11 and v1.9.12 release CI runs failed and shipped no binaries. This release is the same set of changes with the UI compile fix included. Product impact: v1.9.13 = v1.9.11 + v1.9.12 + UI compile fix. Tests: 180 lib + 35 tunnel-node + UI release-mode build all green.
Full Changelog: v1.9.12...v1.9.13
v1.9.10
• exit-node docs بازنویسی شد بهصورت platform-agnostic. اسکریپت TypeScript حالا assets/exit_node/exit_node.ts نام داره (قبلاً valtown.ts) و راهنماها روی Deno Deploy / fly.io / VPS شخصی بهعنوان hostهای توصیهشده تمرکز میکنن. کد TypeScript خود بدون تغییر است — همان web-standard Request / Response / fetch API که روی هر runtime serverless اجرا میشه. کاربرانی که قبلاً exit-node را روی پلتفرم انتخابی خود deploy کردهاند نیازی به تغییر ندارند.
• Telegram channel announcements حالا brief English bullets میگیرن بهجای Persian کامل (commit 9580ce8). subscriberها در یک نگاه میبینن چی ship شده — full Persian + English changelog همچنان در docs/changelog/v*.md برای archive باقی میمونه.
• تست: ۱۷۹ lib + ۳۵ tunnel-node test همه pass.
• Rewrote the exit-node docs to be platform-agnostic. The TypeScript handler is now named assets/exit_node/exit_node.ts (was valtown.ts) and the setup guide focuses on Deno Deploy / fly.io / your own VPS as the recommended hosts. The TypeScript itself is unchanged — same web-standard Request / Response / fetch API that runs on any serverless runtime. Users who already have an exit node deployed on whichever host they picked don't need to change anything.
• Telegram channel announcements now use brief English bullets instead of full Persian (commit 9580ce8). Subscribers see what shipped at a glance — the full Persian + English changelog stays in docs/changelog/v*.md for archival.
• Tests: 179 lib + 35 tunnel-node tests passing.
Full Changelog: v1.9.9...v1.9.10
v1.9.9
• Fix v1.9.8 Android: کرش جدید ~۲ ثانیه بعد از Disconnect (#700 از @ilok67 با root cause + fix کامل): علیرغم fix v1.9.8 برای race lifecycle (#666)، crash جداگانه در MhrvVpnService.teardown() باقی مانده بود. ترتیب قبلی: tun2proxy.stop → tun.close → join → Native.stopProxy. مشکل: tun2proxy worker thread در native code blocked روی socket read از SOCKS5 proxy است. وقتی Tun2proxy.stop کالد میشه + 2s timeout میگذره + 4s join timeout میگذره (worker هنوز alive)، Native.stopProxy runtime Rust رو shutdown میکنه شامل listener socket — worker thread که در native blocking read از همان socket است → use-after-free → SIGSEGV. comment کد قدیمی ادعا میکرد "runtime shutdown will knock the rest of the world over" که اشتباه بود — Native.stopProxy نمیتونه force-terminate یک thread native دیگه. ترتیب جدید: Native.stopProxy اول (socket رو میبنده → blocking read worker با error برمیگرده → worker پاک exit میکنه از error path)، بعد Tun2proxy.stop (cooperative، redundant ولی ارزان) → tun.close → join (تقریباً همیشه فوری چون worker از قبل تموم شده). تشکر بیشتر از @ilok67 برای triage دقیق دومین crash.
• Fix tunnel-node batch drain correctness + lock contention (PR #695 از @dazzling-no-more): چهار باگ، دو correctness، دو latency.
- Cleanup race tail-bytes drop میکرد: session با buffer > ۱۶ MiB + EOF —
drain_nowصحیحeof=falseبرمیگردوند تا tail tail رو در poll بعدی drain کنه، ولی cleanup loop همان atomic رو میخوند،trueمیدید + session رو حذف میکرد +reader_taskرو abort + tail هدر میرفت. حالا cleanup از مقدار returndrain_nowپیروی میکنه — session فقط بعد از shipped شدن drain کهeof=trueمیفرسته، حذف میشه. data loss silent در 1Gbps+ VPS که buffer بین pollها پر میشد، fix شد. - Sessions-map lock روی upstream await نگه میداشت: phase-1
dataop global sessions map رو نگه میداشت رویlast_active.lock،writer.lock،write_all، وflush— head-of-line-block برای هر batch + connect/close op دیگه. حالا (مثلudp_dataکه قبلاً درست بود) Arc از under map clone میشه، lock drop، بعد write/flush. - TCP+UDP batch deadline UDP رو میپرداخت:
tokio::join!(wait_tcp, wait_udp)conjunctive هست — TCP-ready burst هنوز LONGPOLL_DEADLINE 15 ثانیهای UDP رو میپرداخت قبل از پاسخ. comment میگفت "either side"، code "both sides" انجام میداد. تغییر بهselect!. test جدیدbatch_tcp_ready_does_not_pay_udp_longpoll_deadlineاین رد رو حفظ میکنه. - Watcher tasks تحت
select!cancellation leak میکرد:wait_for_any_drainableفقط در trailing loop watcherها رو abort میکرد — past همه cancel pointها. با تبدیل phase-2 wait بهselect!، loser arm's future drop میشه و watcherهاش detach میشن (drop کردنJoinHandleabort نمیکنه). هر orphan یکArc<...Inner>نگه میداشت + میتوانستnotify_one()permit از batch بعدی بدزده. fix:AbortOnDropnewtype روی همهJoinHandlewatcher.
۲ test جدید + 35/35 pass.
• Example config exit-node باaistudio.google.comوai.google.dev— درخواست از #701. AI Studio روی Iran IP sanction میخوره (نه Apps Script طرف ما). exit-node IP val.town رو میبینه که نه Iran است نه Google datacenter.
• Example config fronting-groups با Reddit / Fastly / Pinterest / CNN / BuzzFeed family domains اضافه شد (PR #696 از @Shjpr9). همه روی Fastly Anycast 151.101.x.x — کاربران میتونن از example بیشتر دامنه برداشت کنن، اونی که در شبکهشان کار میکنه نگه دارن.
• تست: ۱۷۹ lib + ۳۵ tunnel-node test همه pass.
• Fix Android ~2-second-delayed crash on Disconnect from v1.9.8 (#700 by @ilok67 with full root cause + fix): despite the v1.9.8 fix for the lifecycle race (#666), a separate crash inside MhrvVpnService.teardown() remained. Old order was tun2proxy.stop → tun.close → join → Native.stopProxy. Problem: tun2proxy's worker thread is blocked in native code on a socket read from the proxy's SOCKS5 port. After Tun2proxy.stop()'s 2s timeout and the 4s thread join both expire (worker still alive), Native.stopProxy() shuts down the Rust runtime — including the listener socket — and the worker, still reading from that socket in native code, hits use-after-free → SIGSEGV. The old code comment claimed "the runtime shutdown will knock the rest of the world over," which was wrong: Native.stopProxy cannot forcibly terminate a separate native thread. New order: Native.stopProxy FIRST (closes the socket → worker's blocking read returns with EOF/error → worker exits cleanly through its error path), then Tun2proxy.stop (cooperative, mostly redundant but cheap), tun.close, then join (almost always immediate now). Thanks @ilok67 again for the precise root-cause work on the second crash.
• Fix tunnel-node batch drain correctness + lock contention (PR #695 from @dazzling-no-more): four bugs, two correctness + two latency.
- Cleanup race dropped tail bytes: when a session's read buffer > 16 MiB and upstream signaled EOF,
drain_nowcorrectly returnedeof=falseand left the tail for the next poll, but the cleanup loop read the raw atomic, sawtrue, removed the session, abortedreader_task, dropped the tail. Cleanup now tracks eof'd sids fromdrain_now's return value — the session is only removed once the drain that returnedeof=truehas shipped to the client. Silent data loss on 1Gbps+ VPS that filled the buffer between polls — fixed. - Sessions-map lock held across upstream awaits: phase-1
dataop held the global sessions map acrosslast_active.lock,writer.lock,write_all, andflush— head-of-line-blocking every other batch and connect/close op. Now (mirroringudp_data's already-correct shape) it clones theArcunder the map lock, drops the lock, then awaits. - Mixed TCP+UDP batch paid the slower side's deadline:
tokio::join!(wait_tcp, wait_udp)is conjunctive — a TCP-ready burst still paid the UDPLONGPOLL_DEADLINE(15 s) before responding. Comment said "either side", code did "both sides". Switched totokio::select!. New testbatch_tcp_ready_does_not_pay_udp_longpoll_deadlinelocks down the regression. - Watcher tasks leaked under
select!cancellation:wait_for_any_drainableonly aborted its watcher tasks in a trailing loop, past every cancellation point. With phase-2 wait flipped toselect!, the loser arm's future drops and detaches its watchers (dropping aJoinHandledoesn't abort). Each orphan held anArc<...Inner>and could steal anotify_one()permit from a future batch. Fix:AbortOnDropnewtype wraps every watcherJoinHandle.
2 new tests + 35/35 pass.
• Example config exit-node now listsaistudio.google.comandai.google.dev— requested in #701. AI Studio sanctions Iran IPs (independently of any Apps Script issue on our side). Routing it through the exit-node makes the destination see val.town's IP, which is neither Iran nor a Google datacenter.
• Example config fronting-groups gained Reddit / Fastly / Pinterest / CNN / BuzzFeed family domains (PR #696 from @Shjpr9). All on the Fastly Anycast151.101.x.xedge — gives users a richer starter list to trim down based on what works in their network.
• Tests: 179 lib + 35 tunnel-node tests all passing.
What's Changed
- fix(tunnel-node): batch drain correctness and lock contention by @dazzling-no-more in #695
- Update config.fronting-groups.example.json by @Shjpr9 in #696
New Contributors
Full Changelog: v1.9.8...v1.9.9
v1.9.8
• Fix v1.9.7 Android: کرش روی tap Disconnect (#666 از @ilok67 با root cause + fix کامل): MainActivity.onStop بعد از startService(ACTION_STOP) بلافاصله stopService() رو هم میزد. ACTION_STOP داخل MhrvVpnService یک thread پسزمینه به نام mhrv-teardown میسازه که teardown() (بستن tun2proxy، fd TUN، runtime) رو اجرا میکنه و در پایانش stopSelf() رو فرامیخونه. ولی stopService() بلافاصله onDestroy() رو روی همان service trigger میکرد — دو thread همزمان دارن از lifecycle میگذرن، و OS process service رو میکشه قبل از اینکه teardown تمام بشه. crash بعد از تب Disconnect، در حدود ۹۹٪ از تستها قابل reproduce. حالا stopService() حذف شده — ACTION_STOP تنها کافی است (هم برای service زنده هم برای حالت زامبی). idempotency guard tornDown AtomicBoolean قبلاً موجود بود ولی محافظت OS-level lifecycle race رو نمیکرد. تشکر از @ilok67 برای triage عالی.
• Fix v1.9.7 UI: دکمهٔ Test Relay در حالت full (و direct) "test result: fail" قرمز نشون میداد (#665 از @cmptrnb). mhrv-rs test فقط برای حالت apps_script سیمکشی شده — در full mode عمداً refuse میکنه چون probe مستقیم Apps Script در حالی که data plane از tunnel-node رد میشه گمراهکننده است. ولی پیام refuse توسط UI بهعنوان test failure ترجمه میشد + کاربر فکر میکرد proxy خراب است. حالا UI mode رو قبل از اجرای test چک میکنه + برای حالتهای نامناسب پیام explainer میده بهجای fail قرمز:
Test Relay is wired only for apps_script mode. In full mode the data plane is the tunnel-node — to verify it end-to-end, start the proxy and load https://whatismyipaddress.com in your browser via 127.0.0.1:8085. The IP shown should be your tunnel-node's VPS IP.
- Tune adaptive batch coalesce (PR #674 از @yyoyoian-pixel): از 40 ms → 10 ms برای client coalesce step و tunnel-node straggler settle step. tunnel-node settle max از 500 ms → 1000 ms. منطق asymmetric: وقتی هیچ op دیگری نیست، fast-fire (10 ms کافی برای catch کردن opهایی که در همان event-loop tick میرسن مثل ۶ موازی parallel browser connection)؛ ولی وقتی هر دو طرف data دارن (uploads، page load بستی)، adaptive reset همچنان batch میکنه تا 1 s cap. در short: «وقتی چیزی برای انتظار نیست منتظر نباش، وقتی هست با تمام توان batch کن.» سازگار به عقب: کاربران با
coalesce_step_ms: 40در config.json رفتار قدیمی رو نگه میدارن.
• تست: ۱۷۹ lib + ۳۳ tunnel-node test همه pass.
• Fix Android crash on tap-Disconnect from v1.9.7 (#666 by @ilok67 with full root cause + fix): MainActivity.onStop was calling stopService() immediately after startService(ACTION_STOP). ACTION_STOP inside MhrvVpnService spawns the mhrv-teardown background thread that runs teardown() (stops tun2proxy, closes TUN fd, shuts down the Rust runtime) and then calls stopSelf() at the end. But stopService() immediately triggered onDestroy() on the same service — two threads racing through the lifecycle, and the OS would kill the process before teardown finished. Crash on every Disconnect tap, ~99% reproducible. Removed the stopService() call — ACTION_STOP alone is sufficient for both the live-service and the zombie-after-process-death cases. The existing tornDown AtomicBoolean idempotency guard protects against double-teardown of native state, but it can't protect against OS-level lifecycle races on stopSelf vs stopService. Thanks @ilok67 for the precise triage.
• Fix UI showing "test result: fail" red status for full (and direct) modes from v1.9.7 (#665 by @cmptrnb). mhrv-rs test is wired only for the apps_script relay path — it deliberately refuses in full mode because probing Apps Script directly while the actual data plane goes via tunnel-node would give a misleading green result. But the refuse path was getting translated by the UI as a generic "test failed" with red status, scaring users into thinking their proxy was broken. Now the UI checks mode before running the test and shows a friendly explainer for full/direct:
Test Relay is wired only for apps_script mode. In full mode the data plane is the tunnel-node — to verify it end-to-end, start the proxy and load https://whatismyipaddress.com in your browser via 127.0.0.1:8085. The IP shown should be your tunnel-node's VPS IP.
• Tune adaptive batch coalesce (PR #674 from @yyoyoian-pixel): client coalesce step + tunnel-node straggler settle step from 40 ms → 10 ms, tunnel-node settle max from 500 ms → 1000 ms. The asymmetric design — small step, generous max — picks up "fire-and-forget when nothing else is queued" without giving up batching on bursts. The 10 ms still catches ops that arrive in the same event-loop tick (e.g. a browser opening 6 parallel connections on page load), so we don't degenerate into single-op batches; but on a download where the client is just waiting for the next chunk, the per-batch dead-air shrinks by ~30 ms. Backwards-compatible: existing configs with explicit coalesce_step_ms: 40 keep the old behaviour.
• Tests: 179 lib + 33 tunnel-node tests all passing.
What's Changed
- tune: lower coalesce/settle step 40 → 10 ms by @yyoyoian-pixel in #674
Full Changelog: v1.9.7...v1.9.8
v1.9.7
• چکباکس «Share with other devices on my Wi-Fi / network» به UI دسکتاپ اضافه شد. بهجای اینکه کاربر listen_host را بهصورت دستی روی 0.0.0.0 تنظیم کند (که اکثر کاربران نمیدانستند)، حالا فقط یک چکباکس ساده روی فرم اصلی است. وقتی روشن میشود:
- Bind بهطور خودکار به
0.0.0.0تغییر میکند (تمام interfaceها) - IP محلی شبکهات با
detect_lan_ip()تشخیص داده میشود (یک trick UDPconnectکه از kernel میپرسد source-IP outbound کدام است — هیچ ترافیک شبکهای واقعی فرستاده نمیشود) و در زیر چکباکس همراه با پورتها نمایش داده میشود تا بتوانی مستقیم به گوشی / لپتاپ مهمان بدهی:Other devices: HTTP 192.168.x.y:8085 · SOCKS5 192.168.x.y:8086 - tooltip توضیح میدهد macOS اولین بار prompt firewall میاندازد
- اگر کاربر از قبل یک bind IP خاص (مثلاً
192.168.1.50یک NIC مشخص) درconfig.jsonنوشته باشد، چکباکس قفل میشود + برچسب «Custom bind: 192.168.1.50» نشان میدهد تا تنظیم دستی توسط Save بعدی پاک نشود.
ماژول جدیدsrc/lan_utils.rsبا ۳ تست (تشخیص wildcard، تشخیص loopback، تست detect واقعی).
• Code.gs / CodeFull.gs hardening + باگفیکس (هیچ تغییری در کانفیگ کاربر لازم نیست — فقط Code.gs خودتان را باassets/apps_script/Code.gs(یاCodeFull.gsبرای حالت full) جایگزین کنید + در Apps Script editor:Manage deployments → ✏️ → Version: New version → Deploy. Deployment ID همان قبلی میماند): Code.gsdoGet تکراری حذف شد: نسخهای که باHtmlService.createHtmlOutputتعریف شده بود بهخاطر hoisting جاوااسکریپت روی نسخهٔ صحیحContentServiceoverwrite میکرد. در نتیجه هر GET به URL deployment پاسخ سندباکسgoog.script.initiframe برمیگرداند بهجای HTML پلیسهولدر ساده.CodeFull.gsdoGetبهContentServiceتغییر کرد (قبلاًHtmlServiceبود) — به همان دلیل بالا.- هدرهای IP-leak در
SKIP_HEADERSاضافه شد (X-Forwarded-For,X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-Port,X-Real-IP,Forwarded,Via) — لایهٔ دفاع دوم به stripping سمت کلاینت v1.2.9 (#104). _doBatchدارای fallback شد: اگرUrlFetchApp.fetchAll()بهعنوان یک کل throw کند، حالا برای متدهای امن (GET / HEAD / OPTIONS) per-item fetch میکند بهجای صفر کردن کل پاسخ batch. port ازmasterking32/MasterHttpRelayVPN@3094288.
•parse_relay_json(سمت Rust): unwrapper برایgoog.script.init("...userHtml...")اضافه شد — اگر هر deploymentای پاسخ HtmlService-wrapped برگرداند (legacy Code.gs قبل از v1.9.6، یا redirect که doGet را GET بزند)، client حالا JSON داخلی را استخراج میکند بهجای fail کردن باkey must be a string at line 2 column 1.
• README بازنویسی شد: نسخهٔ کوتاه دوزبانه (انگلیسی + فارسی RTL) برای کاربر معمولی + راهنمای کامل پیشرفته درdocs/guide.mdوdocs/guide.fa.md. جدا کردن "راهاندازی ۵ دقیقهای" از "همهٔ گزینهها و troubleshooting" راهنما را خیلی قابلفهمتر کرد. در guide.fa.md task list با[x]با جدول جایگزین شد چون رندر RTL در GitHub با چکباکس مارکداون خراب میشد.
• تست: ۶ regression test جدید (۳ برای unwrap goog.script.init + ۳ برای lan_utils). ۱۷۹ lib test + ۳۳ tunnel-node test همه pass.
• Added a "Share with other devices on my Wi-Fi / network" checkbox to the desktop UI. Instead of asking users to know they can set listen_host to 0.0.0.0 (which almost no one did), it's now a single checkbox on the main form. When enabled:
- Bind address auto-flips to
0.0.0.0(all interfaces) - Your LAN IP is detected via
detect_lan_ip()(UDPconnecttrick — asks the kernel which source IP it would use for an outbound packet, no actual network traffic sent) and shown alongside the proxy ports so you can hand them to the guest device directly:Other devices: HTTP 192.168.x.y:8085 · SOCKS5 192.168.x.y:8086 - Tooltip explains macOS will pop a Firewall prompt the first time
- If you've already written a specific bind IP (e.g.
192.168.1.50for one NIC) intoconfig.json, the checkbox locks itself and shows a "Custom bind: 192.168.1.50" badge so the next Save can't clobber your manual setting.
Newsrc/lan_utils.rsmodule with 3 unit tests (wildcard detection, loopback detection, live detect smoke).
• Code.gs / CodeFull.gs hardening + bug fixes (no client config change needed — just replace your own Code.gs withassets/apps_script/Code.gs(orCodeFull.gsfor full mode) and in the Apps Script editor:Manage deployments → ✏️ → Version: New version → Deploy. Your Deployment ID stays the same): - Removed duplicate
doGetinCode.gs: a second copy declared withHtmlService.createHtmlOutputwas silently overriding the correctContentServiceone due to JS function hoisting. Result: every GET to the deployment URL was returning thegoog.script.initsandbox iframe instead of the simple placeholder HTML. CodeFull.gsdoGetswitched toContentService(wasHtmlService) — same reason as above.- Added IP-leak headers to
SKIP_HEADERS(X-Forwarded-For,X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-Port,X-Real-IP,Forwarded,Via) — second line of defense to v1.2.9's client-side stripping (#104). _doBatchgot a fallback path: ifUrlFetchApp.fetchAll()throws as a whole, it now per-item-fetches safe methods (GET / HEAD / OPTIONS) instead of zeroing the entire batch's responses. Ported frommasterking32/MasterHttpRelayVPN@3094288.
•parse_relay_json(Rust client): added unwrapper forgoog.script.init("...userHtml...")iframe — if any deployment ever returns an HtmlService-wrapped response (legacy Code.gs, or a redirect that GET-hits doGet), the client now extracts the inner JSON instead of failing withkey must be a string at line 2 column 1.
• Rewrote the README: short bilingual landing page (English + Persian RTL) for normal users, with the full advanced reference moved todocs/guide.mdanddocs/guide.fa.md. Splitting "5-minute quick start" from "every option + troubleshooting" makes the docs much more approachable. In guide.fa.md the[x]task list was replaced with a table because GitHub's RTL renderer mangled the checkbox positions inside<div dir="rtl">.
• Tests: 6 new regression tests (3 for goog.script.init unwrap + 3 for lan_utils). 179 lib tests + 33 tunnel-node tests all passing.
Full Changelog: v1.9.5...v1.9.7