mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
feat: add exit node deployment guide and templates for Val Town, Cloudflare Workers, and Deno Deploy
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# Exit Node Deployment Guide (Val Town / Cloudflare / Deno)
|
||||
|
||||
This guide explains how to deploy an exit node for MasterHttpRelayVPN on free platforms.
|
||||
|
||||
Traffic path:
|
||||
|
||||
Browser -> Local Proxy -> Apps Script -> Exit Node -> Target Website
|
||||
|
||||
Use this when destinations block Google datacenter egress.
|
||||
|
||||
## 1) Choose One Provider
|
||||
|
||||
- Val Town
|
||||
- Cloudflare Workers
|
||||
- Deno Deploy
|
||||
|
||||
You only need one provider.
|
||||
|
||||
## 2) Set PSK In Code
|
||||
|
||||
Each template includes:
|
||||
|
||||
const PSK = "CHANGE_ME_TO_A_STRONG_SECRET";
|
||||
|
||||
Replace that value with a long random secret.
|
||||
|
||||
Important:
|
||||
- Use the same PSK in your local config under exit_node.psk.
|
||||
- Never share your deployed URL together with a valid PSK.
|
||||
|
||||
## 3) Deploy On Val Town
|
||||
|
||||
Source file: apps_script/valtown.ts
|
||||
|
||||
Steps:
|
||||
1. Sign in at https://www.val.town
|
||||
2. Create a new Val (TypeScript HTTP endpoint).
|
||||
3. Paste content from apps_script/valtown.ts.
|
||||
4. Set the PSK constant in the code.
|
||||
5. Save and deploy.
|
||||
6. Copy your public URL, usually like https://YOUR-NAME.web.val.run
|
||||
|
||||
## 4) Deploy On Cloudflare Workers
|
||||
|
||||
Source file: apps_script/cloudflare_worker.js
|
||||
|
||||
Steps:
|
||||
1. Sign in at https://dash.cloudflare.com
|
||||
2. Go to Compute -> Workers & Pages.
|
||||
3. Create Application -> Start with Hello World -> Deploy -> Edit Code.
|
||||
4. Replace code with apps_script/cloudflare_worker.js content.
|
||||
5. Set PSK constant in code.
|
||||
6. Deploy.
|
||||
7. Copy URL, usually like https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev
|
||||
|
||||
## 5) Deploy On Deno Deploy
|
||||
|
||||
Source file: apps_script/deno_deploy.ts
|
||||
|
||||
Steps:
|
||||
1. Sign in at https://dash.deno.com
|
||||
2. Create new project.
|
||||
3. Upload or paste apps_script/deno_deploy.ts.
|
||||
4. Set PSK constant in code.
|
||||
5. Deploy.
|
||||
6. Copy URL, usually like https://YOUR-PROJECT.deno.dev
|
||||
|
||||
## 6) Configure MasterHttpRelayVPN
|
||||
|
||||
Update config.json:
|
||||
|
||||
{
|
||||
"exit_node": {
|
||||
"enabled": true,
|
||||
"provider": "valtown",
|
||||
"url": "https://YOUR-NAME.web.val.run",
|
||||
"psk": "CHANGE_ME_TO_A_STRONG_SECRET",
|
||||
"mode": "full",
|
||||
"hosts": [
|
||||
"chatgpt.com",
|
||||
"openai.com",
|
||||
"claude.ai",
|
||||
"anthropic.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Provider values:
|
||||
- valtown
|
||||
- cloudflare
|
||||
- deno
|
||||
|
||||
If mode is selective, only hosts listed in hosts use the exit node.
|
||||
If mode is full, all relayed traffic uses the exit node.
|
||||
|
||||
## 7) Quick Test
|
||||
|
||||
1. Start app: python main.py
|
||||
2. Ensure proxy is set in browser.
|
||||
3. Open a site known to require non-Google egress.
|
||||
4. If it fails, check:
|
||||
- provider and url are correct
|
||||
- psk matches exactly between config and deployed code
|
||||
- exit_node.enabled is true
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- unauthorized: PSK mismatch
|
||||
- method_not_allowed: endpoint got non-POST request directly (normal when opened in browser)
|
||||
- bad_url: malformed target URL from relay payload
|
||||
- timeout or 5xx: temporary provider issue, redeploy and retry
|
||||
@@ -178,21 +178,27 @@ Some websites block Google datacenter IPs when traffic exits directly from Apps
|
||||
To fix that, configure an exit node so traffic path becomes:
|
||||
|
||||
```text
|
||||
Browser -> Local Proxy -> Apps Script -> val.town -> Target website
|
||||
Browser -> Local Proxy -> Apps Script -> Exit Node (Val Town / Cloudflare / Deno) -> Target website
|
||||
```
|
||||
|
||||
1. Open [`apps_script/valtown.ts`](apps_script/valtown.ts) and deploy it on [val.town](https://www.val.town/):
|
||||
- Create a new val
|
||||
- Paste file contents
|
||||
- Add HTTP trigger
|
||||
- Copy your generated URL (`https://<name>.web.val.run`)
|
||||
2. Set `PSK` inside the val code to a strong secret.
|
||||
3. Add this block to your `config.json`:
|
||||
You can deploy any one of these free exit-node templates:
|
||||
|
||||
1. Val Town: [`apps_script/valtown.ts`](apps_script/valtown.ts)
|
||||
2. Cloudflare Workers: [`apps_script/cloudflare_worker.js`](apps_script/cloudflare_worker.js)
|
||||
3. Deno Deploy: [`apps_script/deno_deploy.ts`](apps_script/deno_deploy.ts)
|
||||
|
||||
Full step-by-step deployment guide (all providers):
|
||||
- [EXIT_NODE_DEPLOYMENT.md](EXIT_NODE_DEPLOYMENT.md)
|
||||
|
||||
Set the same PSK secret inside the exit-node code (`PSK` constant) and in `config.json`.
|
||||
|
||||
Then configure provider switching like this:
|
||||
|
||||
```json
|
||||
"exit_node": {
|
||||
"enabled": true,
|
||||
"relay_url": "https://YOUR-NAME.web.val.run",
|
||||
"provider": "valtown",
|
||||
"url": "https://YOUR-NAME.web.val.run",
|
||||
"psk": "CHANGE_ME_TO_A_STRONG_SECRET",
|
||||
"mode": "full",
|
||||
"hosts": [
|
||||
@@ -205,9 +211,11 @@ Browser -> Local Proxy -> Apps Script -> val.town -> Target website
|
||||
```
|
||||
|
||||
Notes:
|
||||
- For noob setup, only fill `provider`, `url`, and `psk`.
|
||||
- Switch provider by changing `exit_node.provider` and `exit_node.url`.
|
||||
- `mode: "full"` = everything goes through exit node (ignore `hosts`).
|
||||
- `mode: "selective"` = only domains in `hosts` go through exit node.
|
||||
- `psk` must be exactly the same as `PSK` in `valtown.ts`.
|
||||
- `psk` must exactly match your deployed exit node secret.
|
||||
|
||||
Production recommendation:
|
||||
- Keep `verify_ssl: true`
|
||||
@@ -306,7 +314,7 @@ By default, the proxy only listens on `127.0.0.1` (localhost), meaning only your
|
||||
|
||||
## Modes Overview
|
||||
|
||||
This project focuses entirely on the **Apps Script** relay — a free Google account is all you need, no server, no VPS, no Cloudflare setup. Everything is configured out of the box for this mode.
|
||||
This project is centered on the **Apps Script** relay (free, no VPS needed). For destinations that block Google egress, you can optionally chain a free edge exit node (Val Town, Cloudflare Workers, or Deno Deploy).
|
||||
|
||||
---
|
||||
|
||||
@@ -345,6 +353,8 @@ This project focuses entirely on the **Apps Script** relay — a free Google acc
|
||||
| `direct_google_exclude` | see [config.example.json](config.example.json) | Google apps that must use the MITM relay path instead of the fast direct tunnel. |
|
||||
| `hosts` | `{}` | Manual DNS override: map a hostname to a specific IP. |
|
||||
| `youtube_via_relay` | `false` | Route YouTube (`youtube.com`, `youtu.be`, `youtube-nocookie.com`) through the Apps Script relay instead of the SNI-rewrite path. The SNI-rewrite path uses Google's frontend IP which enforces SafeSearch and can cause **"Video Unavailable"** errors. Setting this to `true` fixes playback at the cost of using more Apps Script executions and slightly higher latency. |
|
||||
| `exit_node.provider` | `valtown` | Selected exit-node backend: `valtown`, `cloudflare`, `deno`, or `custom`. |
|
||||
| `exit_node.url` | `""` | Beginner-friendly single URL for the selected provider. |
|
||||
|
||||
### Optional Dependencies
|
||||
|
||||
@@ -441,6 +451,52 @@ After scanning, update your `config.json` with the recommended IP and restart th
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Releases (Hidden First Release)
|
||||
|
||||
This repository includes a release workflow at `.github/workflows/release.yml`.
|
||||
|
||||
Default behavior is **hidden for users**:
|
||||
- Tag push (`v*`) creates a GitHub Release as **draft** + **prerelease**
|
||||
- It is not marked as **Latest**
|
||||
|
||||
That means you can run the first release via CI/CD, verify assets, and publish later.
|
||||
|
||||
### Create first hidden release from GitHub Actions
|
||||
|
||||
1. Push a tag:
|
||||
```bash
|
||||
git tag v1.1.0
|
||||
git push origin v1.1.0
|
||||
```
|
||||
2. Wait for the **Release** workflow to finish.
|
||||
3. Open GitHub Releases and review the draft release artifacts.
|
||||
|
||||
### Make it public later
|
||||
|
||||
Run **Actions → Release → Run workflow** with:
|
||||
- `publish = true`
|
||||
- `release_tag = v1.1.0`
|
||||
- `make_public = true`
|
||||
|
||||
This will publish a non-draft, non-prerelease release and mark it as latest.
|
||||
|
||||
### Extra targets (optional, non-blocking)
|
||||
|
||||
`macos-13` runners can be heavily queued, which may leave `macos-x64` waiting for a long time.
|
||||
To keep normal releases fast and reliable, default tag releases now build:
|
||||
- `windows-x64`
|
||||
- `linux-x64`
|
||||
- `macos-arm64`
|
||||
|
||||
If you want extra targets, run **Actions -> Release -> Run workflow** and enable one or more:
|
||||
- `build_macos_x64 = true` (intel macOS)
|
||||
- `build_linux_arm64 = true` (native ARM64 Linux runner)
|
||||
- `build_termux_bundle = true` (Termux source package for arm64/armv7/x86_64 on-device install)
|
||||
|
||||
These extra jobs are optional and non-blocking, so even if one is delayed or unavailable, your main release still completes.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
@@ -464,7 +520,10 @@ MasterHttpRelayVPN/
|
||||
├── config.example.json # Copy to config.json and fill in your values
|
||||
├── requirements.txt # Python dependencies
|
||||
├── apps_script/
|
||||
│ └── Code.gs # The relay script you deploy to Google Apps Script
|
||||
│ ├── Code.gs # The relay script you deploy to Google Apps Script
|
||||
│ ├── valtown.ts # Exit node template for val.town
|
||||
│ ├── cloudflare_worker.js # Exit node template for Cloudflare Workers
|
||||
│ └── deno_deploy.ts # Exit node template for Deno Deploy
|
||||
├── ca/ # Generated MITM CA (do NOT share)
|
||||
│ ├── ca.crt
|
||||
│ └── ca.key
|
||||
|
||||
+24
-11
@@ -137,21 +137,27 @@ cp config.example.json config.json
|
||||
برای حل این مورد، نود خروجی (exit node) را فعال کنید تا مسیر اینگونه شود:
|
||||
|
||||
```text
|
||||
مرورگر -> پراکسی محلی -> Apps Script -> val.town -> سایت مقصد
|
||||
مرورگر -> پراکسی محلی -> Apps Script -> Exit Node (Val Town / Cloudflare / Deno) -> سایت مقصد
|
||||
```
|
||||
|
||||
1. فایل [apps_script/valtown.ts](apps_script/valtown.ts) را در val.town deploy کنید:
|
||||
- یک val جدید بسازید
|
||||
- محتوای فایل را paste کنید
|
||||
- HTTP trigger را فعال کنید
|
||||
- آدرس نهایی (`https://<name>.web.val.run`) را کپی کنید
|
||||
2. مقدار `PSK` داخل فایل val را با یک رمز قوی تغییر دهید.
|
||||
3. در `config.json` این بخش را اضافه/تکمیل کنید:
|
||||
میتوانید یکی از این template های رایگان را deploy کنید:
|
||||
|
||||
1. Val Town: [apps_script/valtown.ts](apps_script/valtown.ts)
|
||||
2. Cloudflare Workers: [apps_script/cloudflare_worker.js](apps_script/cloudflare_worker.js)
|
||||
3. Deno Deploy: [apps_script/deno_deploy.ts](apps_script/deno_deploy.ts)
|
||||
|
||||
راهنمای کامل مرحلهبهمرحله برای هر provider:
|
||||
- [EXIT_NODE_DEPLOYMENT.md](EXIT_NODE_DEPLOYMENT.md)
|
||||
|
||||
سپس همان secret را هم در کد نود خروجی (`PSK`) و هم در `config.json` یکسان بگذارید.
|
||||
|
||||
نمونه کانفیگ برای سوییچ بین provider ها:
|
||||
|
||||
```json
|
||||
"exit_node": {
|
||||
"enabled": true,
|
||||
"relay_url": "https://YOUR-NAME.web.val.run",
|
||||
"provider": "valtown",
|
||||
"url": "https://YOUR-NAME.web.val.run",
|
||||
"psk": "CHANGE_ME_TO_A_STRONG_SECRET",
|
||||
"mode": "full",
|
||||
"hosts": [
|
||||
@@ -164,9 +170,11 @@ cp config.example.json config.json
|
||||
```
|
||||
|
||||
نکات:
|
||||
- برای تنظیم ساده، فقط `provider`، `url` و `psk` را پر کنید.
|
||||
- برای تغییر backend مقدار `exit_node.provider` و `exit_node.url` را عوض کنید.
|
||||
- `mode: "full"` یعنی همه ترافیک از exit node عبور میکند (`hosts` نادیده گرفته میشود).
|
||||
- `mode: "selective"` یعنی فقط دامنههای داخل `hosts` از exit node عبور میکنند.
|
||||
- مقدار `psk` باید دقیقا با `PSK` در `valtown.ts` یکی باشد.
|
||||
- مقدار `psk` باید دقیقا با secret تنظیمشده در runtime برابر باشد.
|
||||
|
||||
### مرحله 4: اجرا
|
||||
|
||||
@@ -284,6 +292,8 @@ json
|
||||
| `bypass_hosts` | `["localhost", ".local", ".lan", ".home.arpa"]` | هاستهایی که مستقیم میروند (بدون MITM و بدون رله). برای منابع داخلی شبکه یا سایتهایی که با MITM مشکل دارند. |
|
||||
| `direct_google_exclude` | مراجعه به [config.example.json](config.example.json) | اپهای Google که باید از مسیر MITM برای رله استفاده کنند بهجای tunnel مستقیم. |
|
||||
| `youtube_via_relay` | `false` | مسیردهی YouTube (`youtube.com`، `youtu.be`، `youtube-nocookie.com`) از طریق رله Apps Script بهجای مسیر SNI-rewrite. مسیر SNI-rewrite از IP فرانتاند Google عبور میکند که SafeSearch را اجباری میکند و میتواند باعث خطای **«ویدیو در دسترس نیست»** شود. با فعال کردن این گزینه، پخش ویدیو درست میشود اما تعداد اجراهای Apps Script بیشتر و تأخیر اندکی بالاتر میرود. |
|
||||
| `exit_node.provider` | `valtown` | backend انتخابشده برای exit node: `valtown`، `cloudflare`، `deno` یا `custom`. |
|
||||
| `exit_node.url` | `""` | آدرس ساده و اصلی برای provider انتخابشده. |
|
||||
|
||||
### وابستگیهای اختیاری
|
||||
|
||||
@@ -398,7 +408,10 @@ MasterHttpRelayVPN/
|
||||
├── config.example.json # نمونه کانفیگ (به config.json کپی شود)
|
||||
├── requirements.txt # وابستگیهای اختیاری پایتون
|
||||
├── apps_script/
|
||||
│ └── Code.gs # اسکریپت رله روی Google Apps Script
|
||||
│ ├── Code.gs # اسکریپت رله روی Google Apps Script
|
||||
│ ├── valtown.ts # template نود خروجی برای val.town
|
||||
│ ├── cloudflare_worker.js # template نود خروجی برای Cloudflare Workers
|
||||
│ └── deno_deploy.ts # template نود خروجی برای Deno Deploy
|
||||
├── ca/ # گواهی MITM (هرگز به اشتراک نگذارید)
|
||||
│ ├── ca.crt
|
||||
│ └── ca.key
|
||||
|
||||
@@ -19,23 +19,23 @@ const STRIP_HEADERS = new Set([
|
||||
"via",
|
||||
]);
|
||||
|
||||
function decodeBase64ToBytes(input: string): Uint8Array {
|
||||
function decodeBase64ToBytes(input) {
|
||||
const bin = atob(input);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function encodeBytesToBase64(bytes: Uint8Array): string {
|
||||
function encodeBytesToBase64(bytes) {
|
||||
let bin = "";
|
||||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
function sanitizeHeaders(h: unknown): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
function sanitizeHeaders(h) {
|
||||
const out = {};
|
||||
if (!h || typeof h !== "object") return out;
|
||||
for (const [k, v] of Object.entries(h as Record<string, unknown>)) {
|
||||
for (const [k, v] of Object.entries(h)) {
|
||||
if (!k) continue;
|
||||
if (STRIP_HEADERS.has(k.toLowerCase())) continue;
|
||||
out[k] = String(v ?? "");
|
||||
@@ -44,7 +44,7 @@ function sanitizeHeaders(h: unknown): Record<string, string> {
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
async fetch(req) {
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
return Response.json({ e: "method_not_allowed" }, { status: 405 });
|
||||
@@ -59,28 +59,28 @@ export default {
|
||||
return Response.json({ e: "server_psk_missing" }, { status: 500 });
|
||||
}
|
||||
|
||||
const k = String((body as any).k ?? "");
|
||||
const u = String((body as any).u ?? "");
|
||||
const m = String((body as any).m ?? "GET").toUpperCase();
|
||||
const h = sanitizeHeaders((body as any).h);
|
||||
const b64 = (body as any).b;
|
||||
const k = String(body.k ?? "");
|
||||
const u = String(body.u ?? "");
|
||||
const m = String(body.m ?? "GET").toUpperCase();
|
||||
const h = sanitizeHeaders(body.h);
|
||||
const b64 = body.b;
|
||||
|
||||
if (k !== PSK) return Response.json({ e: "unauthorized" }, { status: 401 });
|
||||
if (!/^https?:\/\//i.test(u)) return Response.json({ e: "bad_url" }, { status: 400 });
|
||||
|
||||
let payload: Uint8Array | undefined;
|
||||
let payload;
|
||||
if (typeof b64 === "string" && b64.length > 0) payload = decodeBase64ToBytes(b64);
|
||||
const requestBody = payload ? Uint8Array.from(payload) : undefined;
|
||||
|
||||
const resp = await fetch(u, {
|
||||
method: m,
|
||||
headers: h,
|
||||
body: requestBody as unknown as BodyInit,
|
||||
body: requestBody,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
const data = new Uint8Array(await resp.arrayBuffer());
|
||||
const respHeaders: Record<string, string> = {};
|
||||
const respHeaders = {};
|
||||
resp.headers.forEach((value, key) => {
|
||||
respHeaders[key] = value;
|
||||
});
|
||||
+7
-3
@@ -87,9 +87,13 @@
|
||||
"hosts": {},
|
||||
"exit_node": {
|
||||
"enabled": false,
|
||||
"relay_url": "",
|
||||
"provider": "cloudflare",
|
||||
"url": "",
|
||||
"psk": "",
|
||||
"mode": "selective",
|
||||
"hosts": []
|
||||
"mode": "full",
|
||||
"hosts": [
|
||||
"example.com",
|
||||
"example.org"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+80
-9
@@ -214,9 +214,17 @@ class DomainFronter:
|
||||
# Useful for sites that block GCP/Apps Script IPs (e.g. ChatGPT).
|
||||
en_cfg = config.get("exit_node") or {}
|
||||
self._exit_node_enabled: bool = bool(en_cfg.get("enabled", False))
|
||||
self._exit_node_url: str = str(en_cfg.get("relay_url") or "").rstrip("/")
|
||||
self._exit_node_provider: str = self._normalize_exit_node_provider(
|
||||
en_cfg.get("provider"),
|
||||
)
|
||||
self._exit_node_url: str = self._resolve_exit_node_url(
|
||||
self._exit_node_provider,
|
||||
en_cfg,
|
||||
)
|
||||
self._exit_node_psk: str = str(en_cfg.get("psk") or "")
|
||||
self._exit_node_mode: str = str(en_cfg.get("mode") or "selective").lower()
|
||||
if self._exit_node_mode not in ("full", "selective"):
|
||||
self._exit_node_mode = "selective"
|
||||
self._exit_node_hosts: frozenset[str] = frozenset(
|
||||
str(h).lower().strip().lstrip(".")
|
||||
for h in (en_cfg.get("hosts") or [])
|
||||
@@ -224,8 +232,15 @@ class DomainFronter:
|
||||
)
|
||||
if self._exit_node_enabled and self._exit_node_url:
|
||||
log.info(
|
||||
"Exit node enabled [mode=%s]: %s",
|
||||
self._exit_node_mode, self._exit_node_url,
|
||||
"Exit node enabled [mode=%s, provider=%s]: %s",
|
||||
self._exit_node_mode,
|
||||
self._exit_node_provider,
|
||||
self._exit_node_url,
|
||||
)
|
||||
elif self._exit_node_enabled:
|
||||
log.warning(
|
||||
"Exit node is enabled but no URL is configured for provider '%s'",
|
||||
self._exit_node_provider,
|
||||
)
|
||||
|
||||
# Capability log for content encodings.
|
||||
@@ -1107,6 +1122,62 @@ class DomainFronter:
|
||||
|
||||
# ── Exit node relay ───────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _normalize_exit_node_provider(raw: object) -> str:
|
||||
provider = str(raw or "custom").strip().lower()
|
||||
aliases = {
|
||||
"val": "valtown",
|
||||
"val-town": "valtown",
|
||||
"cloudflare_worker": "cloudflare",
|
||||
"worker": "cloudflare",
|
||||
"cf": "cloudflare",
|
||||
"deno_deploy": "deno",
|
||||
}
|
||||
return aliases.get(provider, provider or "custom")
|
||||
|
||||
@classmethod
|
||||
def _resolve_exit_node_url(cls, provider: str,
|
||||
en_cfg: dict[str, object]) -> str:
|
||||
providers = en_cfg.get("providers")
|
||||
if not isinstance(providers, dict):
|
||||
providers = {}
|
||||
|
||||
def _pick_from(mapping: dict[str, object], *keys: str) -> str:
|
||||
for key in keys:
|
||||
value = mapping.get(key)
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
if value:
|
||||
return value.rstrip("/")
|
||||
return ""
|
||||
|
||||
# Beginner-first: one URL field is enough for all providers.
|
||||
direct = _pick_from(en_cfg, "url")
|
||||
if direct:
|
||||
return direct
|
||||
|
||||
if provider == "valtown":
|
||||
selected = _pick_from(en_cfg, "valtown_url", "val_url") or _pick_from(
|
||||
providers, "valtown", "val_town", "val",
|
||||
)
|
||||
elif provider == "cloudflare":
|
||||
selected = _pick_from(
|
||||
en_cfg, "cloudflare_url", "worker_url", "cf_url",
|
||||
) or _pick_from(
|
||||
providers, "cloudflare", "cloudflare_worker", "worker", "cf",
|
||||
)
|
||||
elif provider == "deno":
|
||||
selected = _pick_from(en_cfg, "deno_url") or _pick_from(
|
||||
providers, "deno", "deno_deploy",
|
||||
)
|
||||
else:
|
||||
selected = ""
|
||||
|
||||
if selected:
|
||||
return selected
|
||||
# Backward compatibility for older config format.
|
||||
return _pick_from(en_cfg, "relay_url")
|
||||
|
||||
def _exit_node_matches(self, url: str) -> bool:
|
||||
"""Return True if this URL should be routed through the exit node."""
|
||||
if not self._exit_node_enabled or not self._exit_node_url:
|
||||
@@ -1123,22 +1194,22 @@ class DomainFronter:
|
||||
return False
|
||||
|
||||
async def _relay_via_exit_node(self, payload: dict) -> bytes:
|
||||
"""Chain: Apps Script → exit node (val.town) → Destination.
|
||||
"""Chain: Apps Script → edge relay (exit node) → Destination.
|
||||
|
||||
Traffic path:
|
||||
Client → [domain fronting TLS] → Apps Script (Google)
|
||||
→ [UrlFetchApp.fetch] → exit node (val.town / non-Google IP)
|
||||
→ [UrlFetchApp.fetch] → exit node (non-Google IP)
|
||||
→ [fetch()] → Destination
|
||||
|
||||
This preserves the DPI bypass (Apps Script is always the outbound
|
||||
connection from the client's perspective) while giving the destination
|
||||
a non-Google exit IP — fixing Cloudflare Turnstile, ChatGPT, etc.
|
||||
|
||||
The inner payload going to val.town is base64-encoded and sent as the
|
||||
The inner payload going to the exit node is base64-encoded and sent as the
|
||||
body of the outer Apps Script relay call, so Apps Script POSTs it to
|
||||
the exit node URL on our behalf.
|
||||
"""
|
||||
# Build inner payload: what val.town will execute
|
||||
# Build inner payload: what the exit node will execute
|
||||
inner = dict(payload)
|
||||
inner["k"] = self._exit_node_psk
|
||||
inner_json = json.dumps(inner).encode()
|
||||
@@ -1163,9 +1234,9 @@ class DomainFronter:
|
||||
# Send through the normal Apps Script relay path (H2 or H1 + retry)
|
||||
raw = await self._relay_with_retry(outer)
|
||||
|
||||
# raw is now the response from val.town (which is the inner relay JSON)
|
||||
# raw is now the response from the exit node (inner relay JSON)
|
||||
# _parse_relay_response will decode it into the final HTTP response.
|
||||
# But we need to unwrap one level: Apps Script gives us val.town's HTTP
|
||||
# But we need to unwrap one level: Apps Script gives us exit node HTTP
|
||||
# response body (which is itself a relay JSON), so parse twice.
|
||||
_, _, apps_script_body = self._split_raw_response(raw)
|
||||
result = self._parse_relay_response(apps_script_body)
|
||||
|
||||
Reference in New Issue
Block a user