Files
MasterHttpRelayVPN-RUST/assets/exit_node/README.md
T
therealaleph 4aac9a793f feat: v1.9.4 — exit node for ChatGPT/Claude/Grok + drop duplicate Telegram post
Two changes addressing user-reported issues today:

1. Exit-node feature ported from upstream masterking32@464a6e1d, with
   hardening. Cloudflare-protected sites (chatgpt.com, claude.ai,
   grok.com, x.com, openai.com) flag Google datacenter IPs as bots and
   return Turnstile / CAPTCHA / 502 challenges. Apps Script's UrlFetchApp
   exits from those IPs, so v1.9.3 surfaces these as "Relay error: json:
   key must be a string..." with no apps_script-mode workaround.

   Now a small TypeScript HTTP endpoint (assets/exit_node/valtown.ts)
   deployed on val.town / Deno Deploy sits between Apps Script and the
   destination. Chain: client → Apps Script (Google IP) → val.town
   (non-Google IP) → destination. Destination sees val.town's IP, no
   CF challenge.

   Config:
     "exit_node": {
       "enabled": true,
       "relay_url": "https://...web.val.run",
       "psk": "<openssl rand -hex 32>",
       "mode": "selective",
       "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
     }

   Hardening over upstream: PSK fail-closed if still placeholder (fresh
   deploy can't be open relay), loop guard (refuses fetch of own host),
   explicit 503 on misconfigured. Fallback to direct Apps Script on exit
   node failure (CF-affected sites fail, others keep working). Setup
   docs in English + Persian at assets/exit_node/README*.md. Example
   config at config.exit-node.example.json.

2. Removed the legacy `telegram` job from release.yml. With
   TELEGRAM_NOTIFY_ENABLED repo var set to true, every release was
   producing two duplicate APK posts on the main Telegram channel: the
   old bundled-APK-on-main job AND the newer per-file files-channel
   posts (telegram-publish-files.yml). Only the per-file flow is wanted.
   Legacy job and its helper telegram_release_notify.py are gone.
   Recoverable from git log if anyone needs the bundled pattern back.

169 mhrv-rs lib tests + 33 tunnel-node tests + UI build clean.
2026-05-01 11:52:32 +03:00

177 lines
8.2 KiB
Markdown

# Exit node — bypass Cloudflare 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:
- **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)
…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.
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:
```
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) │
└──────────────────────────────────────────────────────┘
```
The destination sees the val.town IP, not Google datacenter. CF's
anti-bot heuristic doesn't fire, and you get the actual page.
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.
## 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:
```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:
```json
"exit_node": {
"enabled": true,
"relay_url": "https://your-handle-mhrv.web.val.run",
"psk": "<the same PSK you set in step 4>",
"mode": "selective",
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
}
```
7. **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.
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
| 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. |
## Failure mode
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.
## Security model
The PSK is the only thing keeping the val.town 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.
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.
## Alternative platforms
The `valtown.ts` script is plain TypeScript using web-standard APIs
(`Request`, `Response`, `fetch`). It runs on:
- **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.
## 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.
**`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 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
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.
**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).
## See also
- [Persian translation](README.fa.md) of this doc
- [`valtown.ts`](valtown.ts) — the val.town source (with hardening)
- [`config.exit-node.example.json`](../../config.exit-node.example.json)
— complete example config
- Issue [#382](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/382)
— canonical Cloudflare anti-bot tracking thread
- Issue [#309](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/309)
— CF WARP integration roadmap (alternative approach, longer-horizon)