Skip to content

Releases: therealaleph/MasterHttpRelayVPN-RUST

v1.9.9

04 May 01:02

Choose a tag to compare

• 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 از مقدار return drain_now پیروی می‌کنه — session فقط بعد از shipped شدن drain که eof=true می‌فرسته، حذف می‌شه. data loss silent در 1Gbps+ VPS که buffer بین poll‌ها پر می‌شد، fix شد.
  • Sessions-map lock روی upstream await نگه می‌داشت: phase-1 data op 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 کردن JoinHandle abort نمی‌کنه). هر orphan یک Arc<...Inner> نگه می‌داشت + می‌توانست notify_one() permit از batch بعدی بدزده. fix: AbortOnDrop newtype روی همه JoinHandle watcher.
    ۲ 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_now correctly returned eof=false and left the tail for the next poll, but the cleanup loop read the raw atomic, saw true, removed the session, aborted reader_task, dropped the tail. Cleanup now tracks eof'd sids from drain_now's return value — the session is only removed once the drain that returned eof=true has 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 data op held the global sessions map across last_active.lock, writer.lock, write_all, and flush — head-of-line-blocking every other batch and connect/close op. Now (mirroring udp_data's already-correct shape) it clones the Arc under 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 UDP LONGPOLL_DEADLINE (15 s) before responding. Comment said "either side", code did "both sides". Switched to tokio::select!. New test batch_tcp_ready_does_not_pay_udp_longpoll_deadline locks down the regression.
  • Watcher tasks leaked under select! cancellation: wait_for_any_drainable only aborted its watcher tasks in a trailing loop, past every cancellation point. With phase-2 wait flipped to select!, the loser arm's future drops and detaches its watchers (dropping a JoinHandle doesn't abort). Each orphan held an Arc<...Inner> and could steal a notify_one() permit from a future batch. Fix: AbortOnDrop newtype wraps every watcher JoinHandle.
    2 new tests + 35/35 pass.
    • Example config exit-node now lists aistudio.google.com and ai.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 Anycast 151.101.x.x edge — 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

New Contributors

Full Changelog: v1.9.8...v1.9.9

v1.9.8

03 May 13:15

Choose a tag to compare

• 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 در حالت fulldirect) "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

Full Changelog: v1.9.7...v1.9.8

v1.9.7

01 May 16:32

Choose a tag to compare

• چک‌باکس «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 UDP connect که از 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.gs doGet تکراری حذف شد: نسخه‌ای که با HtmlService.createHtmlOutput تعریف شده بود به‌خاطر hoisting جاوااسکریپت روی نسخهٔ صحیح ContentService overwrite می‌کرد. در نتیجه هر GET به URL deployment پاسخ سندباکس goog.script.init iframe برمی‌گرداند به‌جای HTML پلیس‌هولدر ساده.
  • CodeFull.gs doGet به 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() (UDP connect trick — 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.50 for one NIC) into config.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.
    New src/lan_utils.rs module 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 with assets/apps_script/Code.gs (or CodeFull.gs for full mode) and in the Apps Script editor: Manage deployments → ✏️ → Version: New version → Deploy. Your Deployment ID stays the same):
  • Removed duplicate doGet in Code.gs: a second copy declared with HtmlService.createHtmlOutput was silently overriding the correct ContentService one due to JS function hoisting. Result: every GET to the deployment URL was returning the goog.script.init sandbox iframe instead of the simple placeholder HTML.
  • CodeFull.gs doGet switched to ContentService (was HtmlService) — 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).
  • _doBatch got a fallback path: if UrlFetchApp.fetchAll() throws as a whole, it now per-item-fetches safe methods (GET / HEAD / OPTIONS) instead of zeroing the entire batch's responses. Ported from masterking32/MasterHttpRelayVPN@3094288.
    parse_relay_json (Rust client): added unwrapper for goog.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 with key 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 to docs/guide.md and docs/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

v1.9.5

01 May 12:55

Choose a tag to compare

