Skip to content

Commit e48a8f6

Browse files
committed
v1.2.2: Android Start crash fix + google_ip preservation + chromewebstore SNI
Three user-facing fixes: - Android Start crash in google_only mode (#73): every early-return path in startEverything now satisfies Android 8+'s foreground-service contract by calling startForeground before stopSelf. Previously if you opened the app, selected google_only mode, and tapped Connect without filling deployment ID + auth key (which google_only doesn't need anyway), the service crashed with ForegroundServiceDidNotStartInTimeException. Also gated the deployment-ID requirement on mode == APPS_SCRIPT. - google_ip auto-overwrite on Start (#71): some carriers serve poisoned DNS for www.google.com that resolves but refuses TLS, clobbering working IPs users had manually set. DNS lookup now only fires when the field is blank — manual configs are preserved across Connect. Explicit "Auto-detect" button still refreshes on demand. - chromewebstore.google.com added to DEFAULT_GOOGLE_SNI_POOL and DEFAULT_SNI_POOL (#75). Same family as the rest of the pool — wildcard cert, GFE-hosted.
1 parent 9ff887a commit e48a8f6

8 files changed

Lines changed: 72 additions & 16 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.2.1"
3+
version = "1.2.2"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"

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 = 121
18-
versionName = "1.2.1"
17+
versionCode = 122
18+
versionName = "1.2.2"
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,6 @@ val DEFAULT_SNI_POOL: List<String> = listOf(
299299
"translate.google.com",
300300
"play.google.com",
301301
"lens.google.com",
302+
// Issue #75.
303+
"chromewebstore.google.com",
302304
)

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,30 @@ class MhrvVpnService : VpnService() {
8383
Native.setDataDir(filesDir.absolutePath)
8484

8585
val cfg = ConfigStore.load(this)
86-
if (!cfg.hasDeploymentId || cfg.authKey.isBlank()) {
87-
Log.e(TAG, "Config is incomplete — can't start proxy")
86+
87+
// Android 8+ requires every service started via
88+
// `startForegroundService()` to call `startForeground()` within a
89+
// short window or the system crashes the app with
90+
// `ForegroundServiceDidNotStartInTimeException`. Every `stopSelf()`
91+
// path below MUST therefore happen after a `startForeground()`
92+
// call — otherwise the user-visible symptom is "the app crashes
93+
// the instant I tap Start". See issue #73: user configured
94+
// google_only mode (no deployment ID needed), which tripped the
95+
// old early-return-before-startForeground branch.
96+
//
97+
// We call startForeground immediately here with the notification
98+
// used by the normal running state; if we bail out below, we
99+
// tear the foreground service down in an orderly way.
100+
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
101+
102+
// Deployment ID + auth key are only required in apps_script mode.
103+
// google_only mode (bootstrap / Telegram-only use cases) runs
104+
// with neither. Closes #73 regression where google_only users
105+
// hit this branch and crashed on startForeground timeout.
106+
val needsAppsScriptCreds = cfg.mode == Mode.APPS_SCRIPT
107+
if (needsAppsScriptCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
108+
Log.e(TAG, "Config is incomplete — can't start proxy in apps_script mode")
109+
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
88110
stopSelf()
89111
return
90112
}
@@ -104,6 +126,7 @@ class MhrvVpnService : VpnService() {
104126
proxyHandle = Native.startProxy(cfg.toJson())
105127
if (proxyHandle == 0L) {
106128
Log.e(TAG, "Native.startProxy returned 0 — see logcat tag mhrv_rs")
129+
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
107130
stopSelf()
108131
return
109132
}
@@ -115,12 +138,11 @@ class MhrvVpnService : VpnService() {
115138
// another VPN app already owns the system VPN slot, the user
116139
// wants per-app opt-in via Wi-Fi proxy settings, or the device
117140
// is a sandboxed/rooted setup where VpnService is unwelcome.
118-
// We still run as a foreground service (required for the native
119-
// listener thread to survive backgrounding), we just skip every
120-
// VPN-specific step below. Issue #37.
141+
// We already called startForeground() at the top of this method,
142+
// which is all PROXY_ONLY needs for the listener thread to survive
143+
// backgrounding. Issue #37.
121144
if (cfg.connectionMode == ConnectionMode.PROXY_ONLY) {
122145
Log.i(TAG, "PROXY_ONLY mode: listeners up, skipping VpnService/TUN")
123-
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
124146
VpnState.setRunning(true)
125147
return
126148
}
@@ -202,6 +224,7 @@ class MhrvVpnService : VpnService() {
202224
Log.e(TAG, "establish() returned null — is VPN permission granted?")
203225
Native.stopProxy(proxyHandle)
204226
proxyHandle = 0L
227+
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
205228
stopSelf()
206229
return
207230
}
@@ -232,7 +255,10 @@ class MhrvVpnService : VpnService() {
232255
}
233256
}, "tun2proxy").apply { start() }
234257

235-
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
258+
// (startForeground was already called at the top of this method
259+
// to satisfy Android 8+'s foreground-service contract — see the
260+
// comment at the start of startEverything. Calling it here again
261+
// would be a no-op but wasteful.)
236262

237263
// Publish "running" state for the UI's Connect/Disconnect button
238264
// to observe. Only flipped true once everything above succeeded —

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,26 @@ fun HomeScreen(
385385
// so a subsequent field edit can't overwrite the
386386
// fresh values with pre-resolve ones.
387387
scope.launch {
388-
val fresh = withContext(Dispatchers.IO) {
389-
NetworkDetect.resolveGoogleIp()
390-
}
388+
// Only auto-fill google_ip if it's empty.
389+
// Issue #71: some Iranian ISPs return
390+
// poisoned A records for www.google.com that
391+
// resolve but then refuse TLS (or route to a
392+
// Google IP that's not on the GFE and can't
393+
// handle our SNI-rewrite). If the user has
394+
// manually set a working IP
395+
// (e.g. 216.239.38.120), we must NOT
396+
// overwrite it with a poisoned fresh lookup
397+
// just because the two values differ. They
398+
// can still force a re-resolve via the
399+
// explicit "Auto-detect" button above.
391400
var updated = cfg
392-
if (!fresh.isNullOrBlank() && fresh != updated.googleIp) {
393-
updated = updated.copy(googleIp = fresh)
401+
if (updated.googleIp.isBlank()) {
402+
val fresh = withContext(Dispatchers.IO) {
403+
NetworkDetect.resolveGoogleIp()
404+
}
405+
if (!fresh.isNullOrBlank()) {
406+
updated = updated.copy(googleIp = fresh)
407+
}
394408
}
395409
if (updated.frontDomain.isBlank() ||
396410
updated.frontDomain.parseAsIpOrNull() != null

docs/changelog/v1.2.2.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
2+
• رفع کرش اندروید هنگام Start وقتی کاربر حالت google_only را با فیلدهای خالی deployment استفاده می‌کرد: همهٔ مسیرهای early-return اکنون قبل از stopSelf تابع startForeground را صدا می‌کنند تا قرارداد foreground-service اندروید ۸+ شکسته نشود (issue #73)
3+
• رفع جایگزین‌شدن خودکار google_ip: حالا فقط وقتی فیلد خالی است، DNS lookup انجام می‌شود. اگر دستی یک IP کارا تنظیم کرده‌اید، دیگر با Start روی آن نوشته نمی‌شود (issue #71)
4+
• افزودن chromewebstore.google.com به پول SNI (issue #75)
5+
• رفع طول Content-Length در پاسخ ۵۰۲ حالت google_only برای HTTP ساده (PR #70)
6+
---
7+
• Fix Android crash on Start when the user picks google_only mode with empty deployment fields: every early-return path now calls startForeground before stopSelf so we don't violate Android 8+'s foreground-service contract (issue #73)
8+
• Fix google_ip auto-overwrite: DNS lookup only fires when the field is blank. If you manually set a working IP, Start no longer clobbers it on every launch (issue #71)
9+
• Add chromewebstore.google.com to the SNI pool (issue #75)
10+
• Fix Content-Length in the google_only plain-HTTP 502 response (PR #70)

src/domain_fronter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,10 @@ pub const DEFAULT_GOOGLE_SNI_POOL: &[&str] = &[
11061106
"translate.google.com",
11071107
"play.google.com",
11081108
"lens.google.com",
1109+
// chromewebstore.google.com — reported in issue #75 as a working
1110+
// SNI. Same family as the rest: wildcard cert, GFE-hosted,
1111+
// handshake against google_ip:443 with no content negotiation.
1112+
"chromewebstore.google.com",
11091113
];
11101114

11111115
/// Build the pool of SNI hosts used for outbound connections to the Google

0 commit comments

Comments
 (0)