From 130ef853742854b6a3a880cc16246bd4330c5c59 Mon Sep 17 00:00:00 2001 From: Farnam Taheri Date: Mon, 11 May 2026 20:52:24 +0330 Subject: [PATCH 1/2] fix(android): start vpn via foreground service path Use ContextCompat.startForegroundService for connect requests and promote the VpnService immediately when handling ACTION_CONNECT, matching Android foreground-service requirements before the longer profile/core startup work begins. Bound the partial wake lock to the existing long SOCKS startup window plus a small grace period so setup work is protected without holding an indefinite wake lock. --- .../java/com/masterdns/vpn/service/MasterDnsVpnService.kt | 6 +++--- .../app/src/main/java/com/masterdns/vpn/util/VpnManager.kt | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt b/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt index 7fe92e6..7f4b796 100644 --- a/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt +++ b/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt @@ -41,6 +41,7 @@ class MasterDnsVpnService : VpnService() { private const val DEFAULT_SOCKS_PORT = 18000 private const val SOCKS_STARTUP_TIMEOUT_MS = 30 * 60 * 1000L private const val SOCKS_POLL_INTERVAL_MS = 500L + private const val WAKE_LOCK_TIMEOUT_MS = SOCKS_STARTUP_TIMEOUT_MS + 5 * 60 * 1000L // Base companions many apps need for network functionality. private val BASE_COMPANION_PACKAGES = setOf( @@ -90,6 +91,7 @@ class MasterDnsVpnService : VpnService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { ACTION_CONNECT -> { + startForeground(NOTIFICATION_ID, buildNotification(getString(R.string.notification_connecting))) val profileId = intent.getLongExtra(EXTRA_PROFILE_ID, -1) if (profileId > 0) { startVpn(profileId) @@ -111,8 +113,6 @@ class MasterDnsVpnService : VpnService() { socksAuthWarningShown = false sessionBusyWarningShown = false - // Show foreground notification - startForeground(NOTIFICATION_ID, buildNotification(getString(R.string.notification_connecting))) acquireWakeLock() // Load profile from DB @@ -712,7 +712,7 @@ class MasterDnsVpnService : VpnService() { val pm = getSystemService(POWER_SERVICE) as? PowerManager ?: return wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$TAG:runtime").apply { setReferenceCounted(false) - acquire() + acquire(WAKE_LOCK_TIMEOUT_MS) } } diff --git a/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt b/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt index 2b7e116..bf6c4e3 100644 --- a/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt +++ b/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt @@ -3,6 +3,7 @@ package com.masterdns.vpn.util import android.content.Context import android.content.Intent import android.net.TrafficStats +import androidx.core.content.ContextCompat import com.masterdns.vpn.data.local.ProfileEntity import com.masterdns.vpn.service.MasterDnsVpnService import kotlinx.coroutines.CoroutineScope @@ -179,7 +180,7 @@ object VpnManager { putExtra(MasterDnsVpnService.EXTRA_PROFILE_ID, profile.id) } - runCatching { context.startService(intent) } + runCatching { ContextCompat.startForegroundService(context, intent) } .onFailure { setError("Failed to start VPN service: ${it.message}") appendLog("Failed to start VPN service: ${it.message}") From 81f834c4a3470da547c40690054a46fd110c938f Mon Sep 17 00:00:00 2001 From: Farnam Taheri Date: Mon, 11 May 2026 21:08:14 +0330 Subject: [PATCH 2/2] fix(android): add vpn shutdown breadcrumbs Add structured Android-side logs around VPN teardown so support reports can show TUN close, DNS-aware TUN bridge stop, Go core stop, Android job cleanup, and foreground notification cleanup. Also log that the current Android VPN route is IPv4-only, making the existing behavior explicit without changing core routing or mobile bridge behavior. --- .../masterdns/vpn/service/MasterDnsVpnService.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt b/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt index 7f4b796..ff2d55b 100644 --- a/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt +++ b/android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt @@ -281,6 +281,7 @@ class MasterDnsVpnService : VpnService() { .setMtu(1500) .addAddress(if (globalSettings.fakeDnsEnabled) "172.19.0.1" else "10.0.0.2", if (globalSettings.fakeDnsEnabled) 30 else 32) .addRoute("0.0.0.0", 0) + VpnManager.appendLog("IPv4 route enabled: 0.0.0.0/0. IPv6 is not routed by this Android VPN path.") vpnDnsServers.forEach { builder.addDnsServer(it) } if (globalSettings.fakeDnsEnabled) { builder.addRoute("198.18.0.0", 16) @@ -382,29 +383,39 @@ class MasterDnsVpnService : VpnService() { serviceScope.launch { try { connectJob?.cancel() - VpnManager.appendLog("Stopping VPN...") + VpnManager.appendLog("VPN stop requested") // Close TUN interface FIRST to unblock any pending I/O in Go tunBridge/tun + VpnManager.appendLog("Closing TUN interface...") runCatching { vpnInterface?.close() } + .onSuccess { VpnManager.appendLog("TUN interface closed") } + .onFailure { VpnManager.appendLog("TUN interface close failed: ${it.message}") } vpnInterface = null if (tunBridgeActive) { + VpnManager.appendLog("Stopping DNS-aware TUN bridge...") runCatching { mobile.Mobile.stopTunBridge() } + .onSuccess { VpnManager.appendLog("DNS-aware TUN bridge stopped") } + .onFailure { VpnManager.appendLog("DNS-aware TUN bridge stop failed: ${it.message}") } tunBridgeActive = false } // Stop Go client and Tun bridge + VpnManager.appendLog("Stopping Go core...") runCatching { if (mobile.Mobile.isRunning()) { mobile.Mobile.stopClient() + VpnManager.appendLog("Go core stop requested") } else { VpnManager.appendLog("Go core already stopped") } }.onFailure { e -> Log.e(TAG, "Error stopping Go core", e) + VpnManager.appendLog("Go core stop failed: ${e.message}") } // Cancel coroutines + VpnManager.appendLog("Stopping Android session jobs...") goClientJob?.cancel() httpProxyJob?.cancel() sharingSocksJob?.cancel() @@ -413,6 +424,7 @@ class MasterDnsVpnService : VpnService() { sharingSocksServer = null runCatching { sharingHttpServer?.close() } sharingHttpServer = null + VpnManager.appendLog("Android session jobs stopped") VpnManager.updateState(VpnManager.VpnState.DISCONNECTED) VpnManager.stopTrafficMonitor() @@ -427,8 +439,10 @@ class MasterDnsVpnService : VpnService() { @Suppress("DEPRECATION") stopForeground(true) } + VpnManager.appendLog("Foreground notification stopped") }.onFailure { Log.w(TAG, "Failed to stop foreground cleanly", it) + VpnManager.appendLog("Foreground notification stop failed: ${it.message}") } // Delay to allow UI to update before stopping service