Skip to content

Commit 3ffb861

Browse files
authored
fix(vpn): retain stopJob and reset isStopping synchronously (#44)
Plan 014. Previously stopVpn() used a local CoroutineScope whose Job was unreachable from the service fields; a low-memory process pause between state=DISCONNECTED and stopSelf() could GC the cleanup coroutine mid- teardown, leaving Go core / TUN fd / WakeLock / NetworkCallback live. isStopping also stayed true until onDestroy(), blocking a fresh connect() during that gap. Fix: retain stopJob on the service so onDestroy() can join it, reset isStopping in a finally block inside the launch lambda so it goes false when teardown actually finishes, and add a DISCONNECTING guard to VpnManager.connect() so the UI can't race the disconnect dispatch.
1 parent 3c38d4f commit 3ffb861

2 files changed

Lines changed: 17 additions & 7 deletions

File tree

android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class MasterDnsVpnService : VpnService() {
6565

6666
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
6767
private var connectJob: Job? = null
68+
private var stopJob: Job? = null
6869
private var vpnInterface: ParcelFileDescriptor? = null
6970
private var goClientJob: Job? = null
7071
private var httpProxyJob: Job? = null
@@ -465,10 +466,10 @@ class MasterDnsVpnService : VpnService() {
465466
if (isStopping) return
466467
isStopping = true
467468

468-
// Use a separate scope so that serviceScope.cancel() in onDestroy()
469-
// does not kill this coroutine mid-cleanup.
470-
val stopScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
471-
stopScope.launch {
469+
// plan 014: retain the cleanup Job in stopJob so a process pause/resume
470+
// can't GC it mid-teardown; onDestroy() joins it before cancelling scope.
471+
if (stopJob?.isActive == true) return
472+
stopJob = serviceScope.launch {
472473
try {
473474
connectJob?.cancel()
474475
VpnManager.appendLog("VPN stop requested")
@@ -550,9 +551,10 @@ class MasterDnsVpnService : VpnService() {
550551
VpnManager.updateState(VpnManager.VpnState.DISCONNECTED)
551552
VpnManager.stopTrafficMonitor()
552553
runCatching { stopSelf() }
554+
} finally {
555+
// plan 014: reset synchronously so connect() doesn't race stopSelf()
556+
isStopping = false
553557
}
554-
// NOTE: isStopping intentionally stays true until onDestroy() completes.
555-
// This prevents onDestroy() from double-closing already-freed resources.
556558
}
557559
}
558560

@@ -573,6 +575,12 @@ class MasterDnsVpnService : VpnService() {
573575
}
574576

575577
override fun onDestroy() {
578+
// plan 014: let any in-flight stopVpn() finish before cancelling serviceScope.
579+
val inFlightStop = stopJob
580+
if (inFlightStop != null && inFlightStop.isActive) {
581+
kotlinx.coroutines.runBlocking { inFlightStop.join() }
582+
}
583+
576584
// Normal path: stopVpn() already ran — Go layer guards make re-calls no-ops.
577585
// Force-kill path: stopVpn() was never called, so do full cleanup.
578586
if (!isStopping) {

android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,9 @@ object VpnManager {
314314
* Start the VPN service.
315315
*/
316316
fun connect(context: Context, profile: ProfileEntity) {
317-
if (_state.value == VpnState.CONNECTED || _state.value == VpnState.CONNECTING) return
317+
if (_state.value == VpnState.CONNECTED ||
318+
_state.value == VpnState.CONNECTING ||
319+
_state.value == VpnState.DISCONNECTING) return
318320

319321
updateState(VpnState.CONNECTING)
320322
clearError()

0 commit comments

Comments
 (0)