diff --git a/Cargo.lock b/Cargo.lock index 1ec515c..1ea207b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2222,7 +2222,7 @@ dependencies = [ [[package]] name = "mhrv-rs" -version = "1.9.9" +version = "1.9.10" dependencies = [ "base64 0.22.1", "bytes", diff --git a/Cargo.toml b/Cargo.toml index c6bcea5..aed0616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mhrv-rs" -version = "1.9.9" +version = "1.9.10" edition = "2021" description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting" license = "MIT" diff --git a/README.md b/README.md index f28661d..73154f8 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ If something doesn't work: **YouTube videos don't play.** YouTube's video chunks come from `googlevideo.com`, which Apps Script can't reach (Google blocks Apps Script from accessing Google's own video CDN). The page itself loads fine; only video playback is affected. Fix: Full Tunnel + VPS, or add `.googlevideo.com` to `passthrough_hosts` in your config (browser hits it directly, but on Iran ISPs it's still throttled). -**ChatGPT / Claude / Grok shows a Cloudflare CAPTCHA.** Cloudflare flags Google datacenter IPs as bots. Fix: set up an **exit node** (a free 5-minute thing on val.town that bridges Apps Script → val.town → claude.ai). See [`assets/exit_node/README.md`](assets/exit_node/README.md). +**ChatGPT / Claude / Grok shows a Cloudflare CAPTCHA.** Cloudflare flags Google datacenter IPs as bots. Fix: set up an **exit node** — a small TypeScript handler you deploy on a serverless host (Deno Deploy, fly.io, your own VPS) that bridges Apps Script → your exit node → claude.ai. See [`assets/exit_node/README.md`](assets/exit_node/README.md). **Telegram is unstable.** Telegram uses MTProto, which Apps Script doesn't speak. Pair with [xray](https://github.com/XTLS/Xray-core) on your machine — see [Telegram via xray in the full guide](docs/guide.md#telegram-via-xray). @@ -324,7 +324,7 @@ System Settings → Network → Wi-Fi → Details → **Proxies** → هر دو **ویدیوی یوتیوب پخش نمی‌شود.** chunkهای ویدیوی یوتیوب از `googlevideo.com` می‌آیند و Apps Script نمی‌تواند به آن برسد (گوگل اجازهٔ دسترسی Apps Script به CDN ویدیوی خودش را نمی‌دهد). صفحهٔ خود یوتیوب لود می‌شود، فقط پخش ویدیو تحت تأثیر است. راه‌حل: Full Tunnel + VPS، یا `.googlevideo.com` را به `passthrough_hosts` در کانفیگت اضافه کن (مرورگر مستقیم می‌رود اما روی ISP ایران throttle می‌خورد). -**ChatGPT / Claude / Grok کپچای Cloudflare نشان می‌دهد.** Cloudflare آی‌پی‌های دیتاسنتر گوگل را به‌عنوان bot شناسایی می‌کند. راه‌حل: یک **exit node** راه‌اندازی کن (پنج دقیقه روی val.town رایگان — پل بین Apps Script و سایت Cloudflare). [`assets/exit_node/README.fa.md`](assets/exit_node/README.fa.md). +**ChatGPT / Claude / Grok کپچای Cloudflare نشان می‌دهد.** Cloudflare آی‌پی‌های دیتاسنتر گوگل را به‌عنوان bot شناسایی می‌کند. راه‌حل: یک **exit node** راه‌اندازی کن — یک handler کوچک TypeScript که روی یک host serverless (Deno Deploy، fly.io، VPS شخصی) deploy می‌کنی و پل می‌سازه از Apps Script به سایت Cloudflare. [`assets/exit_node/README.fa.md`](assets/exit_node/README.fa.md). **تلگرام پایدار نیست.** تلگرام از MTProto استفاده می‌کند که Apps Script نمی‌فهمد. روی کامپیوترت با [xray](https://github.com/XTLS/Xray-core) جفتش کن — [بخش تلگرام در راهنمای کامل](docs/guide.fa.md#تلگرام-با-xray). diff --git a/assets/exit_node/README.fa.md b/assets/exit_node/README.fa.md index ff31f2d..e04d477 100644 --- a/assets/exit_node/README.fa.md +++ b/assets/exit_node/README.fa.md @@ -1,3 +1,5 @@ +
+ # Exit node — دور زدن CF anti-bot برای ChatGPT / Claude / Grok / X بسیاری از سرویس‌های پشت Cloudflare، traffic از رنج IP datacenter @@ -14,9 +16,9 @@ Script از همان رنج IP datacenter Google خروج می‌کنه، پس `502 Relay error` می‌ده چون Code.gs در حال wrap کردن صفحه‌ی HTML challenge CF است که کلاینت نمی‌تونه parse کنه. -**Exit node** یک endpoint کوچک TypeScript HTTP است که روی یک پلتفرم -serverless (val.town، Deno Deploy، fly.io، …) deploy می‌شه + بین Apps -Script و destination قرار می‌گیره. مسیر traffic این می‌شه: +**Exit node** یک handler کوچک HTTP به زبان TypeScript است که روی یک +پلتفرم serverless TypeScript که خودت تأییدش می‌کنی deploy می‌شه و بین +Apps Script و destination قرار می‌گیره. مسیر traffic این می‌شه: ``` Browser ─┐ ┌─→ Destination @@ -30,14 +32,14 @@ Browser ─┐ ┌─→ Destinat │ │ │ UrlFetchApp.fetch(EXIT_NODE_URL) │ ▼ │ - val.town (non-Google IP) │ + exit node خودت (IP غیر گوگل) │ │ │ │ fetch(real_url) │ └──────────────────────────────────────────────────┘ ``` -Destination IP val.town رو می‌بینه، نه Google datacenter. heuristic -anti-bot CF نمی‌سوزه + صفحه واقعی برمی‌گرده. +Destination IP خروجی exit node رو می‌بینه، نه IP datacenter گوگل. +Heuristic anti-bot CF نمی‌سوزه + صفحه واقعی برمی‌گرده. **نکته مهم:** leg user-side (Iran ISP → Apps Script) **بدون تغییر** است. ISP فقط TLS به Google IP می‌بینه — second hop کاملاً درون @@ -46,46 +48,65 @@ evasion property که mhrv-rs براش ساخته شده، دست نمی‌خو ## راه‌اندازی -1. **در [val.town](https://val.town) ثبت‌نام کنید** (free tier کافی - است — bandwidth outbound free tier برای personal use کافی). -2. **یک HTTP val جدید بسازید** (TypeScript). در val.town: New → HTTP. -3. **محتوای `valtown.ts`** از این directory رو paste کنید. -4. **PSK رو در بالای فایل تنظیم کنید**: +handler در [`exit_node.ts`](exit_node.ts) plain TypeScript است که از +APIهای web-standard (`Request`، `Response`، `fetch`) استفاده می‌کنه و +روی هر پلتفرمی که serverless-fetch runtime داره اجرا می‌شه. + +### مراحل عمومی (روی هر host) + +۱. فایل [`exit_node.ts`](exit_node.ts) رو باز کنید و PSK پیش‌فرض رو در +ابتدا عوض کنید: ```ts const PSK = ""; ``` Strong secret تولید کنید با `openssl rand -hex 32` از terminal. - **placeholder رو در production نگذارید** — کد val.town عمداً - fail-closed است (در هر request 503 برمی‌گردونه) تا placeholder - replace نشده، تا جلوی serve شدن به‌عنوان open relay accidentally - گرفته بشه. -5. **Save** کنید val رو. URL public val رو copy کنید — به این شکل: - `https://your-handle-mhrv.web.val.run`. -6. **در `config.json` mhrv-rs**، block `exit_node` اضافه کنید: + **placeholder رو در production نگذارید** — کد عمداً fail-closed است + (در هر request 503 برمی‌گردونه) تا placeholder replace نشده، تا + جلوی serve شدن به‌عنوان open relay accidentally گرفته بشه. +۲. فایل رو روی host انتخابی **deploy** کنید (گزینه‌ها در ادامه). +۳. URL public deployment رو **copy** کنید. +۴. در `config.json` mhrv-rs، block `exit_node` اضافه کنید: ```json "exit_node": { "enabled": true, - "relay_url": "https://your-handle-mhrv.web.val.run", - "psk": "<همان PSK که در گام 4 گذاشتید>", + "relay_url": "https://your-deployed-exit-node.example.com", + "psk": "<همان PSK که در گام ۱ گذاشتید>", "mode": "selective", "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"] } ``` -7. **mhrv-rs رو restart کنید** (Disconnect + Connect، یا `kill` + +۵. mhrv-rs رو **restart** کنید (Disconnect + Connect، یا `kill` + restart binary). -8. **تست کنید** — `chatgpt.com` یا `grok.com` رو از browser pointed به - mhrv-rs proxy باز کنید. صفحه login واقعی رو می‌بینید، نه CF challenge. +۶. **تست** کنید — `chatgpt.com` یا `grok.com` رو از browser pointed به + mhrv-rs proxy باز کنید. صفحه login واقعی رو می‌بینید، نه CF + challenge. config مثال کامل در [`config.exit-node.example.json`](../../config.exit-node.example.json) در root repo. +### گزینه‌های hosting + +اسکریپت یک فایل self-contained است. هر host که می‌توانید signup کنید + +به‌اش اعتماد دارید رو انتخاب کنید: + +| Host | توضیحات | +|---|---| +| **Deno Deploy** ([deno.com/deploy](https://deno.com/deploy)) | free tier برای personal use کافی است. با `deployctl deploy --prod exit_node.ts` یا GitHub Actions deploy کنید. همان web-standard API. | +| **fly.io** | free tier با محدودیت. handler رو در یک server thin بسته‌بندی کنید (`Deno.serve(handler)` برای Deno یا یک Express wrapper برای Node) + Dockerfile اضافه کنید. IP دائم، region جغرافیایی قابل انتخاب. | +| **VPS شخصی خودت** | `deno run --allow-net wrapper.ts` که `wrapper.ts` کارش `Deno.serve({ port: 8443 }, handler)` است. حداکثر کنترل، ~۳-۵ دلار در ماه. | +| **Cloudflare Workers** | **کمک نمی‌کنه.** CF Workers از IP space خود CF خروج می‌کنن، که CF anti-bot هنوز به‌عنوان worker-internal flag می‌کنه. | + +برای اکثر کاربرانی که مسیر local رو اجرا می‌کنن، Deno Deploy +سریع‌ترین setup است. برای deployment طولانی‌مدت تحت کنترل کامل +خودت، VPS کوچک شخصی ایده‌آل است. + ## انتخاب `selective` vs `full` | Mode | چی می‌کنه | کی استفاده کنید | |---|---|---| | `selective` (default) | فقط hosts در `hosts` از طریق exit node می‌رن؛ بقیه از مسیر Apps Script عادی | توصیه می‌شه. exit-node hop ~۲۰۰-۵۰۰ms به هر request اضافه می‌کنه — برای سایت‌هایی reserve کنید که نیاز به non-Google IP دارن. | -| `full` | همه‌ی request‌ها از طریق exit node می‌رن | فقط زمانی که کل workload شما CF-anti-bot affected است، یا exit node خود از Apps Script سریع‌تر روی مسیر شبکه شما (rare). budget runtime val.town رو برای سایت‌هایی که نیاز ندارن می‌سوزونه. | +| `full` | همه‌ی request‌ها از طریق exit node می‌رن | فقط زمانی که کل workload شما CF-anti-bot affected است، یا exit node خود سریع‌تر روی مسیر شبکه شما (rare). budget runtime host رو برای سایت‌هایی که نیاز ندارن می‌سوزونه. | ## رفتار در صورت failure @@ -98,39 +119,25 @@ exit node down شما رو fully offline نمی‌کنه. ## Security model -PSK تنها چیز است که مانع می‌شه val.town endpoint یک public open proxy +PSK تنها چیز است که مانع می‌شه endpoint deployed یک public open proxy بشه. مثل password برخورد کنید: -- **commit نکنید** PSK رو به source control. منبع val.town به‌طور - default برای account شما private است؛ همان‌طور نگه دارید. +- **commit نکنید** PSK رو به source control. اکثر hostها به‌طور default + کد deployed رو private نگه می‌دارن؛ همان‌طور نگه دارید. - **publicly share نکنید** PSK رو. هر کسی که هم URL هم PSK رو داره - می‌تونه quota val.town شما رو به‌عنوان proxy خود استفاده کنه. -- **rotate** اگر leak مشکوک هست. PSK رو در val.town source تغییر بدید، - save کنید، سپس `psk` در `config.json` mhrv-rs رو update + restart. + می‌تونه quota host شما رو به‌عنوان proxy خود استفاده کنه. +- **rotate** اگر leak مشکوک هست. PSK رو در source deployed تغییر بدید، + redeploy کنید، سپس `psk` در `config.json` mhrv-rs رو update + restart. -اسکریپت val.town شامل **loop guard** هم هست (refuse می‌کنه fetch host -خود) + **placeholder check** (در صورت `PSK === "CHANGE_ME_TO_A_STRONG_SECRET"` +اسکریپت همچنین شامل **loop guard** هست (refuse می‌کنه fetch host خود) ++ **placeholder check** (در صورت `PSK === "CHANGE_ME_TO_A_STRONG_SECRET"` return 503 می‌کنه) تا یک fresh deploy بدون setup نتونه به‌طور accidentally به‌عنوان open relay سرو بشه. -## پلتفرم‌های جایگزین - -اسکریپت `valtown.ts` plain TypeScript است که از web-standard APIs -(`Request`، `Response`، `fetch`) استفاده می‌کنه. اجرا می‌شه روی: - -- **val.town** — ساده‌ترین، free tier کافی برای personal use -- **Deno Deploy** — API مشابه؛ deploy با `deployctl` -- **fly.io** — نیاز به `Dockerfile` wrapper؛ region geographic ثابت -- **Cloudflare Workers** — کمک نمی‌کنه (CF Workers از IP space خود CF - خروج می‌کنن، که CF anti-bot هنوز به‌عنوان worker-internal flag می‌کنه) - -برای اکثر کاربران، val.town انتخاب درست است. Deno Deploy اگر option -non-val.town برای redundancy می‌خواید. - ## چرا default-on نیست - ۲۰۰-۵۰۰ms به هر request اضافه می‌کنه (hop اضافی) -- budget bandwidth free-tier val.town رو می‌سوزونه +- budget bandwidth free-tier host رو می‌سوزونه - برای سایت‌هایی که CF anti-bot ندارن benefit نداره - Setup یک account جداگانه روی پلتفرم third-party می‌خواد @@ -140,35 +147,38 @@ Grok اهمیت می‌دن opt in؛ همه‌ی دیگران lighter اجرا ## Troubleshooting **`exit node refused or errored: unauthorized`** — PSK mismatch. -بررسی کنید `psk` در `config.json` دقیقاً با `PSK` constant در val.town -match هست. whitespace + quoting مهم است. +بررسی کنید `psk` در `config.json` دقیقاً با `PSK` constant در source +deployed match هست. whitespace + quoting مهم است. **`exit node refused or errored: exit_node misconfigured: PSK is still the placeholder`** — فراموش کردید `CHANGE_ME_TO_A_STRONG_SECRET` رو -در val.town جایگزین کنید. val رو edit + save کنید. +در source جایگزین کنید. فایل deployed رو edit + save + redeploy کنید. -**`exit node failed for ...: connection refused`** — URL val.town -اشتباه است یا val deploy نشده. با hit کردن URL مستقیم از browser -verify کنید — باید `{"e":"method_not_allowed"}` برگردونه (val expects +**`exit node failed for ...: connection refused`** — URL اشتباه است +یا deployment live نیست. با hit کردن URL مستقیم از browser verify +کنید — باید `{"e":"method_not_allowed"}` برگردونه (handler expects POST). -**`exit node failed for ...: timeout`** — outbound val.town slow است -یا destination slow. region val.town متفاوت رو امتحان کنید، یا latency +**`exit node failed for ...: timeout`** — outbound host slow است +یا destination slow. region متفاوت رو امتحان کنید، یا latency trade-off رو accept کنید. **سایت همچنان CF challenge نشون می‌ده بعد از enable exit node** — CF -IP val.town رو هم flag می‌کنه. برخی customers CF صراحتاً val.town رو -blocklist کردن. workarounds: Deno Deploy رو امتحان کنید، یا سایت رو -به `passthrough_hosts` اضافه کنید (MITM رو bypass می‌کنه؛ از real -IP ISP شما استفاده می‌کنه). +IP host شما رو هم flag کرده. بعضی hosting provider‌ها outbound IP +space‌شون روی CF bot blocklist است. workarounds: host دیگه امتحان +کنید (VPS شخصی شما clean IP می‌ده)، یا سایت رو به `passthrough_hosts` +اضافه کنید (MITM رو bypass می‌کنه؛ از real IP ISP شما استفاده +می‌کنه). ## همچنین ببینید - [English version](README.md) of this doc -- [`valtown.ts`](valtown.ts) — منبع val.town (با hardening) +- [`exit_node.ts`](exit_node.ts) — منبع handler (با hardening) - [`config.exit-node.example.json`](../../config.exit-node.example.json) — config مثال کامل - Issue [#382](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/382) — thread tracking canonical Cloudflare anti-bot - Issue [#309](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/309) — roadmap CF WARP integration (approach جایگزین، longer-horizon) + +
diff --git a/assets/exit_node/README.md b/assets/exit_node/README.md index 4ac4788..118bc1e 100644 --- a/assets/exit_node/README.md +++ b/assets/exit_node/README.md @@ -1,176 +1,183 @@ -# Exit node — bypass Cloudflare anti-bot for ChatGPT / Claude / Grok / X +# Exit node — bypassing CF anti-bot for ChatGPT / Claude / Grok / X -Many Cloudflare-protected services flag traffic from Google datacenter -IP ranges as bots and serve a Turnstile / interactive CAPTCHA / 502 -challenge instead of the actual page. Apps Script's `UrlFetchApp.fetch()` -exits from those Google datacenter IPs, so for sites like: +Many Cloudflare-fronted services flag traffic from Google datacenter +IPs as bots and serve a Turnstile / CAPTCHA / 502 challenge instead of +the real page. `UrlFetchApp.fetch()` in Apps Script always exits from +Google's datacenter IP space, so for sites like: -- **chatgpt.com / openai.com** (Cloudflare anti-bot, often blocks GCP IPs) -- **claude.ai** (same) -- **grok.com / x.com** (CF-fronted, returns 502 on Google IPs) +- **chatgpt.com / openai.com** +- **claude.ai** +- **grok.com / x.com** -…the regular mhrv-rs apps_script-mode path returns errors like -`Relay error: json: key must be a string at line 2 column 1` or -`502 Relay error` because Code.gs is wrapping a CF challenge HTML -page that the client can't make sense of. +…mhrv-rs's normal apps_script-mode path returns errors like `Relay +error: json: key must be a string at line 2 column 1` or `502 Relay +error` because Code.gs is wrapping a CF challenge HTML page that the +client can't parse as relay JSON. -The **exit node** is a small TypeScript HTTP endpoint deployed on a -serverless platform (val.town, Deno Deploy, fly.io, etc.) that sits -between Apps Script and the destination. The traffic chain becomes: +The **exit node** is a small TypeScript HTTP handler you deploy on a +serverless TypeScript host you control. It sits between Apps Script +and the destination, so the request chain becomes: ``` -Browser ─┐ ┌─→ Destination - │ │ (chatgpt.com) - ▼ │ - mhrv-rs │ - │ │ - │ TLS to Google IP, SNI=www.google.com (DPI cover) │ - ▼ │ - Apps Script (Google datacenter) │ - │ │ - │ UrlFetchApp.fetch(EXIT_NODE_URL) │ - ▼ │ - val.town (non-Google IP) │ - │ │ - │ fetch(real_url) │ - └──────────────────────────────────────────────────────┘ +Browser ─┐ ┌─→ Destination + │ │ (chatgpt.com) + ▼ │ + mhrv-rs │ + │ │ + │ TLS to Google IP, SNI=www.google.com (DPI cover)│ + ▼ │ + Apps Script (Google datacenter) │ + │ │ + │ UrlFetchApp.fetch(EXIT_NODE_URL) │ + ▼ │ + your exit node (non-Google IP) │ + │ │ + │ fetch(real_url) │ + └──────────────────────────────────────────────────┘ ``` -The destination sees the val.town IP, not Google datacenter. CF's -anti-bot heuristic doesn't fire, and you get the actual page. +The destination sees the exit node's outbound IP, not a Google +datacenter IP. CF's anti-bot heuristic doesn't fire and the real page +comes back. -Crucially: **the user-side leg (Iran ISP → Apps Script) is unchanged.** -The ISP still only sees TLS to a Google IP — 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. +**Important property preserved:** the user-side leg (Iran ISP → +Apps Script) is unchanged. The ISP only sees TLS to a Google IP — the +second hop happens entirely inside Apps Script's outbound, invisible +from the user's network. The DPI evasion property mhrv-rs is built +around stays intact. ## Setup -1. **Sign up at [val.town](https://val.town)** (free tier is fine — - the free tier's outbound bandwidth is enough for personal use). -2. **Create a new HTTP val** (TypeScript). On val.town: New → HTTP. -3. **Paste the contents of `valtown.ts`** from this directory. -4. **Set the PSK** at the top of the file: +The handler in [`exit_node.ts`](exit_node.ts) is plain TypeScript that +uses only web-standard APIs (`Request`, `Response`, `fetch`). It runs +on any platform with a serverless-fetch runtime. + +### Generic steps (apply to every host) + +1. **Open `exit_node.ts`** and replace the placeholder PSK at the top: ```ts const PSK = ""; ``` - Generate a strong secret with `openssl rand -hex 32` from a terminal. - **Don't leave the placeholder in production** — the val.town code - intentionally fails closed (returns 503 on every request) until - you replace the placeholder, so you can't accidentally serve as - an open relay. -5. **Save** the val. Copy the val's public URL — it looks like - `https://your-handle-mhrv.web.val.run`. -6. **In your mhrv-rs `config.json`**, add an `exit_node` block: + Generate a strong secret with `openssl rand -hex 32`. **Do not leave + the placeholder** — the script is deliberately fail-closed (returns + 503 on every request until the placeholder is replaced) so a fresh + deploy without configuration can't accidentally serve as an open + relay. +2. **Deploy** to your chosen host (see options below). +3. **Copy the public URL** of the deployed handler. +4. **In `mhrv-rs` config.json**, add an `exit_node` block: ```json "exit_node": { "enabled": true, - "relay_url": "https://your-handle-mhrv.web.val.run", - "psk": "", + "relay_url": "https://your-deployed-exit-node.example.com", + "psk": "", "mode": "selective", "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"] } ``` -7. **Restart mhrv-rs** (Disconnect + Connect, or `kill` + restart the +5. **Restart mhrv-rs** (Disconnect + Connect, or kill + restart the binary). -8. **Test** — visit `chatgpt.com` or `grok.com` from a browser pointed - at the mhrv-rs proxy. You should see the actual login page now, - not a CF challenge. +6. **Test** — open `chatgpt.com` or `grok.com` from a browser pointed + at mhrv-rs's proxy. You should see the real login page, not a CF + challenge. A complete example config is at [`config.exit-node.example.json`](../../config.exit-node.example.json) in the repo root. -## How `selective` vs `full` mode pick +### Hosting options + +The script is one self-contained file. Pick whichever host you can +sign up for and trust: + +| Host | Notes | +|---|---| +| **Deno Deploy** ([deno.com/deploy](https://deno.com/deploy)) | Free tier covers personal use. Deploy via `deployctl deploy --prod exit_node.ts` or via GitHub Actions. Same web-standard API as the script expects. | +| **fly.io** | Free tier with limits. Wrap the handler in a thin server (`Deno.serve(handler)` for Deno or an Express wrapper for Node) + add a Dockerfile. Persistent IPs, picks geographic region. | +| **Your own VPS** | Run `deno run --allow-net wrapper.ts` where `wrapper.ts` does `Deno.serve({ port: 8443 }, handler)`. Most control, ~$3-5/mo. | +| **Cloudflare Workers** | **Doesn't help.** CF Workers exit through CF's own IP space, which CF anti-bot still flags as worker-internal traffic. | + +For most users running locally, Deno Deploy is the fastest setup. For +a long-term deployment you control end-to-end, your own small VPS is +ideal. + +## `selective` vs `full` | Mode | What it does | When to use | |---|---|---| -| `selective` (default) | Only hosts in `hosts` route via exit node; everything else takes the regular Apps Script path | Recommended. The exit-node hop adds ~200-500ms per request, so reserve it for sites that need a non-Google IP. | -| `full` | Every request routes via exit node | Only useful when your entire workload is CF-anti-bot affected, or when the exit node happens to be faster than Apps Script alone for your network path (rare). Burns val.town runtime budget for sites that don't need it. | +| `selective` (default) | Only hosts in `hosts` route via the exit node; everything else takes the normal Apps Script path | Recommended. The exit-node hop adds ~200-500ms per request, so reserve it for sites that actually need a non-Google IP. | +| `full` | Every request routes via the exit node | Only when your entire workload is CF-anti-bot affected, or when your exit node is faster than Apps Script on your network path (rare). Burns the exit node's runtime budget on sites that don't need it. | -## Failure mode +## Behaviour on failure -If the exit node is unreachable, returns a 5xx, or returns a malformed -response, mhrv-rs **falls back to the regular Apps Script relay -automatically**. You'll see a `warn: exit node failed for ... — falling -back to direct Apps Script` line in the log. Sites that need the exit -node will fail in that case (CF challenge), but other sites work -normally — a down exit node doesn't take you fully offline. +If the exit node is unreachable, returns 5xx, or returns a malformed +response, mhrv-rs **automatically falls back to the regular Apps +Script relay**. The log shows a `warn: exit node failed for ... — +falling back to direct Apps Script` line. The CF-affected sites then +fail (CF challenge), but every other site keeps working — a downed +exit node doesn't take you fully offline. ## Security model -The PSK is the only thing keeping the val.town endpoint from being a +The PSK is the only thing keeping the deployed endpoint from being a public open proxy. Treat it like a password: -- **Don't commit** the PSK to source control. The val.town source - is private to your account by default; keep it that way. -- **Don't share** the PSK publicly. Anyone who has both the URL and - the PSK can use your val.town quota as their own proxy. -- **Rotate** if you suspect leak. Change the PSK in val.town source, - save, then update `psk` in mhrv-rs `config.json` and restart. +- **Don't commit** the PSK to source control. Most TypeScript hosts + default deployed code to private; keep it that way. +- **Don't share publicly.** Anyone with both the URL and the PSK can + use the deployment as their own proxy and burn your runtime quota. +- **Rotate** if you suspect a leak. Change the PSK in the deployed + source, redeploy, then update `psk` in `mhrv-rs` config.json and + restart. -The val.town script also includes a **loop guard** (refuses to fetch -its own host) and **placeholder check** (returns 503 if `PSK === -"CHANGE_ME_TO_A_STRONG_SECRET"`) so a fresh deploy without setup can't -accidentally serve as an open relay. +The script also includes a **loop guard** (refuses to fetch its own +host) and a **placeholder check** (returns 503 if `PSK === +"CHANGE_ME_TO_A_STRONG_SECRET"`) so a fresh deploy without +configuration can't be accidentally served as an open relay. -## Alternative platforms +## Why isn't this on by default? -The `valtown.ts` script is plain TypeScript using web-standard APIs -(`Request`, `Response`, `fetch`). It runs on: +- Adds ~200-500ms per request through the exit-node hop +- Burns the host's free-tier runtime quota +- No benefit for sites that don't have CF anti-bot +- Requires signing up for a separate third-party platform -- **val.town** — easiest, free tier sufficient for personal use -- **Deno Deploy** — similar API; deploy with `deployctl` -- **fly.io** — needs a `Dockerfile` wrapper; gives you a fixed - geographic region -- **Cloudflare Workers** — won't help (CF Workers exit from CF's own - IP space, which CF anti-bot still flags as worker-internal) - -For most users, val.town's the right choice. Deno Deploy if you want -a non-val.town option for redundancy. - -## Why not always-on by default - -- Adds 200-500ms per request (extra hop) -- Burns val.town's free-tier bandwidth budget -- Offers no benefit for sites that don't have CF anti-bot -- Setup requires a separate account on a third-party platform - -So `enabled: false` is the default. Users who care about ChatGPT / -Claude / Grok specifically opt in; everyone else runs lighter. +So `enabled: false` is the default. Users who specifically need +ChatGPT / Claude / Grok opt in; everyone else runs lighter. ## Troubleshooting **`exit node refused or errored: unauthorized`** — PSK mismatch. -Check that the `psk` in `config.json` exactly matches the `PSK` -constant in val.town. Whitespace and quoting matter. +Double-check `psk` in `config.json` matches the `PSK` constant in your +deployed source character-for-character. Whitespace and quoting +matter. -**`exit node refused or errored: exit_node misconfigured: PSK is still -the placeholder`** — you forgot to replace `CHANGE_ME_TO_A_STRONG_SECRET` -in val.town. Edit + save the val. +**`exit_node misconfigured: PSK is still the placeholder`** — you +forgot to replace `CHANGE_ME_TO_A_STRONG_SECRET` in the source. Edit +the deployed file, save, redeploy. -**`exit node failed for ...: connection refused`** — the val.town URL -is wrong or the val isn't deployed. Verify by hitting the URL directly -from a browser — it should return `{"e":"method_not_allowed"}` (val +**`exit node failed for ...: connection refused`** — the URL is wrong +or the deployment isn't live. Verify by hitting the URL in a browser +— it should respond with `{"e":"method_not_allowed"}` (the script expects POST). -**`exit node failed for ...: timeout`** — val.town outbound is slow -or the destination is slow. Try a different val.town deployment region, -or accept the latency trade-off. +**`exit node failed for ...: timeout`** — the host's outbound or the +destination is slow. Try a different region, or accept the latency +trade-off. -**Site still shows CF challenge after enabling exit node** — CF is -flagging val.town's IP too. Some CF customers explicitly blocklist -val.town. Workarounds: try Deno Deploy instead, or add the site to -`passthrough_hosts` (bypasses MITM entirely; uses your real ISP IP). +**Site still shows a CF challenge after enabling the exit node** — +CF has flagged your host's IP too. Some hosting providers' outbound +IP space is on CF's bot blocklist. Workarounds: try a different host +(your own VPS gives you a clean IP), or add the affected site to +`passthrough_hosts` to bypass the MITM and use your real ISP IP. ## See also -- [Persian translation](README.fa.md) of this doc -- [`valtown.ts`](valtown.ts) — the val.town source (with hardening) +- [Persian (راهنمای فارسی)](README.fa.md) version of this doc +- [`exit_node.ts`](exit_node.ts) — the handler source (with hardening) - [`config.exit-node.example.json`](../../config.exit-node.example.json) - — complete example config + — complete example mhrv-rs config - Issue [#382](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/382) - — canonical Cloudflare anti-bot tracking thread + — canonical thread tracking Cloudflare anti-bot - Issue [#309](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/309) - — CF WARP integration roadmap (alternative approach, longer-horizon) + — roadmap for CF WARP integration (alternative approach, longer-horizon) diff --git a/assets/exit_node/valtown.ts b/assets/exit_node/exit_node.ts similarity index 80% rename from assets/exit_node/valtown.ts rename to assets/exit_node/exit_node.ts index 2740074..0e6a32c 100644 --- a/assets/exit_node/valtown.ts +++ b/assets/exit_node/exit_node.ts @@ -1,25 +1,27 @@ -// mhrv-rs exit node — deploy as an HTTP endpoint on val.town (or Deno -// Deploy, fly.io, anywhere with a public residential-adjacent IP). +// mhrv-rs exit node — deploy as an HTTP endpoint on any serverless +// TypeScript host with a public IP that isn't a Google datacenter +// (Deno Deploy, fly.io, your own VPS, etc.). Uses only web-standard +// `Request` / `Response` / `fetch` so it's portable across runtimes. // // Purpose: chain client → Apps Script → this exit node → destination. // Apps Script's UrlFetchApp can't reach Cloudflare-protected sites that -// flag Google datacenter IPs as bots (chatgpt.com, claude.ai, grok.x.ai, +// flag Google datacenter IPs as bots (chatgpt.com, claude.ai, grok.com, // many other CF-fronted SaaS). This exit node sits between Apps Script -// and the destination; the destination sees the exit node's IP (val.town's -// outbound, generally not flagged as Google datacenter) and accepts the -// request. +// and the destination; the destination sees the exit node's outbound IP +// (generally not flagged as Google datacenter) and accepts the request. // // Setup: -// 1. Sign in to https://val.town and create a new HTTP val (TypeScript) -// 2. Paste the contents of this file -// 3. Set PSK below to a strong secret (use `openssl rand -hex 32` -// from a terminal — DO NOT leave the placeholder in production) -// 4. Save and copy the val's public URL (looks like -// https://your-handle-mhrv.web.val.run) +// 1. Pick a host that runs web-standard fetch handlers (e.g. Deno +// Deploy, fly.io with a thin server wrapper, or any cheap VPS +// running Deno / Node + this script as a handler). +// 2. Paste the contents of this file as the request handler. +// 3. Set PSK below to a strong secret (`openssl rand -hex 32` from +// a terminal — DO NOT leave the placeholder in production). +// 4. Deploy and copy the public URL of the deployed handler. // 5. In mhrv-rs config.json, add: // "exit_node": { // "enabled": true, -// "relay_url": "https://your-handle-mhrv.web.val.run", +// "relay_url": "https://your-deployed-exit-node.example.com", // "psk": "", // "mode": "selective", // "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com"] @@ -87,7 +89,7 @@ export default async function (req: Request): Promise { { e: "exit_node misconfigured: PSK is still the placeholder. Set " + - "a strong secret in the val.town source before deploying.", + "a strong secret in the source before deploying.", }, { status: 503 }, ); @@ -118,7 +120,7 @@ export default async function (req: Request): Promise { // Loop guard: if u points at this exit node's own host, refuse. // Without this, a misconfigured client could chain exit-node → - // exit-node → exit-node → ... and burn the val.town runtime budget. + // exit-node → exit-node → ... and burn the host's runtime budget. try { const reqUrl = new URL(req.url); const dstUrl = new URL(u); diff --git a/config.exit-node.example.json b/config.exit-node.example.json index 2b36407..8af5516 100644 --- a/config.exit-node.example.json +++ b/config.exit-node.example.json @@ -1,5 +1,5 @@ { - "_comment": "Example config for using mhrv-rs with a val.town exit node to bypass Cloudflare anti-bot blocks on chatgpt.com / claude.ai / grok.com / x.com. See assets/exit_node/README.md for the val.town deployment walkthrough.", + "_comment": "Example config for using mhrv-rs with an exit-node deployment to bypass Cloudflare anti-bot blocks on chatgpt.com / claude.ai / grok.com / x.com. See assets/exit_node/README.md for the deployment walkthrough.", "mode": "apps_script", "google_ip": "216.239.38.120", "front_domain": "www.google.com", @@ -15,13 +15,13 @@ "exit_node": { "_comment": "Master switch. Set false to disable exit-node entirely without removing the config. Default false.", "enabled": true, - "_comment_relay_url": "Public URL of your val.town deployment (or Deno Deploy, fly.io, etc. running assets/exit_node/valtown.ts).", - "relay_url": "https://your-handle-mhrv.web.val.run", - "_comment_psk": "Pre-shared key — must match the PSK constant in your val.town source. Generate with: openssl rand -hex 32", - "psk": "PUT_YOUR_VAL_TOWN_PSK_HERE", + "_comment_relay_url": "Public URL of your deployed exit-node handler (assets/exit_node/exit_node.ts running on Deno Deploy, fly.io, your own VPS, etc.).", + "relay_url": "https://your-deployed-exit-node.example.com", + "_comment_psk": "Pre-shared key — must match the PSK constant in your deployed source. Generate with: openssl rand -hex 32", + "psk": "PUT_YOUR_EXIT_NODE_PSK_HERE", "_comment_mode": "selective: only `hosts` route via exit node (recommended). full: every request routes via exit node (slower, ~250-500ms extra hop).", "mode": "selective", - "_comment_hosts": "Hostnames to route through the exit node. Matches exact OR dot-anchored suffix (chatgpt.com covers api.chatgpt.com etc.). The default community list — extend for any other CF-anti-bot blocked sites you need.", + "_comment_hosts": "Hostnames to route through the exit node. Matches exact OR dot-anchored suffix (chatgpt.com covers api.chatgpt.com etc.). Extend for any CF-anti-bot blocked sites you need.", "hosts": [ "chatgpt.com", "claude.ai", diff --git a/docs/changelog/v1.9.10.md b/docs/changelog/v1.9.10.md new file mode 100644 index 0000000..d9d55d6 --- /dev/null +++ b/docs/changelog/v1.9.10.md @@ -0,0 +1,8 @@ + +• 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. diff --git a/docs/changelog/v1.9.4.md b/docs/changelog/v1.9.4.md index 80253e7..d746501 100644 --- a/docs/changelog/v1.9.4.md +++ b/docs/changelog/v1.9.4.md @@ -1,9 +1,9 @@ -• exit node اختیاری برای دور زدن CF anti-bot روی ChatGPT / Claude / Grok / X (port از upstream [`masterking32/MasterHttpRelayVPN@464a6e1d`](https://github.com/masterking32/MasterHttpRelayVPN/commit/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 اختیاری برای دور زدن CF anti-bot روی ChatGPT / Claude / Grok / X (port از upstream [`masterking32/MasterHttpRelayVPN@464a6e1d`](https://github.com/masterking32/MasterHttpRelayVPN/commit/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/exit_node.ts`) روی Deno Deploy / fly.io deploy می‌شه + بین Apps Script + destination قرار می‌گیره. مسیر traffic: `client → SNI rewrite → Apps Script (Google IP) → the exit node (non-Google IP) → destination`. destination IP exit node رو می‌بینه، نه Google datacenter — heuristic anti-bot CF نمی‌سوزه + صفحه واقعی برمی‌گرده. **leg user-side (Iran ISP → Apps Script) بدون تغییر** — second hop کاملاً درون outbound Apps Script اجرا می‌شه، invisible از شبکه‌ی کاربر. config جدید: ```json "exit_node": { "enabled": true, - "relay_url": "https://your-handle-mhrv.web.val.run", + "relay_url": "https://your-deployed-exit-node.example.com", "psk": "", "mode": "selective", "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"] @@ -12,15 +12,15 @@ دو 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`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/assets/exit_node/README.fa.md). config مثال در [`config.exit-node.example.json`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/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`](https://github.com/masterking32/MasterHttpRelayVPN/commit/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: +• Optional exit node to bypass CF anti-bot on ChatGPT / Claude / Grok / X (ported from upstream [`masterking32/MasterHttpRelayVPN@464a6e1d`](https://github.com/masterking32/MasterHttpRelayVPN/commit/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/exit_node.ts`) deployed on Deno Deploy / fly.io sits between Apps Script and the destination. Traffic chain: `client → SNI rewrite → Apps Script (Google IP) → the exit node (non-Google IP) → destination`. The destination sees the exit node'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: ```json "exit_node": { "enabled": true, - "relay_url": "https://your-handle-mhrv.web.val.run", + "relay_url": "https://your-deployed-exit-node.example.com", "psk": "", "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`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/assets/exit_node/README.md) (English) and [`README.fa.md`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/assets/exit_node/README.fa.md) (Persian). Complete example config at [`config.exit-node.example.json`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/config.exit-node.example.json). +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 exit-node 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`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/assets/exit_node/README.md) (English) and [`README.fa.md`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/assets/exit_node/README.fa.md) (Persian). Complete example config at [`config.exit-node.example.json`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/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. diff --git a/docs/changelog/v1.9.5.md b/docs/changelog/v1.9.5.md index 966237d..2e77c59 100644 --- a/docs/changelog/v1.9.5.md +++ b/docs/changelog/v1.9.5.md @@ -1,4 +1,4 @@ -• fix exit-node v1.9.4: مدارا با TLS ungraceful close (peer closed without close_notify) که val.town از Apps Script عبور می‌دهد ([#585](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/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 exit-node v1.9.4: مدارا با TLS ungraceful close (peer closed without close_notify) از سمت host exit-node ([#585](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/585) از @gregtheph): در v1.9.4، کاربری که exit node رو با درست‌ترین 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 (the exit node) 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 exit_node). 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](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/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. +• Fix v1.9.4 exit-node: tolerate ungraceful TLS close (peer closed without close_notify) on the exit-node path ([#585](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/585) by @gregtheph): in v1.9.4, users with a correctly-configured exit-node 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 (the exit-node'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 + exit-node envelope unwrap success and error paths). 173 lib tests + 33 tunnel-node tests passing. diff --git a/docs/changelog/v1.9.9.md b/docs/changelog/v1.9.9.md index 0c4354c..b2af4d5 100644 --- a/docs/changelog/v1.9.9.md +++ b/docs/changelog/v1.9.9.md @@ -6,7 +6,7 @@ - **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](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/701). AI Studio روی Iran IP sanction می‌خوره (نه Apps Script طرف ما). exit-node IP val.town رو می‌بینه که نه Iran است نه Google datacenter. +• Example config exit-node با `aistudio.google.com` و `ai.google.dev` — درخواست از [#701](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/701). AI Studio روی Iran IP sanction می‌خوره (نه Apps Script طرف ما). مقصد IP exit node رو می‌بینه که نه Iran است نه Google datacenter. • Example config fronting-groups با Reddit / Fastly / Pinterest / CNN / BuzzFeed family domains اضافه شد (PR [#696](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pull/696) از @Shjpr9). همه روی Fastly Anycast 151.101.x.x — کاربران می‌تونن از example بیشتر دامنه برداشت کنن، اونی که در شبکه‌شان کار می‌کنه نگه دارن. • تست: ۱۷۹ lib + ۳۵ tunnel-node test همه pass. --- @@ -17,6 +17,6 @@ - **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](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/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 exit-node now lists `aistudio.google.com` and `ai.google.dev` — requested in [#701](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/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 the exit node's IP, which is neither Iran nor a Google datacenter. • Example config fronting-groups gained Reddit / Fastly / Pinterest / CNN / BuzzFeed family domains (PR [#696](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pull/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. diff --git a/docs/guide.fa.md b/docs/guide.fa.md index 55a1395..3e95ffc 100644 --- a/docs/guide.fa.md +++ b/docs/guide.fa.md @@ -257,13 +257,13 @@ HTTP / HTTPS مثل قبل از Apps Script می‌رود (تغییری نمی ## Exit node -سرویس‌های پشت Cloudflare (chatgpt.com، claude.ai، grok.com، x.com، openai.com) ترافیک از IPهای دیتاسنتر گوگل را به‌عنوان bot شناسایی می‌کنند و چالش Turnstile / CAPTCHA می‌فرستند. راه‌حل exit node یک HTTP endpoint کوچک TypeScript است که روی val.town (رایگان) دیپلوی می‌کنی و بین Apps Script و مقصد قرار می‌گیرد: +سرویس‌های پشت Cloudflare (chatgpt.com، claude.ai، grok.com، x.com، openai.com) ترافیک از IPهای دیتاسنتر گوگل را به‌عنوان bot شناسایی می‌کنند و چالش Turnstile / CAPTCHA می‌فرستند. راه‌حل exit node یک handler کوچک TypeScript است که روی یک host serverless (Deno Deploy، fly.io، یا VPS شخصی خودت) دیپلوی می‌کنی و بین Apps Script و مقصد قرار می‌گیرد: ``` -کلاینت → Apps Script (IP گوگل) → val.town (IP غیر گوگل) → سایت پشت CF +کلاینت → Apps Script (IP گوگل) → exit node خودت (IP غیر گوگل) → سایت پشت CF ``` -مقصد IP val.town را می‌بیند نه IP گوگل، پس heuristic ضدبات شلیک نمی‌کند. +مقصد IP خروجی exit node را می‌بیند نه IP گوگل، پس heuristic ضدبات شلیک نمی‌کند. **راه‌اندازی:** [`assets/exit_node/README.fa.md`](../assets/exit_node/README.fa.md). ۵ دقیقه، سهمیهٔ رایگان. diff --git a/docs/guide.md b/docs/guide.md index 9b21a08..55ee955 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -257,13 +257,13 @@ More deployments = more total concurrency = lower per-session latency. Each batc ## Exit node -Cloudflare-fronted services (chatgpt.com, claude.ai, grok.com, x.com, openai.com) flag traffic from Google datacenter IPs as bots and serve a Turnstile / CAPTCHA challenge. The exit node fix is a small TypeScript HTTP endpoint you deploy on val.town (free) that sits between Apps Script and the destination: +Cloudflare-fronted services (chatgpt.com, claude.ai, grok.com, x.com, openai.com) flag traffic from Google datacenter IPs as bots and serve a Turnstile / CAPTCHA challenge. The exit node fix is a small TypeScript HTTP handler you deploy on a serverless host (Deno Deploy, fly.io, or your own VPS) that sits between Apps Script and the destination: ``` -client → Apps Script (Google IP) → val.town (non-Google IP) → CF-protected site +client → Apps Script (Google IP) → your exit node (non-Google IP) → CF-protected site ``` -The destination sees val.town's IP, not Google's, so the anti-bot heuristic doesn't fire. +The destination sees the exit node's IP, not Google's, so the anti-bot heuristic doesn't fire. **Setup:** [`assets/exit_node/README.md`](../assets/exit_node/README.md). 5 min, free tier. diff --git a/src/bin/ui.rs b/src/bin/ui.rs index 837d9e7..4042c40 100644 --- a/src/bin/ui.rs +++ b/src/bin/ui.rs @@ -283,7 +283,7 @@ struct FormState { request_timeout_secs: u64, /// Optional second-hop exit node for CF-anti-bot bypass (chatgpt.com / /// claude.ai / grok.com / x.com). Config-only — no UI editor yet. - /// See `assets/exit_node/` for the val.town deployment script. + /// See `assets/exit_node/` for the generic exit-node handler. exit_node: mhrv_rs::config::ExitNodeConfig, } @@ -676,7 +676,7 @@ struct ConfigWire<'a> { #[serde(skip_serializing_if = "is_default_timeout_secs")] request_timeout_secs: u64, /// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai / - /// grok.com / x.com via val.town second-hop relay). Skip when fully + /// grok.com / x.com via exit-node second-hop relay). Skip when fully /// default (disabled with no URL/PSK/hosts) so configs without /// exit-node setup stay clean. Round-tripped through FormState so /// Save preserves user-edited values. diff --git a/src/config.rs b/src/config.rs index d0281a6..f611e63 100644 --- a/src/config.rs +++ b/src/config.rs @@ -341,7 +341,7 @@ pub struct Config { /// /// Architecture: chain becomes /// `client → SNI rewrite → Apps Script (Google IP) → exit node - /// (val.town / Deno Deploy / etc., non-Google IP) → destination` + /// (Deno Deploy / fly.io / etc., non-Google IP) → destination` /// /// The destination sees the exit node's outbound IP, not Google's. /// CF anti-bot's "this is a Google datacenter" heuristic doesn't @@ -362,9 +362,9 @@ pub struct ExitNodeConfig { #[serde(default)] pub enabled: bool, - /// HTTPS URL of the exit-node endpoint. Typically a val.town / - /// Deno Deploy / fly.io serverless deployment running the - /// `assets/exit_node/valtown.ts` script (or an equivalent). The + /// HTTPS URL of the exit-node endpoint. Typically a Deno Deploy / + /// fly.io serverless deployment (or your own VPS) running the + /// `assets/exit_node/exit_node.ts` script (or an equivalent). The /// exit node is what makes the outbound `fetch()` call to the /// destination, so its IP is what the destination sees. #[serde(default)] diff --git a/src/domain_fronter.rs b/src/domain_fronter.rs index 38a454b..953cc2e 100644 --- a/src/domain_fronter.rs +++ b/src/domain_fronter.rs @@ -151,7 +151,7 @@ pub struct DomainFronter { /// (#430, masterking32 PR #25). Read by `tunnel_client::fire_batch` /// so a single config field tunes the timeout used everywhere. batch_timeout: Duration, - /// Optional second-hop exit node (val.town / Deno Deploy / etc.) + /// Optional second-hop exit node (Deno Deploy / fly.io / etc.) /// to bypass CF-anti-bot blocks on sites that flag Google datacenter /// IPs (chatgpt.com, claude.ai, grok.com, x.com). Mirrors /// `Config::exit_node`. When `exit_node_enabled` is false (the more @@ -770,14 +770,14 @@ impl DomainFronter { }; // Exit-node short-circuit: route through the configured second-hop - // relay (val.town / Deno Deploy / etc.) for hosts that need a + // relay (Deno Deploy / fly.io / etc.) for hosts that need a // non-Google exit IP. The cache + coalesce layer below is bypassed // for these — exit-node-eligible hosts are the ones with active // anti-bot challenges (CF Turnstile, ChatGPT login, Claude.ai, // grok.com), and serving cached responses across users for those // would be wrong (auth tokens, session state, per-user // personalization). Falls back to the regular Apps Script relay - // if the exit node fails (network error, 5xx from val.town, etc.) + // if the exit node fails (network error, 5xx from the exit node, etc.) // so a misconfigured or down exit node doesn't take the user // offline for the sites that DON'T need it. if self.exit_node_matches(url) { @@ -1285,7 +1285,7 @@ impl DomainFronter { /// ```text /// client → SNI rewrite → Apps Script (Google IP) /// → UrlFetchApp.fetch(exit_node_url) - /// → exit node (val.town, non-Google IP) + /// → exit node (non-Google IP) /// → fetch(real_url) /// → response back through both layers /// ``` @@ -1296,7 +1296,7 @@ impl DomainFronter { /// destination, returns a `{s, h, b}` JSON envelope. Apps Script /// returns that envelope as the body of its raw HTTP response /// (because we set `r: true`). We then unwrap one extra layer: - /// extract Apps Script's body → parse the val.town JSON → reconstruct + /// extract Apps Script's body → parse the exit-node JSON → reconstruct /// the destination's raw HTTP response so the rest of the proxy /// pipeline (MITM TLS write-back) sees the same shape it gets from /// the regular path. @@ -1314,7 +1314,7 @@ impl DomainFronter { // Reusing build_payload_json keeps the outer envelope consistent // with everything else (including the random padding for DPI // evasion). The `r: true` flag in RelayRequest makes Code.gs - // return val.town's raw HTTP response, which is what we want to + // return exit-node's raw HTTP response, which is what we want to // unwrap below. let exit_url = self.exit_node_url.clone(); let outer_headers = vec![( @@ -1325,12 +1325,12 @@ impl DomainFronter { self.build_payload_json("POST", &exit_url, &outer_headers, &inner_json)?; // Send the outer payload through the relay machinery and get back - // Apps Script's response body (which is val.town's JSON envelope). + // Apps Script's response body (which is exit-node's JSON envelope). let app_body = self .send_prebuilt_payload_through_relay(outer_payload) .await?; - // val.town's JSON envelope: {s: u16, h: {...}, b: ""} on + // exit-node's JSON envelope: {s: u16, h: {...}, b: ""} on // success, {e: "..."} on its own internal error. parse_exit_node_response(&app_body) } @@ -1376,7 +1376,7 @@ impl DomainFronter { h: hmap, b: b_encoded, ct, - r: false, // val.town returns its own JSON envelope, not raw HTTP + r: false, // the exit node returns its own JSON envelope, not raw HTTP }; Ok(serde_json::to_vec(&req)?) } @@ -1385,7 +1385,7 @@ impl DomainFronter { /// a payload we already built. Mirrors `do_relay_once_with` but /// returns the **raw response body bytes** (Apps Script's HTTP body) /// instead of running the body through `parse_relay_json` — the - /// exit-node path needs to peel off val.town's JSON envelope, which + /// exit-node path needs to peel off exit-node's JSON envelope, which /// has a different shape from Code.gs's raw-HTTP wrapping. async fn send_prebuilt_payload_through_relay( &self, @@ -2133,12 +2133,12 @@ fn unix_to_ymd_utc(secs: u64) -> (i64, u32, u32) { (y, m as u32, d as u32) } -/// Parse the val.town exit-node JSON envelope back into a raw HTTP/1.1 +/// Parse the exit-node JSON envelope back into a raw HTTP/1.1 /// response. The envelope shape is: /// /// - On success: `{ "s": , "h": { ... }, "b": "" }` /// - On exit-node-side error: `{ "e": "" }` with HTTP 4xx/5xx -/// from val.town's own status code (decoded from the outer Apps Script +/// from exit-node's own status code (decoded from the outer Apps Script /// layer, not the inner field). /// /// We synthesize a complete HTTP/1.1 response from these fields so the @@ -2153,8 +2153,8 @@ fn parse_exit_node_response(body: &[u8]) -> Result, FronterError> { )) })?; - // Surface val.town's internal errors clearly rather than as a 502 - // from the outer envelope. The `{e: "..."}` shape is what the val.town + // Surface exit-node's internal errors clearly rather than as a 502 + // from the outer envelope. The `{e: "..."}` shape is what the exit-node's // script emits on bad PSK, malformed URL, or any caught exception. if let Some(err_msg) = v.get("e").and_then(|x| x.as_str()) { return Err(FronterError::Relay(format!( @@ -2484,7 +2484,7 @@ where let want = need.min(tmp.len()); // Handle ungraceful TLS close-without-close_notify (rustls // surfaces this as `io::ErrorKind::UnexpectedEof`). Some - // origins — notably val.town's exit-node path through Apps + // origins — notably exit-node path through Apps // Script (#585, v1.9.4) and certain Apps Script `Connection: // close` responses — terminate the underlying TCP without // sending the TLS close_notify alert first. Treat that the @@ -3085,7 +3085,7 @@ mod tests { #[tokio::test] async fn read_http_response_tolerates_unexpected_eof_with_content_length() { - // Issue #585 / v1.9.4 exit-node bug. Some peers (val.town in + // Issue #585 / v1.9.4 exit-node bug. Some peers (the deployed exit-node in // particular, certain Apps Script `Connection: close` paths) close // the TCP without TLS close_notify. Body should still be returned // when Content-Length is satisfied, even though the read after @@ -3130,8 +3130,8 @@ mod tests { } #[tokio::test] - async fn parse_exit_node_response_unwraps_valtown_envelope() { - // The exit-node path through Apps Script returns val.town's JSON + async fn parse_exit_node_response_unwraps_exit_node_envelope() { + // The exit-node path through Apps Script returns exit node's JSON // envelope as the response body. parse_exit_node_response must // unwrap it back into a raw HTTP/1.1 response so the MITM TLS // write-back path sees the same shape it gets from the regular @@ -3150,7 +3150,7 @@ mod tests { #[tokio::test] async fn parse_exit_node_response_surfaces_explicit_error() { - // When val.town returns `{e: "..."}` instead of the {s,h,b} shape, + // When the exit node returns `{e: "..."}` instead of the {s,h,b} shape, // surface that error message specifically rather than letting // it through as an unparseable 502 — the message string is what // tells the user what went wrong (placeholder PSK, bad URL,