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.
This commit is contained in:
therealaleph
2026-04-23 19:49:40 +03:00
parent 9ff887abaa
commit e48a8f6add
8 changed files with 72 additions and 16 deletions
Generated
+1 -1
View File
@@ -2186,7 +2186,7 @@ dependencies = [
[[package]]
name = "mhrv-rs"
version = "1.2.1"
version = "1.2.2"
dependencies = [
"base64 0.22.1",
"bytes",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mhrv-rs"
version = "1.2.1"
version = "1.2.2"
edition = "2021"
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
license = "MIT"
+2 -2
View File
@@ -14,8 +14,8 @@ android {
applicationId = "com.therealaleph.mhrv"
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
targetSdk = 34
versionCode = 121
versionName = "1.2.1"
versionCode = 122
versionName = "1.2.2"
// Ship all four mainstream Android ABIs:
// - arm64-v8a — 95%+ of real-world Android phones since 2019
@@ -299,4 +299,6 @@ val DEFAULT_SNI_POOL: List<String> = listOf(
"translate.google.com",
"play.google.com",
"lens.google.com",
// Issue #75.
"chromewebstore.google.com",
)
@@ -83,8 +83,30 @@ class MhrvVpnService : VpnService() {
Native.setDataDir(filesDir.absolutePath)
val cfg = ConfigStore.load(this)
if (!cfg.hasDeploymentId || cfg.authKey.isBlank()) {
Log.e(TAG, "Config is incomplete — can't start proxy")
// Android 8+ requires every service started via
// `startForegroundService()` to call `startForeground()` within a
// short window or the system crashes the app with
// `ForegroundServiceDidNotStartInTimeException`. Every `stopSelf()`
// path below MUST therefore happen after a `startForeground()`
// call — otherwise the user-visible symptom is "the app crashes
// the instant I tap Start". See issue #73: user configured
// google_only mode (no deployment ID needed), which tripped the
// old early-return-before-startForeground branch.
//
// We call startForeground immediately here with the notification
// used by the normal running state; if we bail out below, we
// tear the foreground service down in an orderly way.
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
// Deployment ID + auth key are only required in apps_script mode.
// google_only mode (bootstrap / Telegram-only use cases) runs
// with neither. Closes #73 regression where google_only users
// hit this branch and crashed on startForeground timeout.
val needsAppsScriptCreds = cfg.mode == Mode.APPS_SCRIPT
if (needsAppsScriptCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
Log.e(TAG, "Config is incomplete — can't start proxy in apps_script mode")
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
stopSelf()
return
}
@@ -104,6 +126,7 @@ class MhrvVpnService : VpnService() {
proxyHandle = Native.startProxy(cfg.toJson())
if (proxyHandle == 0L) {
Log.e(TAG, "Native.startProxy returned 0 — see logcat tag mhrv_rs")
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
stopSelf()
return
}
@@ -115,12 +138,11 @@ class MhrvVpnService : VpnService() {
// another VPN app already owns the system VPN slot, the user
// wants per-app opt-in via Wi-Fi proxy settings, or the device
// is a sandboxed/rooted setup where VpnService is unwelcome.
// We still run as a foreground service (required for the native
// listener thread to survive backgrounding), we just skip every
// VPN-specific step below. Issue #37.
// We already called startForeground() at the top of this method,
// which is all PROXY_ONLY needs for the listener thread to survive
// backgrounding. Issue #37.
if (cfg.connectionMode == ConnectionMode.PROXY_ONLY) {
Log.i(TAG, "PROXY_ONLY mode: listeners up, skipping VpnService/TUN")
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
VpnState.setRunning(true)
return
}
@@ -202,6 +224,7 @@ class MhrvVpnService : VpnService() {
Log.e(TAG, "establish() returned null — is VPN permission granted?")
Native.stopProxy(proxyHandle)
proxyHandle = 0L
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
stopSelf()
return
}
@@ -232,7 +255,10 @@ class MhrvVpnService : VpnService() {
}
}, "tun2proxy").apply { start() }
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
// (startForeground was already called at the top of this method
// to satisfy Android 8+'s foreground-service contract — see the
// comment at the start of startEverything. Calling it here again
// would be a no-op but wasteful.)
// Publish "running" state for the UI's Connect/Disconnect button
// to observe. Only flipped true once everything above succeeded —
@@ -385,12 +385,26 @@ fun HomeScreen(
// so a subsequent field edit can't overwrite the
// fresh values with pre-resolve ones.
scope.launch {
val fresh = withContext(Dispatchers.IO) {
NetworkDetect.resolveGoogleIp()
}
// Only auto-fill google_ip if it's empty.
// Issue #71: some Iranian ISPs return
// poisoned A records for www.google.com that
// resolve but then refuse TLS (or route to a
// Google IP that's not on the GFE and can't
// handle our SNI-rewrite). If the user has
// manually set a working IP
// (e.g. 216.239.38.120), we must NOT
// overwrite it with a poisoned fresh lookup
// just because the two values differ. They
// can still force a re-resolve via the
// explicit "Auto-detect" button above.
var updated = cfg
if (!fresh.isNullOrBlank() && fresh != updated.googleIp) {
updated = updated.copy(googleIp = fresh)
if (updated.googleIp.isBlank()) {
val fresh = withContext(Dispatchers.IO) {
NetworkDetect.resolveGoogleIp()
}
if (!fresh.isNullOrBlank()) {
updated = updated.copy(googleIp = fresh)
}
}
if (updated.frontDomain.isBlank() ||
updated.frontDomain.parseAsIpOrNull() != null
+10
View File
@@ -0,0 +1,10 @@
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
• رفع کرش اندروید هنگام Start وقتی کاربر حالت google_only را با فیلدهای خالی deployment استفاده می‌کرد: همهٔ مسیرهای early-return اکنون قبل از stopSelf تابع startForeground را صدا می‌کنند تا قرارداد foreground-service اندروید ۸+ شکسته نشود (issue #73)
• رفع جایگزین‌شدن خودکار google_ip: حالا فقط وقتی فیلد خالی است، DNS lookup انجام می‌شود. اگر دستی یک IP کارا تنظیم کرده‌اید، دیگر با Start روی آن نوشته نمی‌شود (issue #71)
• افزودن chromewebstore.google.com به پول SNI (issue #75)
• رفع طول Content-Length در پاسخ ۵۰۲ حالت google_only برای HTTP ساده (PR #70)
---
• 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)
• 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)
• Add chromewebstore.google.com to the SNI pool (issue #75)
• Fix Content-Length in the google_only plain-HTTP 502 response (PR #70)
+4
View File
@@ -1106,6 +1106,10 @@ pub const DEFAULT_GOOGLE_SNI_POOL: &[&str] = &[
"translate.google.com",
"play.google.com",
"lens.google.com",
// chromewebstore.google.com — reported in issue #75 as a working
// SNI. Same family as the rest: wildcard cert, GFE-hosted,
// handshake against google_ip:443 with no content negotiation.
"chromewebstore.google.com",
];
/// Build the pool of SNI hosts used for outbound connections to the Google