Skip to content

Commit 5f346ab

Browse files
committed
Improve other-VPN interaction error checks
1 parent 56f5aaf commit 5f346ab

3 files changed

Lines changed: 27 additions & 25 deletions

File tree

app/src/main/java/tech/httptoolkit/android/Constants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ object IntentExtras {
77
const val PROXY_CONFIG_EXTRA = "tech.httptoolkit.android.PROXY_CONFIG"
88
const val UNINTERCEPTED_APPS_EXTRA = "tech.httptoolkit.android.UNINTERCEPTED_APPS"
99
const val INTERCEPTED_PORTS_EXTRA = "tech.httptoolkit.android.INTERCEPTED_PORTS"
10+
const val VPN_FAILED_EXTRA = "tech.httptoolkit.android.VPN_FAILED"
1011
}
1112

1213
object Constants {

app/src/main/java/tech/httptoolkit/android/ProxyVpnService.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.net.ProxyInfo
88
import android.net.VpnService
99
import android.os.Build
1010
import android.os.ParcelFileDescriptor
11+
import android.os.SystemClock
1112
import android.util.Log
1213
import androidx.core.app.NotificationCompat
1314
import androidx.localbroadcastmanager.content.LocalBroadcastManager
@@ -29,6 +30,10 @@ const val STOP_VPN_ACTION = "tech.httptoolkit.android.STOP_VPN_ACTION"
2930
const val VPN_STARTED_BROADCAST = "tech.httptoolkit.android.VPN_STARTED_BROADCAST"
3031
const val VPN_STOPPED_BROADCAST = "tech.httptoolkit.android.VPN_STOPPED_BROADCAST"
3132

33+
// If our VPN is revoked this quickly after starting, we assume another VPN took it from us,
34+
// rather than the user having deliberately turned us off:
35+
private const val VPN_TAKEOVER_THRESHOLD_MS = 1000
36+
3237
private var currentService: ProxyVpnService? = null
3338
fun isVpnActive(): Boolean {
3439
return if (currentService == null)
@@ -51,6 +56,8 @@ class ProxyVpnService : VpnService(), IProtectSocket {
5156
private set
5257

5358
private var vpnInterface: ParcelFileDescriptor? = null
59+
60+
private var vpnStartedAt = 0L
5461
private var vpnRunnable: ProxyVpnRunnable? = null
5562

5663
override fun onCreate() {
@@ -89,7 +96,7 @@ class ProxyVpnService : VpnService(), IProtectSocket {
8996
return Service.START_REDELIVER_INTENT
9097
} else {
9198
// We failed to start somehow - cleanup
92-
stopVpn()
99+
stopVpn(failed = true)
93100
}
94101
} else if (intent.action == STOP_VPN_ACTION) {
95102
stopVpn()
@@ -103,7 +110,8 @@ class ProxyVpnService : VpnService(), IProtectSocket {
103110
override fun onRevoke() {
104111
super.onRevoke()
105112
Log.i(TAG, "onRevoke called")
106-
stopVpn()
113+
val runtime = SystemClock.elapsedRealtime() - vpnStartedAt
114+
stopVpn(failed = runtime < VPN_TAKEOVER_THRESHOLD_MS)
107115
}
108116

109117
private fun showServiceNotification() {
@@ -232,6 +240,7 @@ class ProxyVpnService : VpnService(), IProtectSocket {
232240
return false
233241
} else {
234242
this.vpnInterface = vpnInterface
243+
this.vpnStartedAt = SystemClock.elapsedRealtime()
235244
}
236245

237246
app.lastProxy = proxyConfig
@@ -281,8 +290,8 @@ class ProxyVpnService : VpnService(), IProtectSocket {
281290
return startVpn(proxyConfig, uninterceptedApps, interceptedPorts)
282291
}
283292

284-
private fun stopVpn() {
285-
Log.i(TAG, "VPN stopping...")
293+
private fun stopVpn(failed: Boolean = false) {
294+
Log.i(TAG, if (failed) "VPN stopping after failure..." else "VPN stopping...")
286295

287296
if (vpnRunnable != null) {
288297
vpnRunnable!!.stop()
@@ -297,7 +306,9 @@ class ProxyVpnService : VpnService(), IProtectSocket {
297306
}
298307

299308
stopForeground(true)
300-
localBroadcastManager!!.sendBroadcast(Intent(VPN_STOPPED_BROADCAST))
309+
localBroadcastManager!!.sendBroadcast(Intent(VPN_STOPPED_BROADCAST).apply {
310+
putExtra(IntentExtras.VPN_FAILED_EXTRA, failed)
311+
})
301312
stopSelf()
302313

303314
currentService = null

app/src/main/java/tech/httptoolkit/android/main/MainActivity.kt

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class MainActivity : ComponentActivity(), CoroutineScope by MainScope() {
7575
} else if (intent.action == VPN_STOPPED_BROADCAST) {
7676
mainState = ConnectionState.DISCONNECTED
7777
currentProxyConfig = null
78+
79+
if (intent.getBooleanExtra(IntentExtras.VPN_FAILED_EXTRA, false)) {
80+
showActiveVpnFailureAlert()
81+
}
7882
}
7983
}
8084
}
@@ -88,10 +92,6 @@ class MainActivity : ComponentActivity(), CoroutineScope by MainScope() {
8892
private var interceptedAppCount: Int by mutableIntStateOf(0)
8993
private var interceptedPorts: Set<Int> by mutableStateOf(emptySet())
9094

91-
// Used to track extremely fast VPN setup failures, indicating setup issues (rather than
92-
// manual user cancellation). Doesn't matter that it's not properly persistent.
93-
private var lastPauseTime = -1L
94-
9595
val pickAppsContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
9696
if (result.resultCode == RESULT_OK) {
9797
Log.i(TAG, "Pick apps result: OK")
@@ -260,7 +260,6 @@ class MainActivity : ComponentActivity(), CoroutineScope by MainScope() {
260260
override fun onPause() {
261261
super.onPause()
262262
Log.d(TAG, "onPause")
263-
this.lastPauseTime = System.currentTimeMillis()
264263
}
265264

266265
override fun onDestroy() {
@@ -548,18 +547,6 @@ class MainActivity : ComponentActivity(), CoroutineScope by MainScope() {
548547
Log.i(TAG, "Notifications OK, starting VPN...")
549548
startVpn()
550549
}
551-
} else if (
552-
requestCode == START_VPN_REQUEST &&
553-
System.currentTimeMillis() - lastPauseTime < 200 && // On Pixel 4a it takes < 50ms
554-
resultCode == RESULT_CANCELED
555-
) {
556-
// If another always-on VPN is active, VPN start requests fail instantly as cancelled.
557-
// We can't check that the VPN is always-on, but given an instant failure that's
558-
// the likely cause, so we warn about it:
559-
showActiveVpnFailureAlert()
560-
561-
// Then go back to the disconnected state:
562-
mainState = ConnectionState.DISCONNECTED
563550
} else if (
564551
requestCode == INSTALL_CERT_REQUEST &&
565552
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q // Required for promptToManuallyInstallCert
@@ -1016,15 +1003,18 @@ class MainActivity : ComponentActivity(), CoroutineScope by MainScope() {
10161003
}
10171004

10181005
private fun showActiveVpnFailureAlert() {
1006+
// The VPN service can report failures while we're in the background:
1007+
if (isFinishing || isDestroyed) return
1008+
10191009
MaterialAlertDialogBuilder(this)
10201010
.setTitle("VPN setup failed")
10211011
.setIcon(R.drawable.ic_exclamation_triangle)
10221012
.setMessage(
10231013
"HTTP Toolkit could not be configured as a VPN on your device." +
10241014
"\n\n" +
1025-
"This usually means you have an always-on VPN configured, which blocks " +
1026-
"installation of other VPNs. To activate HTTP Toolkit you'll need to " +
1027-
"deactivate that VPN first."
1015+
"This usually means another VPN is active, which blocks HTTP Toolkit's VPN. To " +
1016+
"activate HTTP Toolkit you'll need to deactivate that VPN first, including any " +
1017+
"always-on or auto-reconnect options."
10281018
)
10291019
.setNegativeButton("Cancel") { _, _ -> }
10301020
.setPositiveButton("Open VPN Settings") { _, _ ->

0 commit comments

Comments
 (0)