mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-19 08:04:39 +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]]
|
[[package]]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "1.9.9"
|
version = "1.9.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "1.9.9"
|
version = "1.9.10"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
||||||
license = "MIT"
|
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).
|
**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).
|
**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 میخورد).
|
**ویدیوی یوتیوب پخش نمیشود.** 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).
|
**تلگرام پایدار نیست.** تلگرام از 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
|
# Exit node — دور زدن CF anti-bot برای ChatGPT / Claude / Grok / X
|
||||||
|
|
||||||
بسیاری از سرویسهای پشت Cloudflare، traffic از رنج IP datacenter
|
بسیاری از سرویسهای پشت Cloudflare، traffic از رنج IP datacenter
|
||||||
@@ -14,9 +16,9 @@ Script از همان رنج IP datacenter Google خروج میکنه، پس
|
|||||||
`502 Relay error` میده چون Code.gs در حال wrap کردن صفحهی HTML
|
`502 Relay error` میده چون Code.gs در حال wrap کردن صفحهی HTML
|
||||||
challenge CF است که کلاینت نمیتونه parse کنه.
|
challenge CF است که کلاینت نمیتونه parse کنه.
|
||||||
|
|
||||||
**Exit node** یک endpoint کوچک TypeScript HTTP است که روی یک پلتفرم
|
**Exit node** یک handler کوچک HTTP به زبان TypeScript است که روی یک
|
||||||
serverless (val.town، Deno Deploy، fly.io، …) deploy میشه + بین Apps
|
پلتفرم serverless TypeScript که خودت تأییدش میکنی deploy میشه و بین
|
||||||
Script و destination قرار میگیره. مسیر traffic این میشه:
|
Apps Script و destination قرار میگیره. مسیر traffic این میشه:
|
||||||
|
|
||||||
```
|
```
|
||||||
Browser ─┐ ┌─→ Destination
|
Browser ─┐ ┌─→ Destination
|
||||||
@@ -30,14 +32,14 @@ Browser ─┐ ┌─→ Destinat
|
|||||||
│ │
|
│ │
|
||||||
│ UrlFetchApp.fetch(EXIT_NODE_URL) │
|
│ UrlFetchApp.fetch(EXIT_NODE_URL) │
|
||||||
▼ │
|
▼ │
|
||||||
val.town (non-Google IP) │
|
exit node خودت (IP غیر گوگل) │
|
||||||
│ │
|
│ │
|
||||||
│ fetch(real_url) │
|
│ fetch(real_url) │
|
||||||
└──────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Destination IP val.town رو میبینه، نه Google datacenter. heuristic
|
Destination IP خروجی exit node رو میبینه، نه IP datacenter گوگل.
|
||||||
anti-bot CF نمیسوزه + صفحه واقعی برمیگرده.
|
Heuristic anti-bot CF نمیسوزه + صفحه واقعی برمیگرده.
|
||||||
|
|
||||||
**نکته مهم:** leg user-side (Iran ISP → Apps Script) **بدون تغییر**
|
**نکته مهم:** leg user-side (Iran ISP → Apps Script) **بدون تغییر**
|
||||||
است. ISP فقط TLS به Google IP میبینه — second hop کاملاً درون
|
است. ISP فقط TLS به Google IP میبینه — second hop کاملاً درون
|
||||||
@@ -46,46 +48,65 @@ evasion property که mhrv-rs براش ساخته شده، دست نمیخو
|
|||||||
|
|
||||||
## راهاندازی
|
## راهاندازی
|
||||||
|
|
||||||
1. **در [val.town](https://val.town) ثبتنام کنید** (free tier کافی
|
handler در [`exit_node.ts`](exit_node.ts) plain TypeScript است که از
|
||||||
است — bandwidth outbound free tier برای personal use کافی).
|
APIهای web-standard (`Request`، `Response`، `fetch`) استفاده میکنه و
|
||||||
2. **یک HTTP val جدید بسازید** (TypeScript). در val.town: New → HTTP.
|
روی هر پلتفرمی که serverless-fetch runtime داره اجرا میشه.
|
||||||
3. **محتوای `valtown.ts`** از این directory رو paste کنید.
|
|
||||||
4. **PSK رو در بالای فایل تنظیم کنید**:
|
### مراحل عمومی (روی هر host)
|
||||||
|
|
||||||
|
۱. فایل [`exit_node.ts`](exit_node.ts) رو باز کنید و PSK پیشفرض رو در
|
||||||
|
ابتدا عوض کنید:
|
||||||
```ts
|
```ts
|
||||||
const PSK = "<your-strong-secret>";
|
const PSK = "<your-strong-secret>";
|
||||||
```
|
```
|
||||||
Strong secret تولید کنید با `openssl rand -hex 32` از terminal.
|
Strong secret تولید کنید با `openssl rand -hex 32` از terminal.
|
||||||
**placeholder رو در production نگذارید** — کد val.town عمداً
|
**placeholder رو در production نگذارید** — کد عمداً fail-closed است
|
||||||
fail-closed است (در هر request 503 برمیگردونه) تا placeholder
|
(در هر request 503 برمیگردونه) تا placeholder replace نشده، تا
|
||||||
replace نشده، تا جلوی serve شدن بهعنوان open relay accidentally
|
جلوی serve شدن بهعنوان open relay accidentally گرفته بشه.
|
||||||
گرفته بشه.
|
۲. فایل رو روی host انتخابی **deploy** کنید (گزینهها در ادامه).
|
||||||
5. **Save** کنید val رو. URL public val رو copy کنید — به این شکل:
|
۳. URL public deployment رو **copy** کنید.
|
||||||
`https://your-handle-mhrv.web.val.run`.
|
۴. در `config.json` mhrv-rs، block `exit_node` اضافه کنید:
|
||||||
6. **در `config.json` mhrv-rs**، block `exit_node` اضافه کنید:
|
|
||||||
```json
|
```json
|
||||||
"exit_node": {
|
"exit_node": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"relay_url": "https://your-handle-mhrv.web.val.run",
|
"relay_url": "https://your-deployed-exit-node.example.com",
|
||||||
"psk": "<همان PSK که در گام 4 گذاشتید>",
|
"psk": "<همان PSK که در گام ۱ گذاشتید>",
|
||||||
"mode": "selective",
|
"mode": "selective",
|
||||||
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
|
"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).
|
restart binary).
|
||||||
8. **تست کنید** — `chatgpt.com` یا `grok.com` رو از browser pointed به
|
۶. **تست** کنید — `chatgpt.com` یا `grok.com` رو از browser pointed به
|
||||||
mhrv-rs proxy باز کنید. صفحه login واقعی رو میبینید، نه CF challenge.
|
mhrv-rs proxy باز کنید. صفحه login واقعی رو میبینید، نه CF
|
||||||
|
challenge.
|
||||||
|
|
||||||
config مثال کامل در
|
config مثال کامل در
|
||||||
[`config.exit-node.example.json`](../../config.exit-node.example.json)
|
[`config.exit-node.example.json`](../../config.exit-node.example.json)
|
||||||
در root repo.
|
در 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`
|
## انتخاب `selective` vs `full`
|
||||||
|
|
||||||
| Mode | چی میکنه | کی استفاده کنید |
|
| Mode | چی میکنه | کی استفاده کنید |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `selective` (default) | فقط hosts در `hosts` از طریق exit node میرن؛ بقیه از مسیر Apps Script عادی | توصیه میشه. exit-node hop ~۲۰۰-۵۰۰ms به هر request اضافه میکنه — برای سایتهایی reserve کنید که نیاز به non-Google IP دارن. |
|
| `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
|
## رفتار در صورت failure
|
||||||
|
|
||||||
@@ -98,39 +119,25 @@ exit node down شما رو fully offline نمیکنه.
|
|||||||
|
|
||||||
## Security model
|
## Security model
|
||||||
|
|
||||||
PSK تنها چیز است که مانع میشه val.town endpoint یک public open proxy
|
PSK تنها چیز است که مانع میشه endpoint deployed یک public open proxy
|
||||||
بشه. مثل password برخورد کنید:
|
بشه. مثل password برخورد کنید:
|
||||||
|
|
||||||
- **commit نکنید** PSK رو به source control. منبع val.town بهطور
|
- **commit نکنید** PSK رو به source control. اکثر hostها بهطور default
|
||||||
default برای account شما private است؛ همانطور نگه دارید.
|
کد deployed رو private نگه میدارن؛ همانطور نگه دارید.
|
||||||
- **publicly share نکنید** PSK رو. هر کسی که هم URL هم PSK رو داره
|
- **publicly share نکنید** PSK رو. هر کسی که هم URL هم PSK رو داره
|
||||||
میتونه quota val.town شما رو بهعنوان proxy خود استفاده کنه.
|
میتونه quota host شما رو بهعنوان proxy خود استفاده کنه.
|
||||||
- **rotate** اگر leak مشکوک هست. PSK رو در val.town source تغییر بدید،
|
- **rotate** اگر leak مشکوک هست. PSK رو در source deployed تغییر بدید،
|
||||||
save کنید، سپس `psk` در `config.json` mhrv-rs رو update + restart.
|
redeploy کنید، سپس `psk` در `config.json` mhrv-rs رو update + restart.
|
||||||
|
|
||||||
اسکریپت val.town شامل **loop guard** هم هست (refuse میکنه fetch host
|
اسکریپت همچنین شامل **loop guard** هست (refuse میکنه fetch host خود)
|
||||||
خود) + **placeholder check** (در صورت `PSK === "CHANGE_ME_TO_A_STRONG_SECRET"`
|
+ **placeholder check** (در صورت `PSK === "CHANGE_ME_TO_A_STRONG_SECRET"`
|
||||||
return 503 میکنه) تا یک fresh deploy بدون setup نتونه بهطور
|
return 503 میکنه) تا یک fresh deploy بدون setup نتونه بهطور
|
||||||
accidentally بهعنوان open relay سرو بشه.
|
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 نیست
|
## چرا default-on نیست
|
||||||
|
|
||||||
- ۲۰۰-۵۰۰ms به هر request اضافه میکنه (hop اضافی)
|
- ۲۰۰-۵۰۰ms به هر request اضافه میکنه (hop اضافی)
|
||||||
- budget bandwidth free-tier val.town رو میسوزونه
|
- budget bandwidth free-tier host رو میسوزونه
|
||||||
- برای سایتهایی که CF anti-bot ندارن benefit نداره
|
- برای سایتهایی که CF anti-bot ندارن benefit نداره
|
||||||
- Setup یک account جداگانه روی پلتفرم third-party میخواد
|
- Setup یک account جداگانه روی پلتفرم third-party میخواد
|
||||||
|
|
||||||
@@ -140,35 +147,38 @@ Grok اهمیت میدن opt in؛ همهی دیگران lighter اجرا
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**`exit node refused or errored: unauthorized`** — PSK mismatch.
|
**`exit node refused or errored: unauthorized`** — PSK mismatch.
|
||||||
بررسی کنید `psk` در `config.json` دقیقاً با `PSK` constant در val.town
|
بررسی کنید `psk` در `config.json` دقیقاً با `PSK` constant در source
|
||||||
match هست. whitespace + quoting مهم است.
|
deployed match هست. whitespace + quoting مهم است.
|
||||||
|
|
||||||
**`exit node refused or errored: exit_node misconfigured: PSK is still
|
**`exit node refused or errored: exit_node misconfigured: PSK is still
|
||||||
the placeholder`** — فراموش کردید `CHANGE_ME_TO_A_STRONG_SECRET` رو
|
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
|
**`exit node failed for ...: connection refused`** — URL اشتباه است
|
||||||
اشتباه است یا val deploy نشده. با hit کردن URL مستقیم از browser
|
یا deployment live نیست. با hit کردن URL مستقیم از browser verify
|
||||||
verify کنید — باید `{"e":"method_not_allowed"}` برگردونه (val expects
|
کنید — باید `{"e":"method_not_allowed"}` برگردونه (handler expects
|
||||||
POST).
|
POST).
|
||||||
|
|
||||||
**`exit node failed for ...: timeout`** — outbound val.town slow است
|
**`exit node failed for ...: timeout`** — outbound host slow است
|
||||||
یا destination slow. region val.town متفاوت رو امتحان کنید، یا latency
|
یا destination slow. region متفاوت رو امتحان کنید، یا latency
|
||||||
trade-off رو accept کنید.
|
trade-off رو accept کنید.
|
||||||
|
|
||||||
**سایت همچنان CF challenge نشون میده بعد از enable exit node** — CF
|
**سایت همچنان CF challenge نشون میده بعد از enable exit node** — CF
|
||||||
IP val.town رو هم flag میکنه. برخی customers CF صراحتاً val.town رو
|
IP host شما رو هم flag کرده. بعضی hosting providerها outbound IP
|
||||||
blocklist کردن. workarounds: Deno Deploy رو امتحان کنید، یا سایت رو
|
spaceشون روی CF bot blocklist است. workarounds: host دیگه امتحان
|
||||||
به `passthrough_hosts` اضافه کنید (MITM رو bypass میکنه؛ از real
|
کنید (VPS شخصی شما clean IP میده)، یا سایت رو به `passthrough_hosts`
|
||||||
IP ISP شما استفاده میکنه).
|
اضافه کنید (MITM رو bypass میکنه؛ از real IP ISP شما استفاده
|
||||||
|
میکنه).
|
||||||
|
|
||||||
## همچنین ببینید
|
## همچنین ببینید
|
||||||
|
|
||||||
- [English version](README.md) of this doc
|
- [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.exit-node.example.json`](../../config.exit-node.example.json)
|
||||||
— config مثال کامل
|
— config مثال کامل
|
||||||
- Issue [#382](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/382)
|
- Issue [#382](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/382)
|
||||||
— thread tracking canonical Cloudflare anti-bot
|
— thread tracking canonical Cloudflare anti-bot
|
||||||
- Issue [#309](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/309)
|
- Issue [#309](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/309)
|
||||||
— roadmap CF WARP integration (approach جایگزین، longer-horizon)
|
— 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
|
Many Cloudflare-fronted services flag traffic from Google datacenter
|
||||||
IP ranges as bots and serve a Turnstile / interactive CAPTCHA / 502
|
IPs as bots and serve a Turnstile / CAPTCHA / 502 challenge instead of
|
||||||
challenge instead of the actual page. Apps Script's `UrlFetchApp.fetch()`
|
the real page. `UrlFetchApp.fetch()` in Apps Script always exits from
|
||||||
exits from those Google datacenter IPs, so for sites like:
|
Google's datacenter IP space, so for sites like:
|
||||||
|
|
||||||
- **chatgpt.com / openai.com** (Cloudflare anti-bot, often blocks GCP IPs)
|
- **chatgpt.com / openai.com**
|
||||||
- **claude.ai** (same)
|
- **claude.ai**
|
||||||
- **grok.com / x.com** (CF-fronted, returns 502 on Google IPs)
|
- **grok.com / x.com**
|
||||||
|
|
||||||
…the regular mhrv-rs apps_script-mode path returns errors like
|
…mhrv-rs's normal apps_script-mode path returns errors like `Relay
|
||||||
`Relay error: json: key must be a string at line 2 column 1` or
|
error: json: key must be a string at line 2 column 1` or `502 Relay
|
||||||
`502 Relay error` because Code.gs is wrapping a CF challenge HTML
|
error` because Code.gs is wrapping a CF challenge HTML page that the
|
||||||
page that the client can't make sense of.
|
client can't parse as relay JSON.
|
||||||
|
|
||||||
The **exit node** is a small TypeScript HTTP endpoint deployed on a
|
The **exit node** is a small TypeScript HTTP handler you deploy on a
|
||||||
serverless platform (val.town, Deno Deploy, fly.io, etc.) that sits
|
serverless TypeScript host you control. It sits between Apps Script
|
||||||
between Apps Script and the destination. The traffic chain becomes:
|
and the destination, so the request chain becomes:
|
||||||
|
|
||||||
```
|
```
|
||||||
Browser ─┐ ┌─→ Destination
|
Browser ─┐ ┌─→ Destination
|
||||||
│ │ (chatgpt.com)
|
│ │ (chatgpt.com)
|
||||||
▼ │
|
▼ │
|
||||||
mhrv-rs │
|
mhrv-rs │
|
||||||
│ │
|
│ │
|
||||||
│ TLS to Google IP, SNI=www.google.com (DPI cover) │
|
│ TLS to Google IP, SNI=www.google.com (DPI cover)│
|
||||||
▼ │
|
▼ │
|
||||||
Apps Script (Google datacenter) │
|
Apps Script (Google datacenter) │
|
||||||
│ │
|
│ │
|
||||||
│ UrlFetchApp.fetch(EXIT_NODE_URL) │
|
│ UrlFetchApp.fetch(EXIT_NODE_URL) │
|
||||||
▼ │
|
▼ │
|
||||||
val.town (non-Google IP) │
|
your exit node (non-Google IP) │
|
||||||
│ │
|
│ │
|
||||||
│ fetch(real_url) │
|
│ fetch(real_url) │
|
||||||
└──────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
The destination sees the val.town IP, not Google datacenter. CF's
|
The destination sees the exit node's outbound IP, not a Google
|
||||||
anti-bot heuristic doesn't fire, and you get the actual page.
|
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.**
|
**Important property preserved:** the user-side leg (Iran ISP →
|
||||||
The ISP still only sees TLS to a Google IP — the second hop happens
|
Apps Script) is unchanged. The ISP only sees TLS to a Google IP — the
|
||||||
entirely inside Apps Script's outbound, invisible from the user's
|
second hop happens entirely inside Apps Script's outbound, invisible
|
||||||
network. So the DPI evasion property mhrv-rs is built around stays
|
from the user's network. The DPI evasion property mhrv-rs is built
|
||||||
intact.
|
around stays intact.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. **Sign up at [val.town](https://val.town)** (free tier is fine —
|
The handler in [`exit_node.ts`](exit_node.ts) is plain TypeScript that
|
||||||
the free tier's outbound bandwidth is enough for personal use).
|
uses only web-standard APIs (`Request`, `Response`, `fetch`). It runs
|
||||||
2. **Create a new HTTP val** (TypeScript). On val.town: New → HTTP.
|
on any platform with a serverless-fetch runtime.
|
||||||
3. **Paste the contents of `valtown.ts`** from this directory.
|
|
||||||
4. **Set the PSK** at the top of the file:
|
### Generic steps (apply to every host)
|
||||||
|
|
||||||
|
1. **Open `exit_node.ts`** and replace the placeholder PSK at the top:
|
||||||
```ts
|
```ts
|
||||||
const PSK = "<your-strong-secret>";
|
const PSK = "<your-strong-secret>";
|
||||||
```
|
```
|
||||||
Generate a strong secret with `openssl rand -hex 32` from a terminal.
|
Generate a strong secret with `openssl rand -hex 32`. **Do not leave
|
||||||
**Don't leave the placeholder in production** — the val.town code
|
the placeholder** — the script is deliberately fail-closed (returns
|
||||||
intentionally fails closed (returns 503 on every request) until
|
503 on every request until the placeholder is replaced) so a fresh
|
||||||
you replace the placeholder, so you can't accidentally serve as
|
deploy without configuration can't accidentally serve as an open
|
||||||
an open relay.
|
relay.
|
||||||
5. **Save** the val. Copy the val's public URL — it looks like
|
2. **Deploy** to your chosen host (see options below).
|
||||||
`https://your-handle-mhrv.web.val.run`.
|
3. **Copy the public URL** of the deployed handler.
|
||||||
6. **In your mhrv-rs `config.json`**, add an `exit_node` block:
|
4. **In `mhrv-rs` config.json**, add an `exit_node` block:
|
||||||
```json
|
```json
|
||||||
"exit_node": {
|
"exit_node": {
|
||||||
"enabled": true,
|
"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 in step 4>",
|
"psk": "<the same PSK you set in step 1>",
|
||||||
"mode": "selective",
|
"mode": "selective",
|
||||||
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
|
"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).
|
binary).
|
||||||
8. **Test** — visit `chatgpt.com` or `grok.com` from a browser pointed
|
6. **Test** — open `chatgpt.com` or `grok.com` from a browser pointed
|
||||||
at the mhrv-rs proxy. You should see the actual login page now,
|
at mhrv-rs's proxy. You should see the real login page, not a CF
|
||||||
not a CF challenge.
|
challenge.
|
||||||
|
|
||||||
A complete example config is at
|
A complete example config is at
|
||||||
[`config.exit-node.example.json`](../../config.exit-node.example.json)
|
[`config.exit-node.example.json`](../../config.exit-node.example.json)
|
||||||
in the repo root.
|
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 |
|
| 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. |
|
| `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 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. |
|
| `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
|
If the exit node is unreachable, returns 5xx, or returns a malformed
|
||||||
response, mhrv-rs **falls back to the regular Apps Script relay
|
response, mhrv-rs **automatically falls back to the regular Apps
|
||||||
automatically**. You'll see a `warn: exit node failed for ... — falling
|
Script relay**. The log shows a `warn: exit node failed for ... —
|
||||||
back to direct Apps Script` line in the log. Sites that need the exit
|
falling back to direct Apps Script` line. The CF-affected sites then
|
||||||
node will fail in that case (CF challenge), but other sites work
|
fail (CF challenge), but every other site keeps working — a downed
|
||||||
normally — a down exit node doesn't take you fully offline.
|
exit node doesn't take you fully offline.
|
||||||
|
|
||||||
## Security model
|
## 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:
|
public open proxy. Treat it like a password:
|
||||||
|
|
||||||
- **Don't commit** the PSK to source control. The val.town source
|
- **Don't commit** the PSK to source control. Most TypeScript hosts
|
||||||
is private to your account by default; keep it that way.
|
default deployed code to private; keep it that way.
|
||||||
- **Don't share** the PSK publicly. Anyone who has both the URL and
|
- **Don't share publicly.** Anyone with both the URL and the PSK can
|
||||||
the PSK can use your val.town quota as their own proxy.
|
use the deployment as their own proxy and burn your runtime quota.
|
||||||
- **Rotate** if you suspect leak. Change the PSK in val.town source,
|
- **Rotate** if you suspect a leak. Change the PSK in the deployed
|
||||||
save, then update `psk` in mhrv-rs `config.json` and restart.
|
source, redeploy, then update `psk` in `mhrv-rs` config.json and
|
||||||
|
restart.
|
||||||
|
|
||||||
The val.town script also includes a **loop guard** (refuses to fetch
|
The script also includes a **loop guard** (refuses to fetch its own
|
||||||
its own host) and **placeholder check** (returns 503 if `PSK ===
|
host) and a **placeholder check** (returns 503 if `PSK ===
|
||||||
"CHANGE_ME_TO_A_STRONG_SECRET"`) so a fresh deploy without setup can't
|
"CHANGE_ME_TO_A_STRONG_SECRET"`) so a fresh deploy without
|
||||||
accidentally serve as an open relay.
|
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
|
- Adds ~200-500ms per request through the exit-node hop
|
||||||
(`Request`, `Response`, `fetch`). It runs on:
|
- 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
|
So `enabled: false` is the default. Users who specifically need
|
||||||
- **Deno Deploy** — similar API; deploy with `deployctl`
|
ChatGPT / Claude / Grok opt in; everyone else runs lighter.
|
||||||
- **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.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**`exit node refused or errored: unauthorized`** — PSK mismatch.
|
**`exit node refused or errored: unauthorized`** — PSK mismatch.
|
||||||
Check that the `psk` in `config.json` exactly matches the `PSK`
|
Double-check `psk` in `config.json` matches the `PSK` constant in your
|
||||||
constant in val.town. Whitespace and quoting matter.
|
deployed source character-for-character. Whitespace and quoting
|
||||||
|
matter.
|
||||||
|
|
||||||
**`exit node refused or errored: exit_node misconfigured: PSK is still
|
**`exit_node misconfigured: PSK is still the placeholder`** — you
|
||||||
the placeholder`** — you forgot to replace `CHANGE_ME_TO_A_STRONG_SECRET`
|
forgot to replace `CHANGE_ME_TO_A_STRONG_SECRET` in the source. Edit
|
||||||
in val.town. Edit + save the val.
|
the deployed file, save, redeploy.
|
||||||
|
|
||||||
**`exit node failed for ...: connection refused`** — the val.town URL
|
**`exit node failed for ...: connection refused`** — the URL is wrong
|
||||||
is wrong or the val isn't deployed. Verify by hitting the URL directly
|
or the deployment isn't live. Verify by hitting the URL in a browser
|
||||||
from a browser — it should return `{"e":"method_not_allowed"}` (val
|
— it should respond with `{"e":"method_not_allowed"}` (the script
|
||||||
expects POST).
|
expects POST).
|
||||||
|
|
||||||
**`exit node failed for ...: timeout`** — val.town outbound is slow
|
**`exit node failed for ...: timeout`** — the host's outbound or the
|
||||||
or the destination is slow. Try a different val.town deployment region,
|
destination is slow. Try a different region, or accept the latency
|
||||||
or accept the latency trade-off.
|
trade-off.
|
||||||
|
|
||||||
**Site still shows CF challenge after enabling exit node** — CF is
|
**Site still shows a CF challenge after enabling the exit node** —
|
||||||
flagging val.town's IP too. Some CF customers explicitly blocklist
|
CF has flagged your host's IP too. Some hosting providers' outbound
|
||||||
val.town. Workarounds: try Deno Deploy instead, or add the site to
|
IP space is on CF's bot blocklist. Workarounds: try a different host
|
||||||
`passthrough_hosts` (bypasses MITM entirely; uses your real ISP IP).
|
(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
|
## See also
|
||||||
|
|
||||||
- [Persian translation](README.fa.md) of this doc
|
- [Persian (راهنمای فارسی)](README.fa.md) version of this doc
|
||||||
- [`valtown.ts`](valtown.ts) — the val.town source (with hardening)
|
- [`exit_node.ts`](exit_node.ts) — the handler source (with hardening)
|
||||||
- [`config.exit-node.example.json`](../../config.exit-node.example.json)
|
- [`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)
|
- 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)
|
- 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
|
// mhrv-rs exit node — deploy as an HTTP endpoint on any serverless
|
||||||
// Deploy, fly.io, anywhere with a public residential-adjacent IP).
|
// 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.
|
// Purpose: chain client → Apps Script → this exit node → destination.
|
||||||
// Apps Script's UrlFetchApp can't reach Cloudflare-protected sites that
|
// 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
|
// 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
|
// and the destination; the destination sees the exit node's outbound IP
|
||||||
// outbound, generally not flagged as Google datacenter) and accepts the
|
// (generally not flagged as Google datacenter) and accepts the request.
|
||||||
// request.
|
|
||||||
//
|
//
|
||||||
// Setup:
|
// Setup:
|
||||||
// 1. Sign in to https://val.town and create a new HTTP val (TypeScript)
|
// 1. Pick a host that runs web-standard fetch handlers (e.g. Deno
|
||||||
// 2. Paste the contents of this file
|
// Deploy, fly.io with a thin server wrapper, or any cheap VPS
|
||||||
// 3. Set PSK below to a strong secret (use `openssl rand -hex 32`
|
// running Deno / Node + this script as a handler).
|
||||||
// from a terminal — DO NOT leave the placeholder in production)
|
// 2. Paste the contents of this file as the request handler.
|
||||||
// 4. Save and copy the val's public URL (looks like
|
// 3. Set PSK below to a strong secret (`openssl rand -hex 32` from
|
||||||
// https://your-handle-mhrv.web.val.run)
|
// 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:
|
// 5. In mhrv-rs config.json, add:
|
||||||
// "exit_node": {
|
// "exit_node": {
|
||||||
// "enabled": true,
|
// "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>",
|
// "psk": "<the same PSK you set above>",
|
||||||
// "mode": "selective",
|
// "mode": "selective",
|
||||||
// "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com"]
|
// "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com"]
|
||||||
@@ -87,7 +89,7 @@ export default async function (req: Request): Promise<Response> {
|
|||||||
{
|
{
|
||||||
e:
|
e:
|
||||||
"exit_node misconfigured: PSK is still the placeholder. Set " +
|
"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 },
|
{ 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.
|
// Loop guard: if u points at this exit node's own host, refuse.
|
||||||
// Without this, a misconfigured client could chain exit-node →
|
// 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 {
|
try {
|
||||||
const reqUrl = new URL(req.url);
|
const reqUrl = new URL(req.url);
|
||||||
const dstUrl = new URL(u);
|
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",
|
"mode": "apps_script",
|
||||||
"google_ip": "216.239.38.120",
|
"google_ip": "216.239.38.120",
|
||||||
"front_domain": "www.google.com",
|
"front_domain": "www.google.com",
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
"exit_node": {
|
"exit_node": {
|
||||||
"_comment": "Master switch. Set false to disable exit-node entirely without removing the config. Default false.",
|
"_comment": "Master switch. Set false to disable exit-node entirely without removing the config. Default false.",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"_comment_relay_url": "Public URL of your val.town deployment (or Deno Deploy, fly.io, etc. running assets/exit_node/valtown.ts).",
|
"_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-handle-mhrv.web.val.run",
|
"relay_url": "https://your-deployed-exit-node.example.com",
|
||||||
"_comment_psk": "Pre-shared key — must match the PSK constant in your val.town source. Generate with: openssl rand -hex 32",
|
"_comment_psk": "Pre-shared key — must match the PSK constant in your deployed source. Generate with: openssl rand -hex 32",
|
||||||
"psk": "PUT_YOUR_VAL_TOWN_PSK_HERE",
|
"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).",
|
"_comment_mode": "selective: only `hosts` route via exit node (recommended). full: every request routes via exit node (slower, ~250-500ms extra hop).",
|
||||||
"mode": "selective",
|
"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": [
|
"hosts": [
|
||||||
"chatgpt.com",
|
"chatgpt.com",
|
||||||
"claude.ai",
|
"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. -->
|
<!-- 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
|
```json
|
||||||
"exit_node": {
|
"exit_node": {
|
||||||
"enabled": true,
|
"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>",
|
"psk": "<openssl rand -hex 32>",
|
||||||
"mode": "selective",
|
"mode": "selective",
|
||||||
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
|
"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).
|
دو 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) تنها مسیر باقی مونده.
|
• حذف 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
|
```json
|
||||||
"exit_node": {
|
"exit_node": {
|
||||||
"enabled": true,
|
"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>",
|
"psk": "<openssl rand -hex 32>",
|
||||||
"mode": "selective",
|
"mode": "selective",
|
||||||
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
|
"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.
|
• 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. -->
|
<!-- 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` این رد رو حفظ میکنه.
|
- **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.
|
- **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.
|
۲ 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 بیشتر دامنه برداشت کنن، اونی که در شبکهشان کار میکنه نگه دارن.
|
• 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.
|
• تست: ۱۷۹ 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.
|
- **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`.
|
- **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.
|
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.
|
• 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.
|
• Tests: 179 lib + 35 tunnel-node tests all passing.
|
||||||
|
|||||||
+3
-3
@@ -257,13 +257,13 @@ HTTP / HTTPS مثل قبل از Apps Script میرود (تغییری نمی
|
|||||||
|
|
||||||
## Exit node
|
## 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). ۵ دقیقه، سهمیهٔ رایگان.
|
**راهاندازی:** [`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
|
## 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.
|
**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,
|
request_timeout_secs: u64,
|
||||||
/// Optional second-hop exit node for CF-anti-bot bypass (chatgpt.com /
|
/// Optional second-hop exit node for CF-anti-bot bypass (chatgpt.com /
|
||||||
/// claude.ai / grok.com / x.com). Config-only — no UI editor yet.
|
/// 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,
|
exit_node: mhrv_rs::config::ExitNodeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +676,7 @@ struct ConfigWire<'a> {
|
|||||||
#[serde(skip_serializing_if = "is_default_timeout_secs")]
|
#[serde(skip_serializing_if = "is_default_timeout_secs")]
|
||||||
request_timeout_secs: u64,
|
request_timeout_secs: u64,
|
||||||
/// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai /
|
/// 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
|
/// default (disabled with no URL/PSK/hosts) so configs without
|
||||||
/// exit-node setup stay clean. Round-tripped through FormState so
|
/// exit-node setup stay clean. Round-tripped through FormState so
|
||||||
/// Save preserves user-edited values.
|
/// Save preserves user-edited values.
|
||||||
|
|||||||
+4
-4
@@ -341,7 +341,7 @@ pub struct Config {
|
|||||||
///
|
///
|
||||||
/// Architecture: chain becomes
|
/// Architecture: chain becomes
|
||||||
/// `client → SNI rewrite → Apps Script (Google IP) → exit node
|
/// `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.
|
/// The destination sees the exit node's outbound IP, not Google's.
|
||||||
/// CF anti-bot's "this is a Google datacenter" heuristic doesn't
|
/// CF anti-bot's "this is a Google datacenter" heuristic doesn't
|
||||||
@@ -362,9 +362,9 @@ pub struct ExitNodeConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
||||||
/// HTTPS URL of the exit-node endpoint. Typically a val.town /
|
/// HTTPS URL of the exit-node endpoint. Typically a Deno Deploy /
|
||||||
/// Deno Deploy / fly.io serverless deployment running the
|
/// fly.io serverless deployment (or your own VPS) running the
|
||||||
/// `assets/exit_node/valtown.ts` script (or an equivalent). The
|
/// `assets/exit_node/exit_node.ts` script (or an equivalent). The
|
||||||
/// exit node is what makes the outbound `fetch()` call to the
|
/// exit node is what makes the outbound `fetch()` call to the
|
||||||
/// destination, so its IP is what the destination sees.
|
/// destination, so its IP is what the destination sees.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
+19
-19
@@ -151,7 +151,7 @@ pub struct DomainFronter {
|
|||||||
/// (#430, masterking32 PR #25). Read by `tunnel_client::fire_batch`
|
/// (#430, masterking32 PR #25). Read by `tunnel_client::fire_batch`
|
||||||
/// so a single config field tunes the timeout used everywhere.
|
/// so a single config field tunes the timeout used everywhere.
|
||||||
batch_timeout: Duration,
|
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
|
/// to bypass CF-anti-bot blocks on sites that flag Google datacenter
|
||||||
/// IPs (chatgpt.com, claude.ai, grok.com, x.com). Mirrors
|
/// IPs (chatgpt.com, claude.ai, grok.com, x.com). Mirrors
|
||||||
/// `Config::exit_node`. When `exit_node_enabled` is false (the more
|
/// `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
|
// 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
|
// non-Google exit IP. The cache + coalesce layer below is bypassed
|
||||||
// for these — exit-node-eligible hosts are the ones with active
|
// for these — exit-node-eligible hosts are the ones with active
|
||||||
// anti-bot challenges (CF Turnstile, ChatGPT login, Claude.ai,
|
// anti-bot challenges (CF Turnstile, ChatGPT login, Claude.ai,
|
||||||
// grok.com), and serving cached responses across users for those
|
// grok.com), and serving cached responses across users for those
|
||||||
// would be wrong (auth tokens, session state, per-user
|
// would be wrong (auth tokens, session state, per-user
|
||||||
// personalization). Falls back to the regular Apps Script relay
|
// 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
|
// so a misconfigured or down exit node doesn't take the user
|
||||||
// offline for the sites that DON'T need it.
|
// offline for the sites that DON'T need it.
|
||||||
if self.exit_node_matches(url) {
|
if self.exit_node_matches(url) {
|
||||||
@@ -1285,7 +1285,7 @@ impl DomainFronter {
|
|||||||
/// ```text
|
/// ```text
|
||||||
/// client → SNI rewrite → Apps Script (Google IP)
|
/// client → SNI rewrite → Apps Script (Google IP)
|
||||||
/// → UrlFetchApp.fetch(exit_node_url)
|
/// → UrlFetchApp.fetch(exit_node_url)
|
||||||
/// → exit node (val.town, non-Google IP)
|
/// → exit node (non-Google IP)
|
||||||
/// → fetch(real_url)
|
/// → fetch(real_url)
|
||||||
/// → response back through both layers
|
/// → response back through both layers
|
||||||
/// ```
|
/// ```
|
||||||
@@ -1296,7 +1296,7 @@ impl DomainFronter {
|
|||||||
/// destination, returns a `{s, h, b}` JSON envelope. Apps Script
|
/// destination, returns a `{s, h, b}` JSON envelope. Apps Script
|
||||||
/// returns that envelope as the body of its raw HTTP response
|
/// returns that envelope as the body of its raw HTTP response
|
||||||
/// (because we set `r: true`). We then unwrap one extra layer:
|
/// (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
|
/// the destination's raw HTTP response so the rest of the proxy
|
||||||
/// pipeline (MITM TLS write-back) sees the same shape it gets from
|
/// pipeline (MITM TLS write-back) sees the same shape it gets from
|
||||||
/// the regular path.
|
/// the regular path.
|
||||||
@@ -1314,7 +1314,7 @@ impl DomainFronter {
|
|||||||
// Reusing build_payload_json keeps the outer envelope consistent
|
// Reusing build_payload_json keeps the outer envelope consistent
|
||||||
// with everything else (including the random padding for DPI
|
// with everything else (including the random padding for DPI
|
||||||
// evasion). The `r: true` flag in RelayRequest makes Code.gs
|
// 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.
|
// unwrap below.
|
||||||
let exit_url = self.exit_node_url.clone();
|
let exit_url = self.exit_node_url.clone();
|
||||||
let outer_headers = vec![(
|
let outer_headers = vec![(
|
||||||
@@ -1325,12 +1325,12 @@ impl DomainFronter {
|
|||||||
self.build_payload_json("POST", &exit_url, &outer_headers, &inner_json)?;
|
self.build_payload_json("POST", &exit_url, &outer_headers, &inner_json)?;
|
||||||
|
|
||||||
// Send the outer payload through the relay machinery and get back
|
// 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
|
let app_body = self
|
||||||
.send_prebuilt_payload_through_relay(outer_payload)
|
.send_prebuilt_payload_through_relay(outer_payload)
|
||||||
.await?;
|
.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.
|
// success, {e: "..."} on its own internal error.
|
||||||
parse_exit_node_response(&app_body)
|
parse_exit_node_response(&app_body)
|
||||||
}
|
}
|
||||||
@@ -1376,7 +1376,7 @@ impl DomainFronter {
|
|||||||
h: hmap,
|
h: hmap,
|
||||||
b: b_encoded,
|
b: b_encoded,
|
||||||
ct,
|
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)?)
|
Ok(serde_json::to_vec(&req)?)
|
||||||
}
|
}
|
||||||
@@ -1385,7 +1385,7 @@ impl DomainFronter {
|
|||||||
/// a payload we already built. Mirrors `do_relay_once_with` but
|
/// a payload we already built. Mirrors `do_relay_once_with` but
|
||||||
/// returns the **raw response body bytes** (Apps Script's HTTP body)
|
/// returns the **raw response body bytes** (Apps Script's HTTP body)
|
||||||
/// instead of running the body through `parse_relay_json` — the
|
/// 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.
|
/// has a different shape from Code.gs's raw-HTTP wrapping.
|
||||||
async fn send_prebuilt_payload_through_relay(
|
async fn send_prebuilt_payload_through_relay(
|
||||||
&self,
|
&self,
|
||||||
@@ -2133,12 +2133,12 @@ fn unix_to_ymd_utc(secs: u64) -> (i64, u32, u32) {
|
|||||||
(y, m as u32, d as 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:
|
/// response. The envelope shape is:
|
||||||
///
|
///
|
||||||
/// - On success: `{ "s": <status u16>, "h": { ... }, "b": "<base64>" }`
|
/// - On success: `{ "s": <status u16>, "h": { ... }, "b": "<base64>" }`
|
||||||
/// - On exit-node-side error: `{ "e": "<message>" }` with HTTP 4xx/5xx
|
/// - 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).
|
/// layer, not the inner field).
|
||||||
///
|
///
|
||||||
/// We synthesize a complete HTTP/1.1 response from these fields so the
|
/// 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
|
// Surface exit-node's internal errors clearly rather than as a 502
|
||||||
// from the outer envelope. The `{e: "..."}` shape is what the val.town
|
// from the outer envelope. The `{e: "..."}` shape is what the exit-node's
|
||||||
// script emits on bad PSK, malformed URL, or any caught exception.
|
// 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()) {
|
if let Some(err_msg) = v.get("e").and_then(|x| x.as_str()) {
|
||||||
return Err(FronterError::Relay(format!(
|
return Err(FronterError::Relay(format!(
|
||||||
@@ -2484,7 +2484,7 @@ where
|
|||||||
let want = need.min(tmp.len());
|
let want = need.min(tmp.len());
|
||||||
// Handle ungraceful TLS close-without-close_notify (rustls
|
// Handle ungraceful TLS close-without-close_notify (rustls
|
||||||
// surfaces this as `io::ErrorKind::UnexpectedEof`). Some
|
// 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:
|
// Script (#585, v1.9.4) and certain Apps Script `Connection:
|
||||||
// close` responses — terminate the underlying TCP without
|
// close` responses — terminate the underlying TCP without
|
||||||
// sending the TLS close_notify alert first. Treat that the
|
// sending the TLS close_notify alert first. Treat that the
|
||||||
@@ -3085,7 +3085,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn read_http_response_tolerates_unexpected_eof_with_content_length() {
|
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
|
// particular, certain Apps Script `Connection: close` paths) close
|
||||||
// the TCP without TLS close_notify. Body should still be returned
|
// the TCP without TLS close_notify. Body should still be returned
|
||||||
// when Content-Length is satisfied, even though the read after
|
// when Content-Length is satisfied, even though the read after
|
||||||
@@ -3130,8 +3130,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn parse_exit_node_response_unwraps_valtown_envelope() {
|
async fn parse_exit_node_response_unwraps_exit_node_envelope() {
|
||||||
// The exit-node path through Apps Script returns val.town's JSON
|
// The exit-node path through Apps Script returns exit node's JSON
|
||||||
// envelope as the response body. parse_exit_node_response must
|
// envelope as the response body. parse_exit_node_response must
|
||||||
// unwrap it back into a raw HTTP/1.1 response so the MITM TLS
|
// 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
|
// write-back path sees the same shape it gets from the regular
|
||||||
@@ -3150,7 +3150,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn parse_exit_node_response_surfaces_explicit_error() {
|
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
|
// surface that error message specifically rather than letting
|
||||||
// it through as an unparseable 502 — the message string is what
|
// it through as an unparseable 502 — the message string is what
|
||||||
// tells the user what went wrong (placeholder PSK, bad URL,
|
// tells the user what went wrong (placeholder PSK, bad URL,
|
||||||
|
|||||||
Reference in New Issue
Block a user