fix: reset stuck start lock on system Always-on VPN restart (#5958)

* fix: reset stuck start lock on system Always-on VPN restart

When Android restarts CoreVpnService for Always-on VPN (via VpnService.SERVICE_INTERFACE
or null intent), a stuck isStartingLock from a killed process prevents tryLockStart() from
succeeding, causing an immediate return without setupVpnService().

Fix: Reset stuck start lock via unlockStart() on system-initiated VPN start/restart,
allowing setupVpnService() to establish the TUN interface. If setup fails during system
start, return START_STICKY to keep the service alive for OS reconnection.

Also accept LOCKED_BOOT_COMPLETED and MY_PACKAGE_REPLACED in BootReceiver.

* Update BootReceiver.kt

* Update CoreVpnService.kt

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
Alexandrand2dust authored and GitHub committed 2026-07-28 21:09:03 +08:00
1 parent 842567fb89
commit 9896dd2974
3 files changed
+32 -7

No files matched your search

+2
View File
@@ -282,6 +282,8 @@
android:label="BootReceiver"> android:label="BootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@@ -3,6 +3,7 @@ package com.v2ray.ang.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.UserManager
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.core.CoreServiceManager import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
@@ -12,18 +13,34 @@ import com.v2ray.ang.util.LogUtil
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
/** /**
* This method is called when the BroadcastReceiver is receiving an Intent broadcast. * This method is called when the BroadcastReceiver is receiving an Intent broadcast.
* It checks if the context is not null and the action is ACTION_BOOT_COMPLETED. * It handles BOOT_COMPLETED, LOCKED_BOOT_COMPLETED, and MY_PACKAGE_REPLACED.
* If the conditions are met, it starts the V2Ray service. * If the conditions are met, it starts the V2Ray service.
* *
* @param context The Context in which the receiver is running. * @param context The Context in which the receiver is running.
* @param intent The Intent being received. * @param intent The Intent being received.
*/ */
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
LogUtil.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}") val action = intent?.action ?: return
if (context == null) return
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) { LogUtil.i(AppConfig.TAG, "BootReceiver received: $action")
LogUtil.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
return when (action) {
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_MY_PACKAGE_REPLACED -> {
// Continue
}
Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
val userManager = context.getSystemService(Context.USER_SERVICE) as? UserManager
if (userManager != null && !userManager.isUserUnlocked) {
LogUtil.w(AppConfig.TAG, "BootReceiver: User is locked, skipping auto start")
return
}
}
else -> {
LogUtil.w(AppConfig.TAG, "BootReceiver: Unhandled action: $action")
return
}
} }
if (!MmkvManager.decodeStartOnBoot()) { if (!MmkvManager.decodeStartOnBoot()) {
@@ -116,20 +116,26 @@ class CoreVpnService : VpnService(), ServiceControl {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Always-on VPN restarts from OS deliver intent.action == SERVICE_INTERFACE or null intent.
// Reset any stuck start lock left by a killed process to allow setupVpnService() to run.
val isSystemVpnStart = intent == null || intent.action == SERVICE_INTERFACE
if (isSystemVpnStart) {
unlockStart()
}
if (!tryLockStart()) { if (!tryLockStart()) {
LogUtil.w(AppConfig.TAG, "StartCore-VPN: Start already in progress") LogUtil.w(AppConfig.TAG, "StartCore-VPN: Start already in progress")
return START_NOT_STICKY return START_NOT_STICKY
} }
LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service command received") LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service command received, systemVpnStart=$isSystemVpnStart")
NotificationManager.showNotification(null) NotificationManager.showNotification(null)
if (!setupVpnService()) { if (!setupVpnService()) {
unlockStart() unlockStart()
// Stop service if setup fails to avoid infinite restart loops (START_STICKY)
stopSelf() stopSelf()
return START_NOT_STICKY return START_NOT_STICKY
} }
startService() startService()
return START_STICKY return START_STICKY
//return super.onStartCommand(intent, flags, startId)
} }
override fun getService(): Service { override fun getService(): Service {