• fix exit-node v1.9.4: مدارا با TLS ungraceful close (peer closed without close_notify) که val.town از Apps Script عبور می‌دهد (#585 از @gregtheph): در v1.9.4، کاربری که val.town رو با درست‌ترین config setup کرد، در log می‌دید WARN exit node failed for https://chatgpt.com/: io: peer closed connection without sending TLS close_notify — falling back to direct Apps Script + سپس fallback به Apps Script که خود نمی‌تونه ChatGPT رو reach کنه، در نتیجه decoy/no-json error. علت: rustls سختگیر است درباره‌ی TLS shutdown — وقتی peer (val.town) underlying TCP رو می‌بنده بدون اول send کردن TLS close_notify alert، rustls io::ErrorKind::UnexpectedEof می‌فرسته. کد ما در read_http_response این error رو propagate می‌کرد به‌عنوان hard error. حالا UnexpectedEof به‌صورت graceful EOF (مشابه n == 0) درمان می‌شه — اگر body completed شده با Content-Length، response درست برمی‌گرده. اگر mid-body close بود، error real (truncation) همچنان propagate می‌شه. ۴ regression test جدید (شامل UnexpectedEof tolerance + envelope unwrap valtown). 173 lib tests + 33 tunnel-node tests pass.

• Fix v1.9.4 exit-node: tolerate ungraceful TLS close (peer closed without close_notify) on the val.town path (#585 by @gregtheph): in v1.9.4, users with a correctly-configured val.town deployment saw WARN exit node failed for https://chatgpt.com/: io: peer closed connection without sending TLS close_notify — falling back to direct Apps Script in the log, followed by a fallback to direct Apps Script which can't reach ChatGPT either, resulting in the decoy/no-json error. Root cause: rustls is strict about TLS shutdown — when the peer (val.town's host) closes the underlying TCP without first sending a TLS close_notify alert, rustls surfaces this as io::ErrorKind::UnexpectedEof. Our code in read_http_response was propagating this as a hard error rather than treating it as graceful EOF. Now UnexpectedEof is handled like n == 0: if the body has been fully received per Content-Length, the response returns successfully; if it's a real mid-body truncation, the error still propagates as BadResponse. Same handling added to the chunked reader and the no-framing reader. Four regression tests cover the new behavior (UnexpectedEof tolerance for Content-Length and no-framing branches + val.town envelope unwrap success and error paths). 173 lib tests + 33 tunnel-node tests passing.

Full Changelog: v1.9.4...v1.9.5

v1.9.4

01 May 09:09

Choose a tag to compare

• exit node اختیاری برای دور زدن CF anti-bot روی ChatGPT / Claude / Grok / X (port از upstream masterking32/MasterHttpRelayVPN@464a6e1d, با hardening): سایت‌های پشت Cloudflare مانند chatgpt.com، claude.ai، grok.com، x.com، openai.com traffic از Google datacenter IPs (Apps Script's outbound IP space) رو به‌عنوان bot flag می‌کنن + Turnstile / CAPTCHA / 502 challenge برمی‌گردونن. تا v1.9.3 این "Relay error: json: key must be a string at line 2 column 1" یا 502 generic می‌داد + هیچ workaround در apps_script mode نبود. حالا یک endpoint TypeScript کوچک (assets/exit_node/valtown.ts) روی val.town / Deno Deploy / fly.io deploy می‌شه + بین Apps Script + destination قرار می‌گیره. مسیر traffic: client → SNI rewrite → Apps Script (Google IP) → val.town (non-Google IP) → destination. destination IP val.town رو می‌بینه، نه Google datacenter — heuristic anti-bot CF نمی‌سوزه + صفحه واقعی برمی‌گرده. leg user-side (Iran ISP → Apps Script) بدون تغییر — second hop کاملاً درون outbound Apps Script اجرا می‌شه، invisible از شبکه‌ی کاربر. config جدید:

"exit_node": {
  "enabled": true,
  "relay_url": "https://your-handle-mhrv.web.val.run",
  "psk": "<openssl rand -hex 32>",
  "mode": "selective",
  "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
}

دو mode: selective (default — فقط hosts مشخص از طریق exit node می‌رن) و full (همه می‌رن). در صورت failure exit node fallback اتومات به Apps Script direct (سایت‌های CF affected fail می‌گیرن، بقیه کار می‌کنن). hardening over upstream: PSK fail-closed اگر همچنان placeholder باشه (در fresh deploy نمی‌تونه به‌عنوان open relay accidentally سرو بشه)، loop guard (refuse fetch host خود)، 503 explicit برای misconfigured deploys. setup walkthrough در assets/exit_node/README.fa.md. config مثال در config.exit-node.example.json.
• حذف legacy telegram job در release.yml — قبلاً وقتی TELEGRAM_NOTIFY_ENABLED repo variable روی true set بود (در حال حاضر بود)، هر release دو پست duplicate APK روی main channel ایجاد می‌کرد: یکی قدیمی (universal APK + changelog) از release.yml و یکی جدید (cross-link به files channel) از telegram-publish-files.yml. فقط cross-link جدید رو می‌خواستیم. legacy job + helper script .github/scripts/telegram_release_notify.py حذف شدن. telegram-publish-files.yml (per-platform per-file posts با SHA-256 captions) تنها مسیر باقی مونده.

• Optional exit node to bypass CF anti-bot on ChatGPT / Claude / Grok / X (ported from upstream masterking32/MasterHttpRelayVPN@464a6e1d, with hardening): Cloudflare-fronted services like chatgpt.com, claude.ai, grok.com, x.com, openai.com flag traffic from Google datacenter IPs (Apps Script's outbound IP space) as bots and return Turnstile / CAPTCHA / 502 challenges. Through v1.9.3 this surfaced as "Relay error: json: key must be a string at line 2 column 1" or generic 502 with no apps_script-mode workaround. Now a small TypeScript HTTP endpoint (assets/exit_node/valtown.ts) deployed on val.town / Deno Deploy / fly.io sits between Apps Script and the destination. Traffic chain: client → SNI rewrite → Apps Script (Google IP) → val.town (non-Google IP) → destination. The destination sees val.town's IP, not Google datacenter — CF's anti-bot heuristic doesn't fire and the real page comes back. The user-side leg (Iran ISP → Apps Script) is unchanged — the second hop happens entirely inside Apps Script's outbound, invisible from the user's network, so the DPI evasion property mhrv-rs is built around stays intact. New config:

"exit_node": {
  "enabled": true,
  "relay_url": "https://your-handle-mhrv.web.val.run",
  "psk": "<openssl rand -hex 32>",
  "mode": "selective",
  "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
}

Two modes: selective (default, only listed hosts route via exit node, recommended) or full (everything via exit node, slower). On exit-node failure, mhrv-rs falls back to direct Apps Script automatically — CF-affected sites fail in that case but everything else keeps working, so a down exit node doesn't take you fully offline. Hardening over upstream: PSK fail-closed if still the placeholder (fresh val.town deploy can't accidentally serve as open relay until the user replaces the placeholder), loop guard (refuses to fetch its own host), explicit 503 on misconfigured deploys. Setup walkthrough in assets/exit_node/README.md (English) and README.fa.md (Persian). Complete example config at config.exit-node.example.json.
• Removed the legacy telegram job from release.yml. Previously, with the TELEGRAM_NOTIFY_ENABLED repo variable flipped to true (which it had been), every release produced two duplicate APK posts on the main Telegram channel: the old release.yml job (universal APK + bundled changelog) and the newer telegram-publish-files.yml workflow (per-platform per-file posts to the files channel + a single cross-link to the main channel). Only the cross-link was wanted. The legacy job and its helper script .github/scripts/telegram_release_notify.py are gone. telegram-publish-files.yml is now the only Telegram path. The legacy bundled-on-main pattern is recoverable from git log if anyone ever wants it back.

Full Changelog: v1.9.3...v1.9.4

v1.9.3

30 Apr 17:16

Choose a tag to compare

• toggle youtube_via_relay در Android Advanced settings (PR #535 از @yyoyoian-pixel، closes #520): تا قبل، desktop UI checkbox youtube_via_relay داشت ولی Android UI نه — کاربران Android مجبور بودن config.json رو دستی edit کنن (که بدون root کارش نشدنی بود). حالا Switch toggle در بخش Advanced settings در Android UI هست + match با desktop UI checkbox. شامل field youtubeViaRelay در MhrvConfig با JSON serialization (youtube_via_relay به‌عنوان wire format)، deserialization، + encode برای config-sharing. resources rشته EN + FA برای label + helper text. تغییر pure Android/Kotlin؛ بدون Rust impact.
• fix CI: gh release download در workflow Telegram publish با --clobber کار می‌کنه تا retries بعد از partial download کار کنه (no user impact، ولی v1.9.2 release برای Telegram channel به‌خاطر این bug fail شد + manual re-publish لازم شد).

youtube_via_relay toggle in Android Advanced settings (PR #535 by @yyoyoian-pixel, closes #520): the desktop UI has had a youtube_via_relay checkbox for a while, but the Android UI was missing it — Android users had to hand-edit config.json (which is rootful on Android). Now there's a Switch toggle in the Advanced settings section matching the desktop UI checkbox. Adds youtubeViaRelay field to MhrvConfig with JSON serialization (youtube_via_relay as the wire-format key), deserialization, and config-sharing encode. EN + FA string resources for label and helper text. Pure Android/Kotlin change; no Rust impact.
• CI fix: gh release download in the Telegram publish workflow now uses --clobber so retries can survive partial downloads (no user impact, but the v1.9.2 release's Telegram channel publish failed because of this and required manual re-trigger).

What's Changed

Full Changelog: v1.9.2...v1.9.3

v1.9.2

30 Apr 14:20

Choose a tag to compare

• backend جایگزین Apps Script + Cloudflare Worker (PR #533 از @dazzling-no-more): deploy Code.cfw.gs (variant جدید GAS در assets/apps_script/) + worker.js (Cloudflare Worker در assets/cloudflare/)، Apps Script یک layer thin auth+forward می‌شه که outbound fetch رو به CF edge می‌ده. mhrv-rs خود بدون تغییر — همان envelope JSON روی wire، همان mode: "apps_script"، script_id، auth_key. تنها تفاوت چیزی هست که Apps Script deployed بعد از authentication انجام می‌ده. این task audit در roadmap #380 / #393 رو close می‌کنه. چرا کاربران Persian گزارش دادن GAS+CFW combination از pure GAS برای browsing + chat-style سریع‌تر حس می‌شه. سختگیر شده over upstream denuitt1/mhr-cfw: per-request AUTH_KEY check (upstream omit می‌کرد → relay open اگر URL leak شد)، fail-closed اگر AUTH_KEY هنوز placeholder باشه، loop guard x-relay-hop + self-host fetch block، body drop on GET/HEAD برای match با Code.gs/UrlFetchApp permissiveness، SKIP_HEADERS parity، batch handler با Promise.all + soft cap MAX_BATCH_SIZE = 40. محدودیت‌های صادقانه (در docs explicit): با mode: "full" ناسازگار است (فقط HTTP-relay path port شده، نه raw-TCP/UDP tunnel ops). YouTube long-form بدتر می‌شه (30s CF Worker wall vs Apps Script ~6min — SABR cliff زودتر می‌رسه). Cloudflare anti-bot اثر معکوس داره (Worker IP اغلب stricter از Google IP). Day-one quota relief نیست (path batch ready ولی از client شیپ شده single-shape unreachable). docs کامل انگلیسی + فارسی در assets/cloudflare/README.md + README.fa.md شامل setup، model security سه AUTH_KEY match، trade-off table، Full mode incompatibility.

• Apps Script + Cloudflare Worker alternative backend (PR #533 by @dazzling-no-more): deploy Code.cfw.gs (new GAS variant in assets/apps_script/) plus worker.js (Cloudflare Worker in assets/cloudflare/), and Apps Script becomes a thin auth+forward layer that pushes the outbound fetch to CF's edge. mhrv-rs itself is unchanged — same JSON envelope on the wire, same mode: "apps_script", script_id, auth_key. The only difference is what the deployed Apps Script does after it authenticates. Closes the audit task on the v1.9.x roadmap (#380, #393). Why: recurring Persian-community feedback reports that GAS+CFW combination feels noticeably faster than plain GAS for browsing and chat-style workloads. Hardened over upstream denuitt1/mhr-cfw: per-request AUTH_KEY check (upstream omitted → open relay if URL leaks), fail-closed if AUTH_KEY still equals the placeholder, x-relay-hop loop guard + self-host fetch block, drops body on GET/HEAD to match Code.gs/UrlFetchApp permissiveness, SKIP_HEADERS parity, batch handler with Promise.all + soft cap MAX_BATCH_SIZE = 40. Honest limitations called out in docs: not compatible with mode: "full" (only HTTP-relay path ported; raw-TCP / UDP tunnel ops needed for messengers under Android full-mode aren't). YouTube long-form gets worse (30 s CF Worker wall vs Apps Script's ~6 min — SABR cliff arrives sooner). Cloudflare anti-bot is unaffected — exit IP becomes a Workers IP, which CF's anti-bot fingerprints as worker-internal (often stricter than a Google IP). No day-one UrlFetchApp daily-count relief; the batch-aware GAS+Worker path is wired and ready (ceil(N / 40) per N-URL batch) but unreachable from any shipping client today (mhrv-rs's HTTP-relay path is single-shape only). Full docs in English + Persian at assets/cloudflare/README.md + README.fa.md covering setup, the three-matching-AUTH_KEYs security model, trade-off table, full-mode incompatibility section. README updated with alternative-backend callout in both languages.

What's Changed

Full Changelog: v1.9.1...v1.9.2

v1.9.1

30 Apr 05:51

Choose a tag to compare

• tunable کردن آستانه auto-blacklist با ۳ field config جدید: auto_blacklist_strikes (default 3)، auto_blacklist_window_secs (default 30)، auto_blacklist_cooldown_secs (default 120) (#391، #444): تا قبل، threshold روی ۳ timeout در ۳۰ ثانیه = ۱۲۰ ثانیه cooldown hard-coded بود. کاربران single-deployment گزارش دادن این threshold روی شبکه‌های flaky too aggressive هست — یک cold-start stall + دو network blip → فقط deployment آن‌ها lockout می‌شه. حالا قابل تنظیم: single-deployment users می‌تونن auto_blacklist_strikes: 5 یا auto_blacklist_cooldown_secs: 30 بزارن. کاربران multi-deployment با ۱۰+ alternatives می‌تونن auto_blacklist_strikes: 2 بزارن برای fail-fast. defaults رفتار قدیمی رو حفظ می‌کنن — هیچ کاربری چیزی notice نمی‌کنه مگر در config صریح override کنه. کاربر در UI form expose نشده — power-user file edit در config.json. clamp [1, 86400] برای جلوگیری از مقادیر غیرمعقول.
request_timeout_secs config (default 30) برای تنظیم batch HTTP timeout (#430، masterking32 PR #25): تا قبل BATCH_TIMEOUT = 30s hard-coded. شبکه‌های Iran ISP slow ممکنه 45 یا 60 بخوان تا Apps Script پیغام ارسال کنه past throttle window. شبکه‌های با fail-fast preference ممکنه 15 بخوان برای retry سریع‌تر هنگام hang. clamp [5s, 300s]. برای کاربر در UI form expose نشده.
• warning روشن‌تر در tunnel-node startup برای recurring MHRV_AUTH_KEY typo (#391، #444): چندین قدیمی copy-paste guide از MHRV_AUTH_KEY به‌جای TUNNEL_AUTH_KEY در docker run استفاده می‌کرد. tunnel-node اون env var رو هرگز نمی‌خوند + silently default changeme رو fallback می‌کرد، که باعث AUTH_KEY-mismatch decoy می‌شد در client. حالا اگر MHRV_AUTH_KEY set باشه ولی TUNNEL_AUTH_KEY نباشه، tunnel-node پیغام specific می‌ده: "MHRV_AUTH_KEY is set but TUNNEL_AUTH_KEY is not — tunnel-node only reads TUNNEL_AUTH_KEY (uppercase, with underscores). Rename your env var: docker run ... -e TUNNEL_AUTH_KEY=...". این به کاربر مستقیم کمک می‌کنه به‌جای ساعت‌ها debug.
• run.bat fallback به CLI بعد از UI failure (#417، #426، #487): قبلاً وقتی هر دو UI renderer (glow + wgpu) fail می‌گرفتن (روی ماشین‌های قدیمی Windows / RDP / VM بدون GPU)، script پیغام "open issue" می‌داد + exit. حالا بعد از پیغام error، CLI mhrv-rs.exe رو خود اجرا می‌کنه + کاربر می‌تونه به استفاده از proxy ادامه دهد. CLI همان full functionality رو داره بدون UI shell — proxy روی 127.0.0.1:8085 (HTTP) و 127.0.0.1:8086 (SOCKS5).

• Tunable auto-blacklist threshold via three new config fields: auto_blacklist_strikes (default 3), auto_blacklist_window_secs (default 30), auto_blacklist_cooldown_secs (default 120) (#391, #444): previously hard-coded at "3 timeouts in 30s = 120s cooldown". Single-deployment users reported this threshold was too aggressive on flaky networks — one cold-start stall plus two transient network blips would lock them out of their only relay path. Now tunable: single-deployment users can set auto_blacklist_strikes: 5 or auto_blacklist_cooldown_secs: 30 to be more forgiving. Multi-deployment users with 10+ healthy alternatives can set auto_blacklist_strikes: 2 to fail-fast. Defaults preserve existing behavior — no user notices a change unless they explicitly tune in config.json. Not exposed in the UI form yet — power-user file edit. Clamped to [1, 86400] for the duration fields to prevent absurd values.
request_timeout_secs config field (default 30) to tune the batch HTTP timeout (#430, masterking32 PR #25): previously the hard-coded BATCH_TIMEOUT = 30s constant. Slow Iran ISP networks may want 45 or 60 to give Apps Script time to respond past throttle windows. Networks preferring fail-fast may want 15 to retry sooner when a deployment hangs. Clamped to [5s, 300s] (anything beyond exceeds Apps Script's 6-min hard cap with no benefit). Not in the UI form.
• Clearer tunnel-node startup warning for the recurring MHRV_AUTH_KEY typo (#391, #444): several older copy-paste guides used MHRV_AUTH_KEY instead of TUNNEL_AUTH_KEY in docker run. tunnel-node never read that env var and silently fell back to default changeme, producing baffling AUTH_KEY-mismatch decoys on the client. Now if MHRV_AUTH_KEY is set but TUNNEL_AUTH_KEY is not, tunnel-node emits a specific warning: "MHRV_AUTH_KEY is set but TUNNEL_AUTH_KEY is not — tunnel-node only reads TUNNEL_AUTH_KEY (uppercase, with underscores). Rename your env var: docker run ... -e TUNNEL_AUTH_KEY=<your-secret>". Saves users hours of debugging the wrong layer.
run.bat falls back to CLI after UI renderer failure (#417, #426, #487): when both UI renderers (glow + wgpu) fail on older Windows machines, RDP sessions, or VMs without GPU acceleration, the script previously printed an "open an issue on GitHub" message and exited. Now it prints the diagnostic info AND launches mhrv-rs.exe (CLI) so the user can keep using the proxy without the UI shell. CLI has the same proxy functionality on 127.0.0.1:8085 (HTTP) and 127.0.0.1:8086 (SOCKS5); only the visual UI is missing.

Full Changelog: v1.9.0...v1.9.1

v1.9.0

29 Apr 16:49

Choose a tag to compare

شکستگی سازگاری minor: نام‌گذاری mode = "google_only" به mode = "direct" تغییر کرد (PR #488 از @dazzling-no-more): نام قدیمی توصیف وضعیت رو بعد از اضافه شدن fronting_groups (که فراتر از Google می‌رسه) درست نمی‌داد. در Rust + Android + UI dropdown همه به direct تغییر کرده‌اند، ولی google_only به‌عنوان alias deprecated در parser قابل قبول مونده — config‌ها و saved settings قدیمی نمی‌شکنن. در Save بعدی، on-disk file خودکار به direct migrate می‌شه. در docs (README EN/FA, SF_README EN/FA, tunnel-node FA) note "تا قبل v1.9 نام google_only بود — هنوز کار می‌کنه" گذاشته شده برای کاربرانی که از راهنماهای قدیمی یا پست‌های Telegram قدیمی استفاده می‌کنن.
• fronting_groups: domain fronting چند-edge برای CDN غیر-Google (Vercel، Fastly، …) (PR #488 از @dazzling-no-more، با credit به @patterniha/MITM-DomainFronting برای technique اصلی): فیلد جدید config fronting_groups: [{name, ip, sni, domains}]. هر group شامل (edge IP، front SNI، domain‌های member). وقتی CONNECT به یکی از domain‌های member می‌رسه، proxy MITM می‌کنه + upstream با ip به‌عنوان TCP destination + sni به‌عنوان TLS SNI re-encrypt می‌کنه — همان trick که برای google_ip + front_domain می‌کنیم، حالا قابل تنظیم برای هر CDN multi-tenant. بر روی Google fronting (built-in) برتری داره؛ زیر passthrough_hosts و DoH bypass قرار داره. در mode = full غیر فعال (که end-to-end TLS رو حفظ می‌کنه + MITM نمی‌کنه). config مثال: config.fronting-groups.example.json. doc کامل: docs/fronting-groups.md شامل recipe انتخاب (ip, sni)، routing precedence، و warning صریح ⚠️ درباره cross-tenant Host-header leak failure mode (هرگز domain‌هایی که واقعاً روی edge نیستند رو list نکنید). reviews folded: SNI اعتبار رستورد روی config-load gate، Vec<Arc<>> به‌جای clone-on-match، byte-level dot-anchored matcher، startup warnings برای inert combos.
• edge-cache DNS در CodeFull.gs برای skip کردن round-trip tunnel-node (PR #494 از @dazzling-no-more): udp_open ops با port=53 در _doTunnelBatch intercept می‌شن + از CacheService (cache hit) یا DoH (cache miss) سرو می‌شن. cache hit‌ها latency typical first-hop DNS رو از ~۶۰۰-۱۲۰۰ms به ~۲۰۰-۴۰۰ms پایین می‌آرن. تغییر pure server-side در CodeFull.gs (فقط Full mode — apps_script mode UDP path نداره). بدون تغییر Rust/client. DoH fallback chain: Cloudflare → Google → Quad9 روی RFC 8484 GET. cache key per-qtype برای جلوگیری از A/AAAA collision. TTL clamping در [30s, 6h]. NXDOMAIN/SERVFAIL با ۴۵s negative cache. NODATA-with-SOA بر اساس RFC 2308 §5 SOA TTL رو honor می‌کنه. default-on، opt-out با ENABLE_EDGE_DNS_CACHE. هر failure mode به path forward موجود tunnel-node fallback می‌کنه (zero regression). انتخاب CacheService بر روی Sheets به دلیل سرعت (~۱۰ms) + privacy (volatile، روی Drive log persist نمی‌کنه — برای کاربران در صحنه‌های censorship مهمه). ۱۱ تست pure-JS pass.
• default tunnel_doh: true (flipped از false در v1.8.x) (#468): default قبلی (DoH bypass فعال) برای کاربران ایرانی بدون نشان دادن چیزی شکست می‌خورد چون Iran ISP direct connection به dns.google، chrome.cloudflare-dns.com و سایر pinned DoH hosts رو filter می‌کنن — همان hosts که bypass در حال route مستقیم می‌فرستاد. در نتیجه، DNS lookup‌ها fail می‌گرفتن + browsing شکست می‌خورد. حالا default safe است (DoH داخل tunnel نگه داشته می‌شه، در یک شبکه فیلتر شده کار می‌کنه). کاربری روی شبکه‌هایی که direct DoH کار می‌کنه (non-Iran)، می‌تونه tunnel_doh: false در config بگذاره برای latency win. تغییر backwards-compatible برای configs موجود — همه‌ی configs دارای فیلد explicit tunnel_doh رفتار حفظ می‌شن.
• اشتراک‌گذاری Hotspot iOS/laptop (PR #483 از @yyoyoian-pixel): default listen_host از 127.0.0.1 به 0.0.0.0 تغییر کرده. این workflow معمول رو enable می‌کنه — یک phone Android که tunnel run می‌کنه، iPhone یا laptop روی همان hotspot WiFi می‌تونه از proxy استفاده کنه. configs قدیمی با explicit listen_host: "127.0.0.1" honor می‌شن (بازنویسی نمی‌شن).

Minor breaking: mode = "google_only" renamed to mode = "direct" (PR #488 by @dazzling-no-more): the old name no longer described the mode now that fronting_groups reaches more than Google. Rust + Android + UI dropdown all updated, but google_only is preserved as a deprecated alias on parse — existing configs and saved settings don't break. On the next Save, the on-disk file migrates automatically to direct. Docs (README EN+FA, SF_README EN+FA, tunnel-node FA) carry a "was named google_only before v1.9 — old name still works" note so users following older guides / Telegram posts find their way.
fronting_groups: multi-edge domain fronting for non-Google CDNs (PR #488 by @dazzling-no-more, credit to @patterniha/MITM-DomainFronting for the original technique): new config field fronting_groups: [{name, ip, sni, domains}]. Each group is (edge IP, front SNI, member domains): when a CONNECT to one of the member domains arrives, the proxy MITMs at the local CA, then re-encrypts upstream against ip with sni as the TLS SNI — same trick we already do for google_ip + front_domain, now configurable for any multi-tenant CDN edge (Vercel, Fastly, etc.). Wins over the built-in Google SNI-rewrite suffix list; loses to passthrough_hosts and DoH bypass. Skipped in mode = full (which preserves end-to-end TLS and can't MITM). Working example at config.fronting-groups.example.json. Full doc at docs/fronting-groups.md including the recipe for picking (ip, sni), routing precedence, and an explicit ⚠️ warning about the cross-tenant Host-header leak failure mode (don't list domains that aren't actually on the edge). Review fixes folded: SNI validated via rustls at config-load gate; Vec<Arc<>> refcount on per-CONNECT match; byte-level dot-anchored matcher (no per-match format!()); startup warnings for inert combos.
• Edge-cache DNS in CodeFull.gs to skip the tunnel-node round-trip (PR #494 by @dazzling-no-more): intercepts udp_open / port=53 ops in _doTunnelBatch and serves them from CacheService (cache hit) or DoH (cache miss). Cache hits drop typical first-hop DNS latency from ~600-1200ms to ~200-400ms. Pure server-side change in CodeFull.gs (Full mode only — apps_script mode has no UDP path); zero Rust/client changes. DoH fallback chain: Cloudflare → Google → Quad9 over RFC 8484 GET. Per-qtype cache key keeps A and AAAA from colliding. Min RR TTL clamped to [30s, 6h]; NXDOMAIN/SERVFAIL get a 45s negative cache; NODATA-with-SOA honors the SOA TTL per RFC 2308 §5. Default-on, opt-out via ENABLE_EDGE_DNS_CACHE. Every failure mode (parse error, resolver outage, key-too-long, cache.put rejection) falls through to the existing tunnel-node forward path — zero regression on any failure. CacheService chosen over Sheets (#443's pattern) because Sheets reads/writes are 100-500ms per op (often slower than the DoH lookup we'd be caching), have a daily-quota hazard, and persist a Drive-listed log of every domain users resolve — a real privacy regression for users in censorship contexts. CacheService is ~10ms, volatile, free, no on-disk artifact. 11 pure-JS tests covering parsers, txid non-mutation, TTL clamp, NXDOMAIN-with-SOA TTL extraction, malformed/truncated input rejection, splice correctness for mixed batches.
• Default tunnel_doh: true (flipped from false in v1.8.x) (#468): the previous default (DoH bypass active) silently broke for Iranian users because Iran ISPs filter direct connections to dns.google, chrome.cloudflare-dns.com, and other pinned DoH hosts — exactly the hosts the bypass was routing direct. DNS resolution failed and browsing broke. The safer default keeps DoH inside the tunnel; users on networks where direct DoH works can opt back into the bypass with tunnel_doh: false. Backwards-compatible for existing configs — anyone who explicitly set tunnel_doh keeps their behavior. Iranian users on pre-v1.8.6 versions hitting this regression should upgrade.
• Hotspot sharing for iOS / laptop (PR #483 by @yyoyoian-pixel): default listen_host changed from 127.0.0.1 to 0.0.0.0. Enables the common workflow where an Android phone runs the tunnel and an iPhone/iPad/laptop on the same hotspot uses it as a proxy (HTTP 192.168.43.1:8080 or SOCKS5 :1081). For full device-wide coverage on iOS, Shadowrocket or Potatso create a local VPN that routes all traffic through the SOCKS5 on the Android phone. Old configs with explicit "listen_host": "127.0.0.1" are honored (not overwritten).

What's Changed

Full Changelog: v1.8.5...v1.9.0

v1.8.5

29 Apr 03:44

Choose a tag to compare

• fix tunnel-node: cap هر TCP drain روی ۱۶ MiB تا batch response از سقف Apps Script (~۵۰ MiB) عبور نکنه (#460 از @bankbunk): روی VPS های پر-bandwidth (۱ Gbps) reader task می‌تونه هزاران مگابایت رو در buffer per-session جمع کنه قبل از اینکه poll بعدی بیاد. قبلاً drain_now همه‌ی buffer رو در یک batch response می‌گرفت، base64 encoding (~۱.۳۳×) + JSON envelope اضافه می‌کرد، نتیجه از سقف ۵۰ MiB Apps Script رد می‌شد. Apps Script body رو wrap-around mid-base64 کوتاه می‌کرد + client side serde_json parse error با EOF while parsing a string at line 1 column 52428685 می‌گرفت. برای استریم MP4 یا هر بایت‌سنگین upstream این bug stream رو مرتب کرش می‌داد. حالا drain_now حداکثر ۱۶ MiB در هر poll برمی‌گردونه + tail رو در buffer برای poll بعدی نگه می‌داره. eof تا finalize شدن buffer reported نمی‌شه که session بی‌موقع tear نشه. workaround قبلی @bankbunk (محدودکردن interface VPS با wondershaper به ۴۰ Mbps) دیگر لازم نیست — fix server-side پیاده شد و کاربران throughput عادی VPS رو خواهند داشت

• Fix tunnel-node: cap each TCP drain at 16 MiB so batch responses stay under Apps Script's ~50 MiB body ceiling (#460 by @bankbunk): on high-bandwidth VPS (1 Gbps+), the reader task can stuff the per-session read buffer with tens of MiB between client polls. The old drain_now took the entire buffer in one shot, base64-encoded it (1.33× overhead), wrapped it in JSON, and the resulting body exceeded Apps Script's hard ~50 MiB Web App response limit. Apps Script truncated the body mid-base64; the client failed serde_json parse with EOF while parsing a string at line 1 column 52428685 (= 50 MiB) and the stream tore. Most visibly, raw MP4 streams crashed minutes into playback. The fix splits oversized buffers: at most TCP_DRAIN_MAX_BYTES (16 MiB) is returned per drain, and the remainder stays in the buffer for the next poll. EOF is held back until the buffer is fully drained so partial drains don't prematurely close the session. Three regression tests cover the cap, the under-cap pass-through, and the EOF-holdback case (33 tunnel-node tests passing). @bankbunk's wondershaper workaround (rate-limiting the VPS interface to 40 Mbps) is no longer necessary — high-bandwidth VPS users can let throughput run at line rate again.

Full Changelog: v1.8.4...v1.8.5