feature/app-split-tunneling (#702)

App Split Tunneling for Windows and Android
This commit is contained in:
Nethius 2024-04-01 18:45:00 +07:00 committed by GitHub
parent e7bd24f065
commit adab30fc81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1225 additions and 98 deletions

View file

@ -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"

View file

@ -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()) }

View file

@ -79,6 +79,7 @@ open class OpenVpn : Protocol() {
}
configPluggableTransport(configBuilder, config)
configBuilder.configSplitTunneling(config)
configBuilder.configAppSplitTunneling(config)
scope.launch {
val status = client.connect()

View file

@ -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)

View file

@ -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 }

View file

@ -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)
}
}

View 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
}
}

View file

@ -95,6 +95,7 @@ open class Wireguard : Protocol() {
return WireguardConfig.build {
configWireguard(configData, configDataJson)
configSplitTunneling(config)
configAppSplitTunneling(config)
}
}