mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 06:34:41 +03:00
v1.0.2: stable release signature, idempotent Stop, top-level Settings for CA install (#33)
Three fixes + one behaviour change from v1.0.1 reports. APK signature is now stable (release.jks committed) ---------------------------------------------------- v1.0.0 and v1.0.1 signed release APKs with Gradle's auto-generated debug keystore, which is randomly generated per machine and per CI runner. Result: every upgrade failed with INSTALL_FAILED_UPDATE_INCOMPATIBLE and users had to uninstall first. Unfixable without a stable key. android/app/release.jks now holds that key, committed to the repo with the password in plaintext in build.gradle.kts. This is fine for a FOSS sideload project without a Play Store identity — the trust model is "trust the source tree you pulled from," not "trust the key we hold." Anyone forking and shipping a rebranded build should generate their own key. One-time cost: v1.0.1 → v1.0.2 STILL requires uninstall, because we're switching signature keys. Every upgrade from v1.0.2 onward is clean. Stop no longer (sometimes) closes the app ----------------------------------------- teardown() is reachable from three paths on two threads: 1. ACTION_STOP onStartCommand branch (mhrv-teardown worker) 2. onDestroy after stopSelf (main thread) 3. VpnService revocation out-of-band (main thread) Running the full native cleanup sequence twice races the two threads through Tun2proxy.stop() → fd.close() → Native.stopProxy(handle) on state that's already been nullified — SIGSEGV source, user-visible as "tap Stop, app disappears." New AtomicBoolean `tornDown` gates entry: first caller wins, every subsequent caller logs "teardown: already done" and returns. onDestroy also wraps the call in try/catch — crashing out of onDestroy takes the whole process with it, which is exactly the bug we're trying to fix. Smoke-tested on emulator: teardown now logs teardown: begin caller=mhrv-teardown ... clean sequence ... teardown: done onDestroy entered teardown: already done, skipping (caller=main) onDestroy done with PID unchanged throughout. CA install now routes to the Settings search -------------------------------------------- Old flow: `Settings.ACTION_SECURITY_SETTINGS` deep-link, then walk "Encryption & credentials → Install a certificate → CA certificate". That path varies wildly between OEMs (Samsung buries it under "Biometrics and security → Other security settings"; Xiaomi under "Passwords & Security → Privacy"; Pixel splits it between "More security settings" and "Privacy controls" depending on Android version). Users got lost. New flow: open the top-level Settings app (`Settings.ACTION_SETTINGS`) and instruct the user to use the Settings search bar to find "CA certificate". Search is consistent across OEMs and Android versions; the menu paths are not. Dialog, snackbar, and `docs/android.md` copy all updated to match. Version bump: 1.0.1 → 1.0.2 (versionCode 101 → 102). releases/mhrv-rs-android-universal-v1.0.1.apk replaced with the v1.0.2 build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
b734f41faa
commit
64409f6b41
@@ -36,6 +36,18 @@ class MhrvVpnService : VpnService() {
|
||||
private var tun2proxyThread: Thread? = null
|
||||
private val tun2proxyRunning = AtomicBoolean(false)
|
||||
|
||||
// Idempotency guard. teardown() is reachable from three paths:
|
||||
// 1. ACTION_STOP onStartCommand branch (background thread)
|
||||
// 2. onDestroy() (main thread, fires whenever stopSelf resolves
|
||||
// OR Android decides to kill the service)
|
||||
// 3. Android revoking the VPN profile out-of-band (also onDestroy)
|
||||
// Running the full native cleanup sequence twice races two threads
|
||||
// through Tun2proxy.stop(), fd.close(), Native.stopProxy() on state
|
||||
// that's already been nullified — the second pass was the
|
||||
// SIGSEGV-or-zombie source. This flag makes the second call a
|
||||
// no-op.
|
||||
private val tornDown = AtomicBoolean(false)
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.i(TAG, "onStartCommand action=${intent?.action ?: "<null>"} startId=$startId")
|
||||
return when (intent?.action) {
|
||||
@@ -187,7 +199,21 @@ class MhrvVpnService : VpnService() {
|
||||
* 4. Shut down the Rust proxy runtime (nothing left to forward to).
|
||||
*/
|
||||
private fun teardown() {
|
||||
Log.i(TAG, "teardown: begin (tun2proxy running=${tun2proxyRunning.get()}, proxyHandle=$proxyHandle)")
|
||||
// Idempotency guard. Without this, onDestroy racing the
|
||||
// ACTION_STOP background thread has been observed to crash the
|
||||
// process — two threads into Tun2proxy.stop() and
|
||||
// Native.stopProxy(handle) where handle has already been zeroed
|
||||
// is a SIGSEGV waiting to happen. First caller wins, subsequent
|
||||
// callers return immediately.
|
||||
if (!tornDown.compareAndSet(false, true)) {
|
||||
Log.i(TAG, "teardown: already done, skipping (caller=${Thread.currentThread().name})")
|
||||
return
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"teardown: begin caller=${Thread.currentThread().name} " +
|
||||
"(tun2proxy running=${tun2proxyRunning.get()}, proxyHandle=$proxyHandle)",
|
||||
)
|
||||
|
||||
// 1. Cooperative stop signal.
|
||||
if (tun2proxyRunning.get()) {
|
||||
@@ -200,7 +226,9 @@ class MhrvVpnService : VpnService() {
|
||||
// ParcelFileDescriptor no longer owns the fd and close() here
|
||||
// is a no-op; the real fd is owned by tun2proxy (closeFdOnDrop
|
||||
// = true), which closes it on return from run().
|
||||
try { tun?.close() } catch (_: Throwable) {}
|
||||
try { tun?.close() } catch (t: Throwable) {
|
||||
Log.w(TAG, "tun.close: ${t.message}")
|
||||
}
|
||||
tun = null
|
||||
|
||||
// 3. Join the worker. 4s is enough in the happy case; if tun2proxy
|
||||
@@ -219,19 +247,31 @@ class MhrvVpnService : VpnService() {
|
||||
// on the Rust side, so this is bounded even if the runtime
|
||||
// has in-flight tasks (common when the Apps Script relay has
|
||||
// piled up pending 30s timeouts).
|
||||
if (proxyHandle != 0L) {
|
||||
Log.i(TAG, "teardown: stopping proxy handle=$proxyHandle")
|
||||
try { Native.stopProxy(proxyHandle) } catch (t: Throwable) {
|
||||
val handle = proxyHandle
|
||||
proxyHandle = 0L
|
||||
if (handle != 0L) {
|
||||
Log.i(TAG, "teardown: stopping proxy handle=$handle")
|
||||
try { Native.stopProxy(handle) } catch (t: Throwable) {
|
||||
Log.e(TAG, "Native.stopProxy threw: ${t.message}", t)
|
||||
}
|
||||
proxyHandle = 0L
|
||||
}
|
||||
Log.i(TAG, "teardown: done")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
teardown()
|
||||
Log.i(TAG, "onDestroy entered")
|
||||
try {
|
||||
teardown()
|
||||
} catch (t: Throwable) {
|
||||
// Belt-and-suspenders. Crashing out of onDestroy takes the
|
||||
// whole process with it — user-visible as the app closing
|
||||
// right when they tap Stop, which is exactly the symptom we
|
||||
// are trying to fix. Anything that gets here is logged and
|
||||
// swallowed.
|
||||
Log.e(TAG, "onDestroy teardown threw: ${t.message}", t)
|
||||
}
|
||||
super.onDestroy()
|
||||
Log.i(TAG, "onDestroy done")
|
||||
}
|
||||
|
||||
private fun buildNotif(proxyPort: Int): Notification {
|
||||
|
||||
Reference in New Issue
Block a user