Releases: therealaleph/MasterHttpRelayVPN-RUST
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
v1.9.5
• 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
• 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
• 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
- feat(android): add youtube_via_relay toggle to Advanced settings by @yyoyoian-pixel in #535
Full Changelog: v1.9.2...v1.9.3
v1.9.2
• 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
- feat(cfw): add Apps Script + Cloudflare Worker alternative backend by @dazzling-no-more in #533
Full Changelog: v1.9.1...v1.9.2