mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 23:54:48 +03:00
chore: redact val.town from code and docs, rename exit-node script
The val.town founder asked us not to promote using their service. This commit removes every val.town reference from the codebase and rewrites the exit-node guides to be platform-agnostic. Changes: - Renamed assets/exit_node/valtown.ts → assets/exit_node/exit_node.ts. TypeScript itself is unchanged — same web-standard Request/Response/ fetch API that runs on any serverless runtime. - Rewrote assets/exit_node/README.md and README.fa.md to recommend Deno Deploy as the primary host for users who want a free serverless TS endpoint, with fly.io and your-own-VPS as alternatives. CF Workers is explicitly called out as not-helpful (CF outbound is still on CF's flagged IP space). - Updated all val.town mentions in source comments (src/config.rs, src/domain_fronter.rs, src/bin/ui.rs) to neutral wording. - Updated config.exit-node.example.json `_comment` strings and the example URL. - Updated main README.md FAQ entries (Persian + English) and docs/guide.md / docs/guide.fa.md. - Old changelog files (v1.9.4 / v1.9.5 / v1.9.9) had val.town mentions retroactively replaced too — same redaction principle. - Bumped to v1.9.10 with a changelog noting the rename + Telegram channel brief format from earlier today. Users who already have an exit node deployed (on whichever host they picked) don't need to change anything — the wire protocol is identical and the renamed script is byte-identical to the old one. Tests: 179 lib + 35 tunnel-node green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -2222,7 +2222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mhrv-rs"
|
||||
version = "1.9.9"
|
||||
version = "1.9.10"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<div dir="rtl">
|
||||
|
||||
# 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 = "<your-strong-secret>";
|
||||
```
|
||||
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)
|
||||
|
||||
</div>
|
||||
|
||||
+126
-119
@@ -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 = "<your-strong-secret>";
|
||||
```
|
||||
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": "<the same PSK you set in step 4>",
|
||||
"relay_url": "https://your-deployed-exit-node.example.com",
|
||||
"psk": "<the same PSK you set in step 1>",
|
||||
"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)
|
||||
|
||||
@@ -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": "<the same PSK you set above>",
|
||||
// "mode": "selective",
|
||||
// "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com"]
|
||||
@@ -87,7 +89,7 @@ export default async function (req: Request): Promise<Response> {
|
||||
{
|
||||
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<Response> {
|
||||
|
||||
// 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);
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
|
||||
• 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.
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
|
||||
• 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": "<openssl rand -hex 32>",
|
||||
"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": "<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`](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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
|
||||
• 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+3
-3
@@ -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). ۵ دقیقه، سهمیهٔ رایگان.
|
||||
|
||||
|
||||
+3
-3
@@ -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.
|
||||
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
+4
-4
@@ -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)]
|
||||
|
||||
+19
-19
@@ -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: "<base64>"} on
|
||||
// exit-node's JSON envelope: {s: u16, h: {...}, b: "<base64>"} 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": <status u16>, "h": { ... }, "b": "<base64>" }`
|
||||
/// - On exit-node-side error: `{ "e": "<message>" }` 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<Vec<u8>, 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,
|
||||
|
||||
Reference in New Issue
Block a user