Skip to content

Commit 79cca10

Browse files
authored
feat: multi-edge fronting_groups + rename google_only to direct (#488)
Generalizes the Google-edge SNI-rewrite trick to any multi-tenant CDN edge (Vercel, Fastly, …). By @dazzling-no-more, with credit to @patterniha for the original technique (MITM-DomainFronting). New `fronting_groups: [{name, ip, sni, domains}]` config field — matched hosts get MITM-decrypted at the local CA and re-encrypted upstream against `ip` with `sni` as the TLS SNI. Works alongside the built-in Google fronting and `passthrough_hosts`. Rename: `mode = "google_only"` → `mode = "direct"`. Old name kept as deprecated alias on parse — no existing config / saved settings break. UI dropdown updated, on-disk file migrates on next Save. Review fixes folded in: SNI validated via rustls at config-load gate, Vec<Arc<>> refcount instead of clone-on-match, byte-level dot-anchored matcher (no per-match format!()), startup warnings for inert combos. Working example at config.fronting-groups.example.json. Full doc at docs/fronting-groups.md including precedence rules + the cross-tenant Host-header leak warning. Test plan: cargo build --release clean, cargo test --lib 169/169 passing (+8 new: dispatch matching, config validation, alias back-compat). Per author's recommendation, this lands as the v1.9.0 headline — new top-level config field + public mode-string rename are minor-bump territory. xmux moves to v1.10.0.
2 parents aad900e + f32d343 commit 79cca10

17 files changed

Lines changed: 811 additions & 119 deletions

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ This part is unchanged from the original project. Follow @masterking32's guide o
104104

105105
#### Can't reach `script.google.com` from your network?
106106

107-
If your ISP is already blocking Google Apps Script (or all of Google), you need Step 1's browser connection to succeed *before* you have a relay to use. `mhrv-rs` ships a small bootstrap mode for exactly this: `google_only`.
107+
If your ISP is already blocking Google Apps Script (or all of Google), you need Step 1's browser connection to succeed *before* you have a relay to use. `mhrv-rs` ships a `direct` mode for exactly this — SNI-rewrite tunnel only, no Apps Script relay required. (Was named `google_only` before v1.9 — the old name is still accepted in config files.)
108108

109109
1. Build / download the binary as in Step 2 below.
110-
2. Copy [`config.google-only.example.json`](config.google-only.example.json) to `config.json` — no `script_id`, no `auth_key` required.
110+
2. Copy [`config.direct.example.json`](config.direct.example.json) to `config.json` — no `script_id`, no `auth_key` required.
111111
3. Run `mhrv-rs serve` and set your browser's HTTP proxy to `127.0.0.1:8085`.
112-
4. In `google_only` mode the proxy only relays `*.google.com`, `*.youtube.com`, and the other Google-edge hosts via the same SNI-rewrite tunnel the full client uses. Other traffic goes direct — no Apps Script relay exists yet.
112+
4. In `direct` mode the proxy only routes `*.google.com`, `*.youtube.com`, and the other Google-edge hosts (plus any [`fronting_groups`](docs/fronting-groups.md) you've configured) via the SNI-rewrite tunnel. Other traffic goes raw — no Apps Script relay exists yet.
113113
5. Do Step 1 in your browser (the connection to `script.google.com` will be SNI-fronted). Deploy Code.gs, copy the Deployment ID.
114114
6. In the desktop UI or the Android app (or by editing `config.json`) switch the mode back to `apps_script`, paste the Deployment ID and your auth key, and restart.
115115

@@ -501,15 +501,15 @@ Donations cover hosting, self-hosted CI runner costs, and continued maintenance.
501501
502502
#### به `script.google.com` هم دسترسی ندارید؟
503503

504-
اگر `ISP` شما از قبل `Apps Script` (یا کل گوگل) را مسدود کرده، برای مرحلهٔ ۱ باید مرورگرتان **اول** به `script.google.com` برسد — قبل از اینکه رله‌ای داشته باشید. `mhrv-rs` یک حالت بوت‌استرپ کوچک دقیقاً برای همین دارد: `google_only`.
504+
اگر `ISP` شما از قبل `Apps Script` (یا کل گوگل) را مسدود کرده، برای مرحلهٔ ۱ باید مرورگرتان **اول** به `script.google.com` برسد — قبل از اینکه رله‌ای داشته باشید. `mhrv-rs` یک حالت `direct` دقیقاً برای همین دارد — فقط تونل بازنویسی `SNI`، بدون نیاز به رلهٔ `Apps Script`. (قبل از v1.9 این حالت `google_only` نام داشت — نام قدیمی همچنان در فایل کانفیگ پذیرفته می‌شود.)
505505

506506
۱. برنامه را طبق مرحلهٔ ۲ پایین دانلود کنید
507507

508-
۲. فایل [`config.google-only.example.json`](config.google-only.example.json) را در کنار فایل اجرایی به نام `config.json` کپی کنید — نه `script_id` لازم دارد و نه `auth_key`
508+
۲. فایل [`config.direct.example.json`](config.direct.example.json) را در کنار فایل اجرایی به نام `config.json` کپی کنید — نه `script_id` لازم دارد و نه `auth_key`
509509

510510
۳. برنامه را اجرا کنید و `HTTP proxy` مرورگرتان را روی `127.0.0.1:8085` تنظیم کنید
511511

512-
۴. در حالت `google_only`، پروکسی فقط `*.google.com`، `*.youtube.com` و بقیهٔ میزبان‌های لبهٔ گوگل را از طریق همان تونل بازنویسی `SNI` رد می‌کند. بقیهٔ ترافیک مستقیم می‌رود — هنوز رله‌ای در کار نیست
512+
۴. در حالت `direct`، پروکسی فقط `*.google.com`، `*.youtube.com` و بقیهٔ میزبان‌های لبهٔ گوگل (به علاوهٔ هر [`fronting_groups`](docs/fronting-groups.md) که تنظیم کرده باشید) را از طریق تونل بازنویسی `SNI` رد می‌کند. بقیهٔ ترافیک مستقیم می‌رود — هنوز رله‌ای در کار نیست
513513

514514
۵. حالا مرحلهٔ ۱ را در مرورگر انجام دهید (اتصال به `script.google.com` با `SNI` فرونت می‌شود). `Code.gs` را مستقر کنید و `Deployment ID` را کپی کنید
515515

SF_README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ A free way to bypass internet censorship by routing your traffic through your ow
2323
**1. Set up the relay in your Google account (one-time).**
2424
Go to <https://script.google.com>, sign in, click **New project**. Delete the sample code, paste in the [Code.gs file from this repo](assets/apps_script/Code.gs), change `AUTH_KEY = "..."` to a password only you know. Click **Deploy → New deployment → Web app**, set "Execute as: Me", "Who has access: Anyone". Copy the long ID from the URL — that's your **Deployment ID**.
2525

26-
> Can't reach `script.google.com` because it's blocked? Run mhrv-rs first in `google_only` mode (use [`config.google-only.example.json`](config.google-only.example.json)). It only relays Google sites and lets you reach the Apps Script editor through the bypass tunnel. Do step 1 in your browser, then switch back to normal mode.
26+
> Can't reach `script.google.com` because it's blocked? Run mhrv-rs first in `direct` mode (use [`config.direct.example.json`](config.direct.example.json)). It only relays Google sites (plus any [fronting_groups](docs/fronting-groups.md) you've configured) and lets you reach the Apps Script editor through the bypass tunnel. Do step 1 in your browser, then switch back to normal mode. (`direct` was named `google_only` before v1.9 — the old name still works.)
2727
2828
**2. Install and run mhrv-rs.**
2929
Download the package for your system from [Releases](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases/latest) and unzip it.
@@ -94,7 +94,7 @@ This project is free and run by volunteers. If it helped you and you can spare a
9494
**۱. ساخت ریله در حساب گوگل (فقط یک بار).**
9595
به <https://script.google.com> بروید، وارد حساب گوگل شوید و روی **New project** بزنید. کد پیش‌فرض را پاک کنید و محتوای [فایل Code.gs](assets/apps_script/Code.gs) همین مخزن را در آن جای‌گذاری کنید. خط `AUTH_KEY = "..."` را به یک رمز دلخواه که فقط خودتان می‌دانید تغییر دهید. سپس **Deploy → New deployment → Web app** را بزنید، گزینهٔ "Execute as: Me" و "Who has access: Anyone" را انتخاب کنید. آی‌دی طولانی توی URL را کپی کنید — این **Deployment ID** شماست.
9696

97-
> اگر `script.google.com` خودش بسته است، اول mhrv-rs را در حالت `google_only` اجرا کنید (از [`config.google-only.example.json`](config.google-only.example.json) استفاده کنید). این حالت فقط سایت‌های گوگل را تونل می‌کند تا بتوانید به ویرایشگر Apps Script برسید. مرحلهٔ ۱ را در مرورگر انجام دهید و بعد به حالت معمولی برگردید.
97+
> اگر `script.google.com` خودش بسته است، اول mhrv-rs را در حالت `direct` اجرا کنید (از [`config.direct.example.json`](config.direct.example.json) استفاده کنید). این حالت فقط سایت‌های گوگل (به علاوهٔ هر [fronting_groups](docs/fronting-groups.md) که تنظیم کرده باشید) را تونل می‌کند تا بتوانید به ویرایشگر Apps Script برسید. مرحلهٔ ۱ را در مرورگر انجام دهید و بعد به حالت معمولی برگردید. (نام قبلی این حالت `google_only` بود — همچنان پذیرفته می‌شود.)
9898
9999
**۲. نصب و اجرای mhrv-rs.**
100100
بستهٔ مخصوص سیستم خودتان را از [بخش Releases](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases/latest) دانلود کنید و از حالت فشرده در بیاورید.

android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,18 @@ enum class UiLang { AUTO, FA, EN }
6464
*
6565
* - [APPS_SCRIPT] (default) — full DPI bypass through the user's deployed
6666
* Apps Script relay. Requires a Deployment ID + Auth key.
67-
* - [GOOGLE_ONLY] — bootstrap mode. Only the SNI-rewrite tunnel to the
68-
* Google edge is active, so the user can reach `script.google.com` to
69-
* deploy Code.gs in the first place. No Deployment ID / Auth key needed.
70-
* Non-Google traffic goes direct (no relay).
67+
* - [DIRECT] — no Apps Script relay. Only the SNI-rewrite tunnel is
68+
* active: Google edge by default, plus any user-configured
69+
* `fronting_groups` (Vercel, Fastly, …). Useful as a bootstrap to
70+
* reach `script.google.com` and deploy Code.gs, or as a standalone
71+
* mode for users who only need fronting-group targets. No Deployment
72+
* ID / Auth key needed. Non-matching traffic goes raw (no relay).
73+
* Was named `GOOGLE_ONLY` before fronting_groups was added — the
74+
* string `"google_only"` is still accepted on parse for back-compat.
7175
* - [FULL] — full tunnel mode. ALL traffic is tunneled end-to-end through
7276
* Apps Script + a remote tunnel node. No certificate installation needed.
7377
*/
74-
enum class Mode { APPS_SCRIPT, GOOGLE_ONLY, FULL }
78+
enum class Mode { APPS_SCRIPT, DIRECT, FULL }
7579

7680
data class MhrvConfig(
7781
val mode: Mode = Mode.APPS_SCRIPT,
@@ -177,14 +181,14 @@ data class MhrvConfig(
177181
// "missing field `mode`" and startProxy silently returns 0.
178182
put("mode", when (mode) {
179183
Mode.APPS_SCRIPT -> "apps_script"
180-
Mode.GOOGLE_ONLY -> "google_only"
184+
Mode.DIRECT -> "direct"
181185
Mode.FULL -> "full"
182186
})
183187
put("listen_host", listenHost)
184188
put("listen_port", listenPort)
185189
socks5Port?.let { put("socks5_port", it) }
186190

187-
// In google_only mode these are unused by the Rust side, but we
191+
// In direct mode these are unused by the Rust side, but we
188192
// still persist whatever the user typed so flipping back to
189193
// apps_script mode doesn't wipe their settings.
190194
put("script_ids", JSONArray().apply { ids.forEach { put(it) } })
@@ -286,7 +290,7 @@ object ConfigStore {
286290
// Always include essential fields.
287291
obj.put("mode", when (cfg.mode) {
288292
Mode.APPS_SCRIPT -> "apps_script"
289-
Mode.GOOGLE_ONLY -> "google_only"
293+
Mode.DIRECT -> "direct"
290294
Mode.FULL -> "full"
291295
})
292296
val ids = cfg.appsScriptUrls.mapNotNull { url ->
@@ -391,7 +395,10 @@ object ConfigStore {
391395

392396
return MhrvConfig(
393397
mode = when (obj.optString("mode", "apps_script")) {
394-
"google_only" -> Mode.GOOGLE_ONLY
398+
"direct" -> Mode.DIRECT
399+
// Deprecated alias kept forever for back-compat with
400+
// configs written before the rename.
401+
"google_only" -> Mode.DIRECT
395402
"full" -> Mode.FULL
396403
else -> Mode.APPS_SCRIPT
397404
},

android/app/src/main/java/com/therealaleph/mhrv/MhrvVpnService.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ class MhrvVpnService : VpnService() {
104104
startForeground(NOTIF_ID, buildNotif(cfg.listenPort, notifSocks5Port))
105105

106106
// Deployment ID + auth key are required for apps_script and full
107-
// modes — both talk to Apps Script. Only google_only (bootstrap)
108-
// runs without them. Closes #73 regression where google_only
109-
// users hit this branch and crashed on startForeground timeout.
110-
val needsCreds = cfg.mode != Mode.GOOGLE_ONLY
107+
// modes — both talk to Apps Script. Only `direct` mode runs
108+
// without them. Closes #73 regression where direct-mode users
109+
// hit this branch and crashed on startForeground timeout.
110+
val needsCreds = cfg.mode != Mode.DIRECT
111111
if (needsCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
112112
Log.e(TAG, "Config is incomplete — deployment ID + auth key required for ${cfg.mode}")
113113
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}

android/app/src/main/java/com/therealaleph/mhrv/Native.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ object Native {
8383
* Live traffic/usage counters for a running proxy handle. Returns a
8484
* JSON blob with the StatsSnapshot fields — or an empty string if the
8585
* handle is unknown or the proxy isn't using the Apps Script relay
86-
* (google_only / full-only modes).
86+
* (direct / full-only modes).
8787
*
8888
* Schema (all integer fields unless noted):
8989
* relay_calls, relay_failures, coalesced, bytes_relayed,

android/app/src/main/java/com/therealaleph/mhrv/ui/ConfigSharing.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ private fun ImportConfirmDialog(
264264
val preview = ids.take(3).joinToString("\n") { " ${it.take(20)}" }
265265
val modeLabel = when (cfg.mode) {
266266
com.therealaleph.mhrv.Mode.APPS_SCRIPT -> "apps_script"
267-
com.therealaleph.mhrv.Mode.GOOGLE_ONLY -> "google_only"
267+
com.therealaleph.mhrv.Mode.DIRECT -> "direct"
268268
com.therealaleph.mhrv.Mode.FULL -> "full"
269269
}
270270

android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ fun HomeScreen(
316316
}
317317
},
318318
enabled = (isVpnRunning ||
319-
cfg.mode == Mode.GOOGLE_ONLY ||
319+
cfg.mode == Mode.DIRECT ||
320320
(cfg.hasDeploymentId && cfg.authKey.isNotBlank())) && !transitioning,
321321
colors = ButtonDefaults.buttonColors(
322322
containerColor = if (isVpnRunning) ErrRed else OkGreen,
@@ -837,7 +837,7 @@ private fun DeploymentIdsField(
837837
}
838838

839839
// =========================================================================
840-
// Mode dropdown: apps_script (default) vs google_only (bootstrap).
840+
// Mode dropdown: apps_script (default), direct (no relay), or full.
841841
// =========================================================================
842842

843843
@OptIn(ExperimentalMaterial3Api::class)
@@ -847,11 +847,11 @@ private fun ModeDropdown(
847847
onChange: (Mode) -> Unit,
848848
) {
849849
val labelApps = "Apps Script (MITM)"
850-
val labelGoogle = "Google-only (bootstrap)"
850+
val labelDirect = "Direct (no relay)"
851851
val labelFull = "Full tunnel (no cert)"
852852
val currentLabel = when (mode) {
853853
Mode.APPS_SCRIPT -> labelApps
854-
Mode.GOOGLE_ONLY -> labelGoogle
854+
Mode.DIRECT -> labelDirect
855855
Mode.FULL -> labelFull
856856
}
857857
var expanded by remember { mutableStateOf(false) }
@@ -878,8 +878,8 @@ private fun ModeDropdown(
878878
onClick = { onChange(Mode.APPS_SCRIPT); expanded = false },
879879
)
880880
DropdownMenuItem(
881-
text = { Text(labelGoogle) },
882-
onClick = { onChange(Mode.GOOGLE_ONLY); expanded = false },
881+
text = { Text(labelDirect) },
882+
onClick = { onChange(Mode.DIRECT); expanded = false },
883883
)
884884
DropdownMenuItem(
885885
text = { Text(labelFull) },
@@ -891,8 +891,8 @@ private fun ModeDropdown(
891891
val help = when (mode) {
892892
Mode.APPS_SCRIPT ->
893893
"Full DPI bypass through your deployed Apps Script relay."
894-
Mode.GOOGLE_ONLY ->
895-
"Bootstrap: reach *.google.com directly so you can open script.google.com and deploy Code.gs. Non-Google traffic goes direct."
894+
Mode.DIRECT ->
895+
"SNI-rewrite tunnel only — no relay. Reach *.google.com (and any configured fronting_groups) directly. Useful as a bootstrap to open script.google.com and deploy Code.gs."
896896
Mode.FULL ->
897897
"All traffic tunneled end-to-end through Apps Script + remote tunnel node. No certificate needed."
898898
}
@@ -1430,7 +1430,7 @@ private fun CollapsibleSection(
14301430
* this device relayed.
14311431
*
14321432
* Hidden when the handle is 0 (proxy not running) or the JSON comes back
1433-
* empty (google_only / full-only configs don't run a DomainFronter and so
1433+
* empty (direct / full-only configs don't run a DomainFronter and so
14341434
* have nothing to report).
14351435
*/
14361436
@Composable
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"mode": "google_only",
2+
"mode": "direct",
33
"google_ip": "216.239.38.120",
44
"front_domain": "www.google.com",
55
"listen_host": "127.0.0.1",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"mode": "direct",
3+
"google_ip": "216.239.38.120",
4+
"front_domain": "www.google.com",
5+
"listen_host": "127.0.0.1",
6+
"listen_port": 8085,
7+
"socks5_port": 8086,
8+
"log_level": "info",
9+
"verify_ssl": true,
10+
"fronting_groups": [
11+
{
12+
"name": "vercel",
13+
"ip": "76.76.21.21",
14+
"sni": "react.dev",
15+
"domains": [
16+
"vercel.com",
17+
"vercel.app",
18+
"vercel.dev",
19+
"vercel.live",
20+
"vercel.sh",
21+
"nextjs.org",
22+
"now.sh",
23+
"cursor.com",
24+
"ai-sdk.dev"
25+
]
26+
},
27+
{
28+
"name": "fastly",
29+
"ip": "151.101.1.140",
30+
"sni": "www.python.org",
31+
"domains": [
32+
"reddit.com",
33+
"redditstatic.com",
34+
"redditmedia.com",
35+
"githubassets.com",
36+
"githubusercontent.com",
37+
"pypi.org",
38+
"fastly.com"
39+
]
40+
},
41+
{
42+
"name": "netlify",
43+
"ip": "35.157.26.135",
44+
"sni": "letsencrypt.org",
45+
"domains": [
46+
"netlify.app",
47+
"netlify.com"
48+
]
49+
}
50+
]
51+
}

0 commit comments

Comments
 (0)