Many Cloudflare-fronted services flag traffic from Google datacenter
IPs as bots and serve a Turnstile / CAPTCHA / 502 challenge instead of
the real page. UrlFetchApp.fetch() in Apps Script always exits from
Google's datacenter IP space, so for sites like:
- chatgpt.com / openai.com
- claude.ai
- grok.com / x.com
…mhrv-rs's normal apps_script-mode path returns errors like Relay error: json: key must be a string at line 2 column 1 or 502 Relay error because Code.gs is wrapping a CF challenge HTML page that the
client can't parse as relay JSON.
The exit node is a small TypeScript HTTP handler you deploy on a serverless TypeScript host you control. It sits between Apps Script and the destination, so the request chain becomes:
Browser ─┐ ┌─→ Destination
│ │ (chatgpt.com)
▼ │
mhrv-rs │
│ │
│ TLS to Google IP, SNI=www.google.com (DPI cover)│
▼ │
Apps Script (Google datacenter) │
│ │
│ UrlFetchApp.fetch(EXIT_NODE_URL) │
▼ │
your exit node (non-Google IP) │
│ │
│ fetch(real_url) │
└──────────────────────────────────────────────────┘
The destination sees the exit node's outbound IP, not a Google datacenter IP. CF's anti-bot heuristic doesn't fire and the real page comes back.
Important property preserved: the user-side leg (Iran ISP → Apps Script) is unchanged. The ISP only sees TLS to a Google IP — the second hop happens entirely inside Apps Script's outbound, invisible from the user's network. The DPI evasion property mhrv-rs is built around stays intact.
The handler in exit_node.ts is plain TypeScript that
uses only web-standard APIs (Request, Response, fetch). It runs
on any platform with a serverless-fetch runtime.
- Open
exit_node.tsand replace the placeholder PSK at the top:Generate a strong secret withconst PSK = "<your-strong-secret>";
openssl rand -hex 32. Do not leave the placeholder — the script is deliberately fail-closed (returns 503 on every request until the placeholder is replaced) so a fresh deploy without configuration can't accidentally serve as an open relay. - Deploy to your chosen host (see options below).
- Copy the public URL of the deployed handler.
- In
mhrv-rsconfig.json, add anexit_nodeblock:"exit_node": { "enabled": true, "relay_url": "https://your-deployed-exit-node.example.com", "psk": "<the same PSK you set in step 1>", "mode": "selective", "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"] }
- Restart mhrv-rs (Disconnect + Connect, or kill + restart the binary).
- Test — open
chatgpt.comorgrok.comfrom a browser pointed at mhrv-rs's proxy. You should see the real login page, not a CF challenge.
A complete example config is at
config.exit-node.example.json
in the repo root.
The script is one self-contained file. Pick whichever host you can sign up for and trust:
| Host | Notes |
|---|---|
| Deno Deploy (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.
| Mode | What it does | When to use |
|---|---|---|
selective (default) |
Only hosts in hosts route via the exit node; everything else takes the normal Apps Script path |
Recommended. The exit-node hop adds ~200-500ms per request, so reserve it for sites that actually need a non-Google IP. |
full |
Every request routes via the exit node | Only when your entire workload is CF-anti-bot affected, or when your exit node is faster than Apps Script on your network path (rare). Burns the exit node's runtime budget on sites that don't need it. |
If the exit node is unreachable, returns 5xx, or returns a malformed
response, mhrv-rs automatically falls back to the regular Apps
Script relay. The log shows a warn: exit node failed for ... — falling back to direct Apps Script line. The CF-affected sites then
fail (CF challenge), but every other site keeps working — a downed
exit node doesn't take you fully offline.
The PSK is the only thing keeping the deployed endpoint from being a public open proxy. Treat it like a password:
- Don't commit the PSK to source control. Most TypeScript hosts default deployed code to private; keep it that way.
- Don't share publicly. Anyone with both the URL and the PSK can use the deployment as their own proxy and burn your runtime quota.
- Rotate if you suspect a leak. Change the PSK in the deployed
source, redeploy, then update
pskinmhrv-rsconfig.json and restart.
The script also includes a loop guard (refuses to fetch its own
host) and a placeholder check (returns 503 if PSK === "CHANGE_ME_TO_A_STRONG_SECRET") so a fresh deploy without
configuration can't be accidentally served as an open relay.
- Adds ~200-500ms per request through the exit-node hop
- Burns the host's free-tier runtime quota
- No benefit for sites that don't have CF anti-bot
- Requires signing up for a separate third-party platform
So enabled: false is the default. Users who specifically need
ChatGPT / Claude / Grok opt in; everyone else runs lighter.
exit node refused or errored: unauthorized — PSK mismatch.
Double-check psk in config.json matches the PSK constant in your
deployed source character-for-character. Whitespace and quoting
matter.
exit_node misconfigured: PSK is still the placeholder — you
forgot to replace CHANGE_ME_TO_A_STRONG_SECRET in the source. Edit
the deployed file, save, redeploy.
exit node failed for ...: connection refused — the URL is wrong
or the deployment isn't live. Verify by hitting the URL in a browser
— it should respond with {"e":"method_not_allowed"} (the script
expects POST).
exit node failed for ...: timeout — the host's outbound or the
destination is slow. Try a different region, or accept the latency
trade-off.
Site still shows a CF challenge after enabling the exit node —
CF has flagged your host's IP too. Some hosting providers' outbound
IP space is on CF's bot blocklist. Workarounds: try a different host
(your own VPS gives you a clean IP), or add the affected site to
passthrough_hosts to bypass the MITM and use your real ISP IP.
- Persian (راهنمای فارسی) version of this doc
exit_node.ts— the handler source (with hardening)config.exit-node.example.json— complete example mhrv-rs config- Issue #382 — canonical thread tracking Cloudflare anti-bot
- Issue #309 — roadmap for CF WARP integration (alternative approach, longer-horizon)