Skip to content

Commit 539fee3

Browse files
feat: add google_only bootstrap mode (#62)
Second operating mode for users whose network already blocks script.google.com and therefore cannot reach it to deploy Code.gs in the first place. In google_only, the client runs only the SNI-rewrite tunnel to *.google.com and the other Google-edge suffixes that are already allowlisted; non-Google traffic falls through to direct TCP. No script_id or auth_key is required. Once Code.gs is deployed, the user switches to apps_script mode and pastes the Deployment ID. - config: Mode enum, relaxed validation when mode is google_only - proxy_server: mode check in dispatch_tunnel; DomainFronter is now Option<Arc<_>> so it is not constructed in google_only - desktop UI and Android app: Mode dropdown, Apps Script fields disable in google_only - README: bootstrap subsection in English and Persian - config.google-only.example.json - version bump to 1.2.0 + changelog entry Backward compatible with existing apps_script configs.
1 parent ba44c9e commit 539fee3

13 files changed

Lines changed: 499 additions & 104 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "1.1.5"
3+
version = "1.2.0"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ This part is unchanged from the original project. Follow @masterking32's guide o
9797
- Who has access: **Anyone**
9898
6. Copy the **Deployment ID** (the long random string in the URL).
9999

100+
#### Can't reach `script.google.com` from your network?
101+
102+
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`.
103+
104+
1. Build / download the binary as in Step 2 below.
105+
2. Copy [`config.google-only.example.json`](config.google-only.example.json) to `config.json` — no `script_id`, no `auth_key` required.
106+
3. Run `mhrv-rs serve` and set your browser's HTTP proxy to `127.0.0.1:8085`.
107+
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.
108+
5. Do Step 1 in your browser (the connection to `script.google.com` will be SNI-fronted). Deploy Code.gs, copy the Deployment ID.
109+
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.
110+
111+
You can also verify reachability before even starting the proxy: `mhrv-rs test-sni` probes `*.google.com` directly and works without any config beyond `google_ip` + `front_domain`.
112+
100113
### Step 2 — Download
101114

102115
Grab the archive for your platform from the [releases page](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases) and extract it.
@@ -398,6 +411,24 @@ Original project: <https://github.com/masterking32/MasterHttpRelayVPN> by [@mast
398411

399412
> **نکته:** اگر نمی‌دانید رمز `AUTH_KEY` چه بگذارید، یک رشتهٔ تصادفی ۱۶ تا ۲۴ کاراکتری بسازید. مهم فقط این است که **دقیقاً همان رشته** را در برنامه هم وارد کنید.
400413
414+
#### به `script.google.com` هم دسترسی ندارید؟
415+
416+
اگر `ISP` شما از قبل `Apps Script` (یا کل گوگل) را مسدود کرده، برای مرحلهٔ ۱ باید مرورگرتان **اول** به `script.google.com` برسد — قبل از اینکه رله‌ای داشته باشید. `mhrv-rs` یک حالت بوت‌استرپ کوچک دقیقاً برای همین دارد: `google_only`.
417+
418+
۱. برنامه را طبق مرحلهٔ ۲ پایین دانلود کنید
419+
420+
۲. فایل [`config.google-only.example.json`](config.google-only.example.json) را در کنار فایل اجرایی به نام `config.json` کپی کنید — نه `script_id` لازم دارد و نه `auth_key`
421+
422+
۳. برنامه را اجرا کنید و `HTTP proxy` مرورگرتان را روی `127.0.0.1:8085` تنظیم کنید
423+
424+
۴. در حالت `google_only`، پروکسی فقط `*.google.com`، `*.youtube.com` و بقیهٔ میزبان‌های لبهٔ گوگل را از طریق همان تونل بازنویسی `SNI` رد می‌کند. بقیهٔ ترافیک مستقیم می‌رود — هنوز رله‌ای در کار نیست
425+
426+
۵. حالا مرحلهٔ ۱ را در مرورگر انجام دهید (اتصال به `script.google.com` با `SNI` فرونت می‌شود). `Code.gs` را مستقر کنید و `Deployment ID` را کپی کنید
427+
428+
۶. در `UI` دسکتاپ یا اندروید (یا با ویرایش `config.json`) حالت را به `apps_script` برگردانید، `Deployment ID` و `auth_key` را بچسبانید و برنامه را دوباره راه‌اندازی کنید
429+
430+
برای بررسی قابلیت دسترسی قبل از راه‌اندازی پروکسی: دستور `mhrv-rs test-sni` دامنه‌های `*.google.com` را مستقیماً تست می‌کند و فقط به `google_ip` و `front_domain` نیاز دارد.
431+
401432
#### مرحلهٔ ۲ — دانلود برنامه
402433

403434
به [صفحهٔ Releases](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases) بروید و آرشیو مناسب سیستم‌عامل خود را دانلود و از حالت فشرده خارج کنید:

android/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ android {
1414
applicationId = "com.therealaleph.mhrv"
1515
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
1616
targetSdk = 34
17-
versionCode = 115
18-
versionName = "1.1.5"
17+
versionCode = 120
18+
versionName = "1.2.0"
1919

2020
// Ship all four mainstream Android ABIs:
2121
// - arm64-v8a — 95%+ of real-world Android phones since 2019

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,21 @@ enum class SplitMode { ALL, ONLY, EXCEPT }
5959
*/
6060
enum class UiLang { AUTO, FA, EN }
6161

62+
/**
63+
* Operating mode. Mirrors the Rust-side `Mode` enum.
64+
*
65+
* - [APPS_SCRIPT] (default) — full DPI bypass through the user's deployed
66+
* 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).
71+
*/
72+
enum class Mode { APPS_SCRIPT, GOOGLE_ONLY }
73+
6274
data class MhrvConfig(
75+
val mode: Mode = Mode.APPS_SCRIPT,
76+
6377
val listenHost: String = "127.0.0.1",
6478
val listenPort: Int = 8080,
6579
val socks5Port: Int? = 1081,
@@ -130,11 +144,17 @@ data class MhrvConfig(
130144
val obj = JSONObject().apply {
131145
// `mode` is required — without it serde errors with
132146
// "missing field `mode`" and startProxy silently returns 0.
133-
put("mode", "apps_script")
147+
put("mode", when (mode) {
148+
Mode.APPS_SCRIPT -> "apps_script"
149+
Mode.GOOGLE_ONLY -> "google_only"
150+
})
134151
put("listen_host", listenHost)
135152
put("listen_port", listenPort)
136153
socks5Port?.let { put("socks5_port", it) }
137154

155+
// In google_only mode these are unused by the Rust side, but we
156+
// still persist whatever the user typed so flipping back to
157+
// apps_script mode doesn't wipe their settings.
138158
put("script_ids", JSONArray().apply { ids.forEach { put(it) } })
139159
put("auth_key", authKey)
140160

@@ -209,6 +229,10 @@ object ConfigStore {
209229
}?.filter { it.isNotBlank() }.orEmpty()
210230

211231
MhrvConfig(
232+
mode = when (obj.optString("mode", "apps_script")) {
233+
"google_only" -> Mode.GOOGLE_ONLY
234+
else -> Mode.APPS_SCRIPT
235+
},
212236
listenHost = obj.optString("listen_host", "127.0.0.1"),
213237
listenPort = obj.optInt("listen_port", 8080),
214238
socks5Port = obj.optInt("socks5_port", 1081).takeIf { it > 0 },

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.therealaleph.mhrv.CaInstall
3333
import com.therealaleph.mhrv.ConfigStore
3434
import com.therealaleph.mhrv.DEFAULT_SNI_POOL
3535
import com.therealaleph.mhrv.MhrvConfig
36+
import com.therealaleph.mhrv.Mode
3637
import com.therealaleph.mhrv.Native
3738
import com.therealaleph.mhrv.ConnectionMode
3839
import com.therealaleph.mhrv.NetworkDetect
@@ -228,18 +229,28 @@ fun HomeScreen(
228229
.padding(16.dp),
229230
verticalArrangement = Arrangement.spacedBy(12.dp),
230231
) {
232+
SectionHeader("Mode")
233+
ModeDropdown(
234+
mode = cfg.mode,
235+
onChange = { persist(cfg.copy(mode = it)) },
236+
)
237+
238+
Spacer(Modifier.height(4.dp))
231239
SectionHeader(stringResource(R.string.sec_apps_script_relay))
232240

241+
val appsScriptEnabled = cfg.mode == Mode.APPS_SCRIPT
233242
DeploymentIdsField(
234243
urls = cfg.appsScriptUrls,
235244
onChange = { persist(cfg.copy(appsScriptUrls = it)) },
245+
enabled = appsScriptEnabled,
236246
)
237247

238248
OutlinedTextField(
239249
value = cfg.authKey,
240250
onValueChange = { persist(cfg.copy(authKey = it)) },
241251
label = { Text(stringResource(R.string.field_auth_key)) },
242252
singleLine = true,
253+
enabled = appsScriptEnabled,
243254
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
244255
modifier = Modifier.fillMaxWidth(),
245256
supportingText = {
@@ -392,6 +403,7 @@ fun HomeScreen(
392403
}
393404
},
394405
enabled = (isVpnRunning ||
406+
cfg.mode == Mode.GOOGLE_ONLY ||
395407
(cfg.hasDeploymentId && cfg.authKey.isNotBlank())) && !transitionCooldown,
396408
colors = ButtonDefaults.buttonColors(
397409
containerColor = if (isVpnRunning) ErrRed else OkGreen,
@@ -669,6 +681,7 @@ private fun ConnectionModeDropdown(
669681
private fun DeploymentIdsField(
670682
urls: List<String>,
671683
onChange: (List<String>) -> Unit,
684+
enabled: Boolean = true,
672685
) {
673686
// Treat the list as newline-joined text. Keep trailing newlines so the
674687
// cursor behaves naturally while the user is adding a new entry.
@@ -682,6 +695,7 @@ private fun DeploymentIdsField(
682695
onChange(parsed)
683696
},
684697
label = { Text(stringResource(R.string.field_deployment_urls)) },
698+
enabled = enabled,
685699
modifier = Modifier.fillMaxWidth(),
686700
minLines = 2,
687701
maxLines = 6,
@@ -691,6 +705,66 @@ private fun DeploymentIdsField(
691705
)
692706
}
693707

708+
// =========================================================================
709+
// Mode dropdown: apps_script (default) vs google_only (bootstrap).
710+
// =========================================================================
711+
712+
@OptIn(ExperimentalMaterial3Api::class)
713+
@Composable
714+
private fun ModeDropdown(
715+
mode: Mode,
716+
onChange: (Mode) -> Unit,
717+
) {
718+
val labelApps = "Apps Script (full)"
719+
val labelGoogle = "Google-only (bootstrap)"
720+
val currentLabel = when (mode) {
721+
Mode.APPS_SCRIPT -> labelApps
722+
Mode.GOOGLE_ONLY -> labelGoogle
723+
}
724+
var expanded by remember { mutableStateOf(false) }
725+
726+
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
727+
ExposedDropdownMenuBox(
728+
expanded = expanded,
729+
onExpandedChange = { expanded = !expanded },
730+
) {
731+
OutlinedTextField(
732+
value = currentLabel,
733+
onValueChange = {},
734+
readOnly = true,
735+
label = { Text("Mode") },
736+
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
737+
modifier = Modifier.fillMaxWidth().menuAnchor(),
738+
)
739+
ExposedDropdownMenu(
740+
expanded = expanded,
741+
onDismissRequest = { expanded = false },
742+
) {
743+
DropdownMenuItem(
744+
text = { Text(labelApps) },
745+
onClick = { onChange(Mode.APPS_SCRIPT); expanded = false },
746+
)
747+
DropdownMenuItem(
748+
text = { Text(labelGoogle) },
749+
onClick = { onChange(Mode.GOOGLE_ONLY); expanded = false },
750+
)
751+
}
752+
}
753+
754+
val help = when (mode) {
755+
Mode.APPS_SCRIPT ->
756+
"Full DPI bypass through your deployed Apps Script relay."
757+
Mode.GOOGLE_ONLY ->
758+
"Bootstrap: reach *.google.com directly so you can open script.google.com and deploy Code.gs. Non-Google traffic goes direct."
759+
}
760+
Text(
761+
help,
762+
style = MaterialTheme.typography.labelSmall,
763+
color = MaterialTheme.colorScheme.onSurfaceVariant,
764+
)
765+
}
766+
}
767+
694768
// =========================================================================
695769
// SNI pool editor + per-SNI probe.
696770
// =========================================================================

config.google-only.example.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mode": "google_only",
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+
}

docs/changelog/v1.2.0.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!--
2+
Telegram changelog posted automatically by .github/workflows/release.yml
3+
on every tag push. Format:
4+
- Persian changelog first (goes in an HTML <blockquote>)
5+
- A separator line of just `---`
6+
- English changelog second (also in a <blockquote>)
7+
- Leave ASCII `-`, digits, and "issue #NN" refs as-is
8+
9+
The workflow splits on the `---` separator. Bullets can be whatever
10+
you like; the bot forwards verbatim.
11+
-->
12+
13+
• حالت جدید «فقط گوگل» (بوت‌استرپ): دسترسی مستقیم به *.google.com برای استقرار Code.gs وقتی هنوز به script.google.com دسترسی ندارید — بدون نیاز به Deployment ID یا Auth key
14+
15+
• انتخابگر حالت در UI دسکتاپ و اندروید؛ CLI از طریق فیلد `mode` در config
16+
17+
• سازگاری کامل با حالت apps_script؛ کانفیگ‌های موجود بدون تغییر بارگذاری می‌شوند
18+
19+
• نمونه کانفیگ آمادهٔ google_only در ریشهٔ پروژه (`config.google-only.example.json`)
20+
21+
---
22+
• New "Google-only" bootstrap mode: direct SNI-rewrite tunnel to *.google.com so users blocked from script.google.com can still reach it to deploy Code.gs. No Deployment ID or Auth key needed. Non-Google traffic goes direct.
23+
24+
• Mode selector added to the desktop UI and the Android app; CLI picks it up from the `mode` field in config.
25+
26+
• Fully backward compatible with apps_script mode — existing configs load unchanged.
27+
28+
• New ready-to-use `config.google-only.example.json` at the repo root.

0 commit comments

Comments
 (0)