Releases: therealaleph/MasterHttpRelayVPN-RUST
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
v1.9.1
• 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
• شکستگی سازگاری 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 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
- feat: listen on all interfaces, hotspot sharing for iOS/laptop by @yyoyoian-pixel in #483
- feat(codefull.gs): edge-cache DNS to skip tunnel-node round-trip by @dazzling-no-more in #494
- feat: multi-edge fronting_groups + rename google_only to direct by @dazzling-no-more in #488
Full Changelog: v1.8.5...v1.9.0
v1.8.5
• 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
v1.8.4
• adaptive batch coalescing برای کاهش تعداد Apps Script roundtripها (PR #448 از @yyoyoian-pixel): قبلاً هر op فقط ۸ms برای opهای همزمان دیگر صبر میکرد + اکثراً batchها فقط ۱ op داشتن. حالا scheme adaptive: اولین op یک timer ۴۰ms استارت میزنه، opهای جدید reset میکنن، اگر ۴۰ms idle شد batch fire میشه. cap نهایی ۱۰۰۰ms. نتیجه field test روی شبکه ایران: ops/batch از ۱.۰ به ۲-۳، P75 RTT از ۶.۲s به ۳.۰s، کاهش ~۵۰٪ Apps Script call. configurable از طریق coalesce_step_ms / coalesce_max_ms در config + در Android UI Advanced section با slider
• tunnel-node long-poll از ۵s به ۱۵s برای پایداری Telegram + Google Push (PR #446 از @yyoyoian-pixel): قبلاً connectionهای persistent (Telegram XMPP پورت ۵۲۲۲، Google Push پورت ۵۲۲۸) هر ۵ ثانیه empty-response برمیگرفتن + apps این رو instability تفسیر میکردن + session reconnect میزدن. هر reconnect معادل یک TLS handshake کامل (~۴s از طریق Apps Script) یعنی buffering قابل مشاهده در تماس Telegram یا playback. حالا long-poll تا ۱۵ ثانیه باز میمونه + persistent connection پایدار میمونه
• adaptive straggler settle در tunnel-node (PR #446): قبلاً fixed ۳۰ms wait بعد از اولین session که data داشت. حالا adaptive ۴۰ms-step تا ۵۰۰ms-max که زود break میشه اگر همه sessionها ready باشن. در شبکههای latency بالا (~۱.۵s Apps Script overhead) packing بیشتر session response در یک batch، quota مصرف کمتری ایجاد میکنه
• Adaptive batch coalescing to reduce Apps Script round-trip count (PR #448 by @yyoyoian-pixel): the previous fixed 8ms coalesce window barely caught concurrent ops — most batches were a single op. The new adaptive scheme starts a 40ms timer on first arrival, resets on each new op, fires the batch when the window stays empty, with a hard 1000ms cap. Field testing on a network in Iran showed ops/batch went from ~1.0 to 2-3, P75 RTT 6.2s → 3.0s, fast (<3s) batches 61% → 74-85%, total Apps Script calls roughly halved. Both values configurable via coalesce_step_ms / coalesce_max_ms in config.json and via sliders in the Android UI's Advanced section. Desktop UI sliders are queued for the v1.8.x desktop UI batch alongside the system-proxy toggle (#432).
• tunnel-node long-poll raised from 5s to 15s for persistent-connection stability (PR #446 by @yyoyoian-pixel): at 5s long-poll, persistent connections like Telegram XMPP (:5222) and Google Push (mtalk.google.com:5228) interpreted the frequent empty-poll returns as connection instability and rotated sessions. Each reconnect cost a full TLS handshake (~4s through Apps Script), causing visible interruptions during Telegram video/voice calls and media playback. The 15s long-poll holds the response open until server data actually arrives, keeping persistent sessions alive without unnecessary re-handshakes.
• Adaptive straggler settle in tunnel-node (PR #446): the previous fixed 30ms straggler settle was too short to catch neighboring sessions with data. Replaced with adaptive 40ms-step / 500ms-max settle that breaks early when all sessions in a batch have data. On high-latency relays (~1.5s Apps Script overhead per call), packing more session responses into one batch saves quota; the early-break prevents wasted time when all data is already ready.
What's Changed
- fix(tunnel-node): raise long-poll to 15s, adaptive straggler settle by @yyoyoian-pixel in #446
- feat(client): adaptive batch coalescing with configurable UI by @yyoyoian-pixel in #448
Full Changelog: v1.8.3...v1.8.4
v1.8.3
• cache spreadsheet اختیاری در Code.gs برای کاهش مصرف quota (#400، PR #443 از @euvel): GET requests عمومی که Cache-Control header دارن میتونن از Google Sheet بهجای Apps Script's UrlFetchApp serve بشن. هزینهی هر cache hit ~۵-۲۰ms (Sheet read) بهجای ۲۵۰-۵۰۰ms (UrlFetchApp roundtrip). features کامل: TTL-aware (max-age, no-cache, no-store, private respect)، header rewriting (Date/Age/X-Cache)، circular buffer برای O(1) writes، Vary-aware با Accept-Encoding/Accept-Language. opt-in via یک constant CACHE_SPREADSHEET_ID در Code.gs — default غیرفعال، بدون overhead برای کاربران که نمیخوان. setup: ساخت یک Google Sheet جدید + قرار دادن ID آن در CACHE_SPREADSHEET_ID + redeploy as new version
• bypass DoH endpoints from Apps Script tunnel (#377، PR #439 از @dazzling-no-more): قبلاً در Full mode هر DNS-over-HTTPS lookup browser از طریق Apps Script tunnel میرفت — chrome.cloudflare-dns.com:443، dns.google:443 و سایر هزینه ~۲ ثانیه UrlFetchApp roundtrip به ازای هر name داشتن. ولی DoH از قبل encrypted هست + tunnel privacy اضافهای نمیده — فقط fact-of-DoH رو از local network مخفی میکنه که ناچیزه. حالا bypass_doh_hosts config (default true) routes DoH lookups مستقیم via TCP/443. لیست کامل bypass شامل: Cloudflare (incl. chrome./mozilla./1dot1dot1dot1.)، Google، Quad9، AdGuard، NextDNS، OpenDNS، CleanBrowsing، dns.sb، dns0.eu، AliDNS، doh.pub، Mullvad. کاربران میتوانند با tunnel_doh: true در config opt-out کنن یا با bypass_doh_hosts: ["custom1.com", "custom2.com"] لیست رو extend کنن
• H1 container keepalive (~۲۴۰s) برای جلوگیری از Apps Script V8 cold-start stalls (PR #438 از @dazzling-no-more): Apps Script containerها بعد از ~۵ دقیقه idle cold میشن + ۱-۳ ثانیه به wake-up زمان میبرن. این بهخصوص در YouTube playback بعد از pause طولانی stall بهوضوح دیده میشد. با ping HEAD به example.com هر ۲۴۰ ثانیه از طریق relay، container warm نگه داشته میشه. cache + inflight coalescer bypass شده تا ping واقعاً به Apps Script برسه. در google_only mode غیرفعال
• 431 Request Header Fields Too Large بهجای drop سکوتی (PR #438 از @dazzling-no-more): قبلاً اگر header block > ۱ MB میشد، socket drop میشد + browser silently retry میکرد + loop ابدی. حالا cap به ۶۴ KB کاهش یافته (match upstream Python) + explicit HTTP/1.1 431 Request Header Fields Too Large response برمیگرده + close میشه. browser ارور رو ببینه + loop رو نمیسازه
• پیام error config port-collision واضحتر شد (PR #438): قبلاً پیام cryptic بود. حالا: "both set to 8080 on 127.0.0.1. Change one of them in config.json." — کاربر مستقیم میفهمه چی fix بکنه
• Optional spreadsheet-backed response cache in Code.gs to reduce UrlFetchApp quota consumption (#400, PR #443 by @euvel): public GET requests with Cache-Control headers can now be served from a Google Sheet instead of round-tripping through UrlFetchApp. Cache hit costs ~5-20ms (Sheet read) vs ~250-500ms (UrlFetchApp). Features: TTL-aware caching (respects max-age, no-cache, no-store, private), 35 KB body-size gate (under the Sheets cell limit), header rewriting (Date/Age/Cache-Control/X-Cache/X-Cached-At), circular buffer for O(1) writes, Vary-aware compound keys (Accept-Encoding + Accept-Language). Opt-in via a single CACHE_SPREADSHEET_ID constant — default off, zero overhead for users who don't want it. Setup: create a new Google Sheet, paste its ID into CACHE_SPREADSHEET_ID, redeploy as new version. Run getCacheStats() from the Apps Script editor to see hit/miss/eviction counts.
• Bypass Apps Script tunnel for DoH endpoints on TCP/443 (#377, PR #439 by @dazzling-no-more): previously every browser DNS-over-HTTPS lookup in Full mode rode through the Apps Script tunnel — chrome.cloudflare-dns.com:443, dns.google:443, etc. each paid the ~2-second UrlFetchApp round-trip per name. But DoH is already encrypted at the transport layer; tunneling it adds no real privacy (only hiding fact-of-DoH from the local network, which is marginal). Now bypass_doh_hosts config (default true) routes known DoH hosts around the tunnel via plain TCP. Built-in list: Cloudflare (incl. chrome./mozilla./1dot1dot1dot1. browser-pinned variants), Google, Quad9, AdGuard, NextDNS, OpenDNS, CleanBrowsing, dns.sb, dns0.eu, AliDNS, doh.pub, Mullvad. Users can opt out with tunnel_doh: true or extend the list with bypass_doh_hosts: ["custom1.com", "custom2.com"]. Gated to TCP/443 only — private DoH endpoints on :8443 should use passthrough_hosts instead. ProxyServer warns at startup if tunnel_doh: true is paired with non-empty bypass_doh_hosts (the otherwise-silent inert combo). 6 unit tests for matches_doh_host covering exact match, case insensitivity, trailing dots, suffix tenant subdomains, user extras extending the default list, and the asymmetric-matching footgun guard.
• H1 container keepalive (~240s) to prevent Apps Script V8 cold-start stalls (PR #438 by @dazzling-no-more): Apps Script V8 containers go cold after ~5 minutes idle and cost 1-3s to wake. Most visible as YouTube player stalls after a quiet pause. Now sends a HEAD http://example.com/ ping every 240s through the relay to keep the container warm. Bypasses the response cache and inflight coalescer (otherwise the second iteration would just hit the cache and never reach Apps Script). Skipped in google_only mode. The JoinHandle is captured so shutdown's select! arm can abort it cleanly — without that, hitting Stop in the UI would leave the keepalive holding an Arc<DomainFronter> on stale config (same class of bug as #99 hit for accept loops).
• 431 Request Header Fields Too Large instead of silent drop (PR #438 by @dazzling-no-more): previously header blocks larger than 1 MB were silently dropped at the socket level, causing browsers to retry on connection-reset and loop indefinitely on the same oversized request. Now the cap is tightened to 64 KB (matching upstream Python's MAX_HEADER_BYTES) and oversized requests get an explicit HTTP/1.1 431 Request Header Fields Too Large reply followed by close. Both the plaintext HTTP frontend and the MITM HTTPS relay path now do this. Browsers see the error and don't loop.
• Clearer port-collision error message (PR #438): the same-port validation already existed; only the message was vague. Now reads: "both set to 8080 on 127.0.0.1. Change one of them in config.json." matching upstream Python's clarity.
What's Changed
- feat: H1 container keepalive + 431 oversized-headers by @dazzling-no-more in #438
- feat: bypass Apps Script tunnel for DoH endpoints on TCP/443 by @dazzling-no-more in #439
- feat(code.gs): optional spreadsheet-backed response cache with TTL by @euvel in #443
New Contributors
Full Changelog: v1.8.2...v1.8.3