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
@@ -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 —