feature/app-split-tunneling (#702)
App Split Tunneling for Windows and Android
This commit is contained in:
parent
e7bd24f065
commit
adab30fc81
48 changed files with 1225 additions and 98 deletions
|
|
@ -24,9 +24,7 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Enable when VPN-per-app mode will be implemented -->
|
||||
<!-- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".AmneziaApplication"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class Awg : Wireguard() {
|
|||
return AwgConfig.build {
|
||||
configWireguard(configData, configDataJson)
|
||||
configSplitTunneling(config)
|
||||
configAppSplitTunneling(config)
|
||||
configData["Jc"]?.let { setJc(it.toInt()) }
|
||||
configData["Jmin"]?.let { setJmin(it.toInt()) }
|
||||
configData["Jmax"]?.let { setJmax(it.toInt()) }
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ open class OpenVpn : Protocol() {
|
|||
}
|
||||
configPluggableTransport(configBuilder, config)
|
||||
configBuilder.configSplitTunneling(config)
|
||||
configBuilder.configAppSplitTunneling(config)
|
||||
|
||||
scope.launch {
|
||||
val status = client.connect()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,22 @@ abstract class Protocol {
|
|||
}
|
||||
}
|
||||
|
||||
protected fun ProtocolConfig.Builder.configAppSplitTunneling(config: JSONObject) {
|
||||
val splitTunnelType = config.optInt("appSplitTunnelType")
|
||||
if (splitTunnelType == SPLIT_TUNNEL_DISABLE) return
|
||||
val splitTunnelApps = config.getJSONArray("splitTunnelApps")
|
||||
val appHandlerFunc = when (splitTunnelType) {
|
||||
SPLIT_TUNNEL_INCLUDE -> ::includeApplication
|
||||
SPLIT_TUNNEL_EXCLUDE -> ::excludeApplication
|
||||
|
||||
else -> throw BadConfigException("Unexpected value of the 'appSplitTunnelType' parameter: $splitTunnelType")
|
||||
}
|
||||
|
||||
for (i in 0 until splitTunnelApps.length()) {
|
||||
appHandlerFunc(splitTunnelApps.getString(i))
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) {
|
||||
vpnBuilder.setSession(VPN_SESSION_NAME)
|
||||
|
||||
|
|
@ -101,6 +117,11 @@ abstract class Protocol {
|
|||
}
|
||||
}
|
||||
|
||||
for (app in config.includedApplications) {
|
||||
Log.d(TAG, "addAllowedApplication: $app")
|
||||
vpnBuilder.addAllowedApplication(app)
|
||||
}
|
||||
|
||||
for (app in config.excludedApplications) {
|
||||
Log.d(TAG, "addDisallowedApplication: $app")
|
||||
vpnBuilder.addDisallowedApplication(app)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ open class ProtocolConfig protected constructor(
|
|||
val excludedRoutes: Set<InetNetwork>,
|
||||
val includedAddresses: Set<InetNetwork>,
|
||||
val excludedAddresses: Set<InetNetwork>,
|
||||
val includedApplications: Set<String>,
|
||||
val excludedApplications: Set<String>,
|
||||
val httpProxy: ProxyInfo?,
|
||||
val allowAllAF: Boolean,
|
||||
|
|
@ -31,6 +32,7 @@ open class ProtocolConfig protected constructor(
|
|||
builder.excludedRoutes,
|
||||
builder.includedAddresses,
|
||||
builder.excludedAddresses,
|
||||
builder.includedApplications,
|
||||
builder.excludedApplications,
|
||||
builder.httpProxy,
|
||||
builder.allowAllAF,
|
||||
|
|
@ -45,6 +47,7 @@ open class ProtocolConfig protected constructor(
|
|||
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val includedApplications: MutableSet<String> = hashSetOf()
|
||||
internal val excludedApplications: MutableSet<String> = hashSetOf()
|
||||
|
||||
internal var searchDomain: String? = null
|
||||
|
|
@ -88,6 +91,9 @@ open class ProtocolConfig protected constructor(
|
|||
fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr }
|
||||
fun excludeAddresses(addresses: Collection<InetNetwork>) = apply { this.excludedAddresses += addresses }
|
||||
|
||||
fun includeApplication(application: String) = apply { this.includedApplications += application }
|
||||
fun includeApplications(applications: Collection<String>) = apply { this.includedApplications += applications }
|
||||
|
||||
fun excludeApplication(application: String) = apply { this.excludedApplications += application }
|
||||
fun excludeApplications(applications: Collection<String>) = apply { this.excludedApplications += applications }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.content.Intent.EXTRA_MIME_TYPES
|
|||
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
|
|
@ -24,12 +25,15 @@ import androidx.core.content.ContextCompat
|
|||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
import AppListProvider
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
|
|
@ -484,4 +488,24 @@ class AmneziaActivity : QtActivity() {
|
|||
moveTaskToBack(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getAppList(): String {
|
||||
Log.v(TAG, "Get app list")
|
||||
var appList = ""
|
||||
runBlocking {
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
appList = AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
}.join()
|
||||
}
|
||||
return appList
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getAppIcon(packageName: String, width: Int, height: Int): Bitmap {
|
||||
Log.v(TAG, "Get app icon: $packageName")
|
||||
return AppListProvider.getAppIcon(packageManager, packageName, width, height)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
73
client/android/src/org/amnezia/vpn/AppListProvider.kt
Normal file
73
client/android/src/org/amnezia/vpn/AppListProvider.kt
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import android.Manifest.permission.INTERNET
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Bitmap.Config.ARGB_8888
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val TAG = "AppListProvider"
|
||||
|
||||
object AppListProvider {
|
||||
fun getAppList(pm: PackageManager, selfPackageName: String): String {
|
||||
val jsonArray = JSONArray()
|
||||
pm.getPackagesHoldingPermissions(arrayOf(INTERNET), 0)
|
||||
.filter { it.packageName != selfPackageName }
|
||||
.map { App(it, pm) }
|
||||
.sortedWith(App::compareTo)
|
||||
.map(App::toJson)
|
||||
.forEach(jsonArray::put)
|
||||
return jsonArray.toString()
|
||||
}
|
||||
|
||||
fun getAppIcon(pm: PackageManager, packageName: String, width: Int, height: Int): Bitmap {
|
||||
val icon = try {
|
||||
pm.getApplicationIcon(packageName)
|
||||
} catch (e: NameNotFoundException) {
|
||||
Log.e(TAG, "Package $packageName was not found: $e")
|
||||
pm.defaultActivityIcon
|
||||
}
|
||||
val w: Int = if (width > 0) width else icon.intrinsicWidth
|
||||
val h: Int = if (height > 0) height else icon.intrinsicHeight
|
||||
return icon.toBitmapOrNull(w, h, ARGB_8888)
|
||||
?: Bitmap.createBitmap(w, h, ARGB_8888)
|
||||
}
|
||||
}
|
||||
|
||||
private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo = pi.applicationInfo) : Comparable<App> {
|
||||
val name: String?
|
||||
val packageName: String = pi.packageName
|
||||
val icon: Boolean = ai.icon != 0
|
||||
val isLaunchable: Boolean = pm.getLaunchIntentForPackage(packageName) != null
|
||||
|
||||
init {
|
||||
val name = ai.loadLabel(pm).toString()
|
||||
this.name = if (name != packageName) name else null
|
||||
}
|
||||
|
||||
override fun compareTo(other: App): Int {
|
||||
val r = other.isLaunchable.compareTo(isLaunchable)
|
||||
if (r != 0) return r
|
||||
if (name != other.name) {
|
||||
return when {
|
||||
name == null -> 1
|
||||
other.name == null -> -1
|
||||
else -> String.CASE_INSENSITIVE_ORDER.compare(name, other.name)
|
||||
}
|
||||
}
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(packageName, other.packageName)
|
||||
}
|
||||
|
||||
fun toJson(): JSONObject {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("package", packageName)
|
||||
jsonObject.put("name", name)
|
||||
jsonObject.put("icon", icon)
|
||||
jsonObject.put("launchable", isLaunchable)
|
||||
return jsonObject
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@ open class Wireguard : Protocol() {
|
|||
return WireguardConfig.build {
|
||||
configWireguard(configData, configDataJson)
|
||||
configSplitTunneling(config)
|
||||
configAppSplitTunneling(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue