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
|
@ -14,10 +14,12 @@
|
||||||
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
#include "ui/models/installedAppsModel.h"
|
||||||
|
|
||||||
#include "platforms/ios/QRCodeReaderBase.h"
|
#include "platforms/ios/QRCodeReaderBase.h"
|
||||||
#if defined(Q_OS_ANDROID)
|
#if defined(Q_OS_ANDROID)
|
||||||
#include "platforms/android/android_controller.h"
|
#include "platforms/android/android_controller.h"
|
||||||
|
#include "core/installedAppsImageProvider.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "protocols/qml_register_protocols.h"
|
#include "protocols/qml_register_protocols.h"
|
||||||
|
@ -124,8 +126,12 @@ void AmneziaApplication::init()
|
||||||
m_importController->extractConfigFromData(data);
|
m_importController->extractConfigFromData(data);
|
||||||
m_pageController->goToPageViewConfig();
|
m_pageController->goToPageViewConfig();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef Q_OS_IOS
|
#ifdef Q_OS_IOS
|
||||||
IosController::Instance()->initialize();
|
IosController::Instance()->initialize();
|
||||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) {
|
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) {
|
||||||
|
@ -234,7 +240,8 @@ void AmneziaApplication::registerTypes()
|
||||||
qmlRegisterSingletonType(QUrl("qrc:/ui/qml/Filters/ContainersModelFilters.qml"), "ContainersModelFilters", 1, 0,
|
qmlRegisterSingletonType(QUrl("qrc:/ui/qml/Filters/ContainersModelFilters.qml"), "ContainersModelFilters", 1, 0,
|
||||||
"ContainersModelFilters");
|
"ContainersModelFilters");
|
||||||
|
|
||||||
//
|
qmlRegisterType<InstalledAppsModel>("InstalledAppsModel", 1, 0, "InstalledAppsModel");
|
||||||
|
|
||||||
Vpn::declareQmlVpnConnectionStateEnum();
|
Vpn::declareQmlVpnConnectionStateEnum();
|
||||||
PageLoader::declareQmlPageEnum();
|
PageLoader::declareQmlPageEnum();
|
||||||
}
|
}
|
||||||
|
@ -325,6 +332,9 @@ void AmneziaApplication::initModels()
|
||||||
m_sitesModel.reset(new SitesModel(m_settings, this));
|
m_sitesModel.reset(new SitesModel(m_settings, this));
|
||||||
m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get());
|
m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get());
|
||||||
|
|
||||||
|
m_appSplitTunnelingModel.reset(new AppSplitTunnelingModel(m_settings, this));
|
||||||
|
m_engine->rootContext()->setContextProperty("AppSplitTunnelingModel", m_appSplitTunnelingModel.get());
|
||||||
|
|
||||||
m_protocolsModel.reset(new ProtocolsModel(m_settings, this));
|
m_protocolsModel.reset(new ProtocolsModel(m_settings, this));
|
||||||
m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get());
|
m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get());
|
||||||
|
|
||||||
|
@ -407,6 +417,9 @@ void AmneziaApplication::initControllers()
|
||||||
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
||||||
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
||||||
|
|
||||||
|
m_appSplitTunnelingController.reset(new AppSplitTunnelingController(m_settings, m_appSplitTunnelingModel));
|
||||||
|
m_engine->rootContext()->setContextProperty("AppSplitTunnelingController", m_appSplitTunnelingController.get());
|
||||||
|
|
||||||
m_systemController.reset(new SystemController(m_settings));
|
m_systemController.reset(new SystemController(m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
|
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include "ui/controllers/sitesController.h"
|
#include "ui/controllers/sitesController.h"
|
||||||
#include "ui/controllers/systemController.h"
|
#include "ui/controllers/systemController.h"
|
||||||
#include "ui/controllers/apiController.h"
|
#include "ui/controllers/apiController.h"
|
||||||
|
#include "ui/controllers/appSplitTunnelingController.h"
|
||||||
#include "ui/models/containers_model.h"
|
#include "ui/models/containers_model.h"
|
||||||
#include "ui/models/languageModel.h"
|
#include "ui/models/languageModel.h"
|
||||||
#include "ui/models/protocols/cloakConfigModel.h"
|
#include "ui/models/protocols/cloakConfigModel.h"
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
#include "ui/models/services/sftpConfigModel.h"
|
#include "ui/models/services/sftpConfigModel.h"
|
||||||
#include "ui/models/sites_model.h"
|
#include "ui/models/sites_model.h"
|
||||||
#include "ui/models/clientManagementModel.h"
|
#include "ui/models/clientManagementModel.h"
|
||||||
|
#include "ui/models/appSplitTunnelingModel.h"
|
||||||
|
|
||||||
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
||||||
|
|
||||||
|
@ -98,6 +100,7 @@ private:
|
||||||
QSharedPointer<LanguageModel> m_languageModel;
|
QSharedPointer<LanguageModel> m_languageModel;
|
||||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||||
QSharedPointer<SitesModel> m_sitesModel;
|
QSharedPointer<SitesModel> m_sitesModel;
|
||||||
|
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||||
|
|
||||||
QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel;
|
QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel;
|
||||||
|
@ -125,6 +128,7 @@ private:
|
||||||
QScopedPointer<SitesController> m_sitesController;
|
QScopedPointer<SitesController> m_sitesController;
|
||||||
QScopedPointer<SystemController> m_systemController;
|
QScopedPointer<SystemController> m_systemController;
|
||||||
QScopedPointer<ApiController> m_apiController;
|
QScopedPointer<ApiController> m_apiController;
|
||||||
|
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // AMNEZIA_APPLICATION_H
|
#endif // AMNEZIA_APPLICATION_H
|
||||||
|
|
|
@ -24,9 +24,7 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||||
<!-- Enable when VPN-per-app mode will be implemented -->
|
|
||||||
<!-- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> -->
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AmneziaApplication"
|
android:name=".AmneziaApplication"
|
||||||
|
|
|
@ -66,6 +66,7 @@ class Awg : Wireguard() {
|
||||||
return AwgConfig.build {
|
return AwgConfig.build {
|
||||||
configWireguard(configData, configDataJson)
|
configWireguard(configData, configDataJson)
|
||||||
configSplitTunneling(config)
|
configSplitTunneling(config)
|
||||||
|
configAppSplitTunneling(config)
|
||||||
configData["Jc"]?.let { setJc(it.toInt()) }
|
configData["Jc"]?.let { setJc(it.toInt()) }
|
||||||
configData["Jmin"]?.let { setJmin(it.toInt()) }
|
configData["Jmin"]?.let { setJmin(it.toInt()) }
|
||||||
configData["Jmax"]?.let { setJmax(it.toInt()) }
|
configData["Jmax"]?.let { setJmax(it.toInt()) }
|
||||||
|
|
|
@ -79,6 +79,7 @@ open class OpenVpn : Protocol() {
|
||||||
}
|
}
|
||||||
configPluggableTransport(configBuilder, config)
|
configPluggableTransport(configBuilder, config)
|
||||||
configBuilder.configSplitTunneling(config)
|
configBuilder.configSplitTunneling(config)
|
||||||
|
configBuilder.configAppSplitTunneling(config)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val status = client.connect()
|
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) {
|
protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) {
|
||||||
vpnBuilder.setSession(VPN_SESSION_NAME)
|
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) {
|
for (app in config.excludedApplications) {
|
||||||
Log.d(TAG, "addDisallowedApplication: $app")
|
Log.d(TAG, "addDisallowedApplication: $app")
|
||||||
vpnBuilder.addDisallowedApplication(app)
|
vpnBuilder.addDisallowedApplication(app)
|
||||||
|
|
|
@ -16,6 +16,7 @@ open class ProtocolConfig protected constructor(
|
||||||
val excludedRoutes: Set<InetNetwork>,
|
val excludedRoutes: Set<InetNetwork>,
|
||||||
val includedAddresses: Set<InetNetwork>,
|
val includedAddresses: Set<InetNetwork>,
|
||||||
val excludedAddresses: Set<InetNetwork>,
|
val excludedAddresses: Set<InetNetwork>,
|
||||||
|
val includedApplications: Set<String>,
|
||||||
val excludedApplications: Set<String>,
|
val excludedApplications: Set<String>,
|
||||||
val httpProxy: ProxyInfo?,
|
val httpProxy: ProxyInfo?,
|
||||||
val allowAllAF: Boolean,
|
val allowAllAF: Boolean,
|
||||||
|
@ -31,6 +32,7 @@ open class ProtocolConfig protected constructor(
|
||||||
builder.excludedRoutes,
|
builder.excludedRoutes,
|
||||||
builder.includedAddresses,
|
builder.includedAddresses,
|
||||||
builder.excludedAddresses,
|
builder.excludedAddresses,
|
||||||
|
builder.includedApplications,
|
||||||
builder.excludedApplications,
|
builder.excludedApplications,
|
||||||
builder.httpProxy,
|
builder.httpProxy,
|
||||||
builder.allowAllAF,
|
builder.allowAllAF,
|
||||||
|
@ -45,6 +47,7 @@ open class ProtocolConfig protected constructor(
|
||||||
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
|
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
|
||||||
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
||||||
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
||||||
|
internal val includedApplications: MutableSet<String> = hashSetOf()
|
||||||
internal val excludedApplications: MutableSet<String> = hashSetOf()
|
internal val excludedApplications: MutableSet<String> = hashSetOf()
|
||||||
|
|
||||||
internal var searchDomain: String? = null
|
internal var searchDomain: String? = null
|
||||||
|
@ -88,6 +91,9 @@ open class ProtocolConfig protected constructor(
|
||||||
fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr }
|
fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr }
|
||||||
fun excludeAddresses(addresses: Collection<InetNetwork>) = apply { this.excludedAddresses += addresses }
|
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 excludeApplication(application: String) = apply { this.excludedApplications += application }
|
||||||
fun excludeApplications(applications: Collection<String>) = apply { this.excludedApplications += applications }
|
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.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -24,12 +25,15 @@ import androidx.core.content.ContextCompat
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.LazyThreadSafetyMode.NONE
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
import kotlin.text.RegexOption.IGNORE_CASE
|
import kotlin.text.RegexOption.IGNORE_CASE
|
||||||
|
import AppListProvider
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.vpn.protocol.getStatistics
|
import org.amnezia.vpn.protocol.getStatistics
|
||||||
import org.amnezia.vpn.protocol.getStatus
|
import org.amnezia.vpn.protocol.getStatus
|
||||||
import org.amnezia.vpn.qt.QtAndroidController
|
import org.amnezia.vpn.qt.QtAndroidController
|
||||||
|
@ -484,4 +488,24 @@ class AmneziaActivity : QtActivity() {
|
||||||
moveTaskToBack(false)
|
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 {
|
return WireguardConfig.build {
|
||||||
configWireguard(configData, configDataJson)
|
configWireguard(configData, configDataJson)
|
||||||
configSplitTunneling(config)
|
configSplitTunneling(config)
|
||||||
|
configAppSplitTunneling(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ set(QT_ANDROID_MULTI_ABI_FORWARD_VARS "QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL;CMAKE
|
||||||
|
|
||||||
# We need to include qtprivate api's
|
# We need to include qtprivate api's
|
||||||
# As QAndroidBinder is not yet implemented with a public api
|
# As QAndroidBinder is not yet implemented with a public api
|
||||||
set(LIBS ${LIBS} Qt6::CorePrivate)
|
set(LIBS ${LIBS} Qt6::CorePrivate -ljnigraphics)
|
||||||
|
|
||||||
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
|
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ set(HEADERS ${HEADERS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h
|
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(SOURCES ${SOURCES}
|
set(SOURCES ${SOURCES}
|
||||||
|
@ -38,6 +39,7 @@ set(SOURCES ${SOURCES}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
|
foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
|
||||||
|
|
|
@ -22,6 +22,20 @@ namespace amnezia
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct InstalledAppInfo {
|
||||||
|
QString appName;
|
||||||
|
QString packageName;
|
||||||
|
QString appPath;
|
||||||
|
|
||||||
|
bool operator==(const InstalledAppInfo& other) const {
|
||||||
|
if (!packageName.isEmpty()) {
|
||||||
|
return packageName == other.packageName;
|
||||||
|
} else {
|
||||||
|
return appPath == other.appPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
enum ErrorCode {
|
enum ErrorCode {
|
||||||
// General error codes
|
// General error codes
|
||||||
NoError = 0,
|
NoError = 0,
|
||||||
|
|
12
client/core/installedAppsImageProvider.cpp
Normal file
12
client/core/installedAppsImageProvider.cpp
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#include "installedAppsImageProvider.h"
|
||||||
|
|
||||||
|
#include "platforms/android/android_controller.h"
|
||||||
|
|
||||||
|
InstalledAppsImageProvider::InstalledAppsImageProvider() : QQuickImageProvider(QQuickImageProvider::Pixmap)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap InstalledAppsImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
|
||||||
|
{
|
||||||
|
return AndroidController::instance()->getAppIcon(id, size, requestedSize);
|
||||||
|
}
|
15
client/core/installedAppsImageProvider.h
Normal file
15
client/core/installedAppsImageProvider.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#ifndef INSTALLEDAPPSIMAGEPROVIDER_H
|
||||||
|
#define INSTALLEDAPPSIMAGEPROVIDER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQuickImageProvider>
|
||||||
|
|
||||||
|
class InstalledAppsImageProvider : public QQuickImageProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InstalledAppsImageProvider();
|
||||||
|
|
||||||
|
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INSTALLEDAPPSIMAGEPROVIDER_H
|
|
@ -10,6 +10,8 @@
|
||||||
#include <Iptypes.h>
|
#include <Iptypes.h>
|
||||||
#include <WinSock2.h>
|
#include <WinSock2.h>
|
||||||
#include <winsock.h>
|
#include <winsock.h>
|
||||||
|
#include <QNetworkInterface>
|
||||||
|
#include "qendian.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef Q_OS_LINUX
|
#ifdef Q_OS_LINUX
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
@ -159,6 +161,27 @@ bool NetworkUtilities::checkIpSubnetFormat(const QString &ip)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
int NetworkUtilities::AdapterIndexTo(const QHostAddress& dst) {
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
qDebug() << "Getting Current Internet Adapter that routes to"
|
||||||
|
<< dst.toString();
|
||||||
|
quint32_be ipBigEndian;
|
||||||
|
quint32 ip = dst.toIPv4Address();
|
||||||
|
qToBigEndian(ip, &ipBigEndian);
|
||||||
|
_MIB_IPFORWARDROW routeInfo;
|
||||||
|
auto result = GetBestRoute(ipBigEndian, 0, &routeInfo);
|
||||||
|
if (result != NO_ERROR) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
auto adapter =
|
||||||
|
QNetworkInterface::interfaceFromIndex(routeInfo.dwForwardIfIndex);
|
||||||
|
qDebug() << "Internet Adapter:" << adapter.name();
|
||||||
|
return routeInfo.dwForwardIfIndex;
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
DWORD GetAdaptersAddressesWrapper(const ULONG Family,
|
DWORD GetAdaptersAddressesWrapper(const ULONG Family,
|
||||||
const ULONG Flags,
|
const ULONG Flags,
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QRegExp>
|
#include <QRegExp>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QHostAddress>
|
||||||
|
|
||||||
|
|
||||||
class NetworkUtilities : public QObject
|
class NetworkUtilities : public QObject
|
||||||
{
|
{
|
||||||
|
@ -14,6 +16,8 @@ public:
|
||||||
static bool checkIPv4Format(const QString &ip);
|
static bool checkIPv4Format(const QString &ip);
|
||||||
static bool checkIpSubnetFormat(const QString &ip);
|
static bool checkIpSubnetFormat(const QString &ip);
|
||||||
static QString getGatewayAndIface();
|
static QString getGatewayAndIface();
|
||||||
|
// Returns the Interface Index that could Route to dst
|
||||||
|
static int AdapterIndexTo(const QHostAddress& dst);
|
||||||
|
|
||||||
static QRegularExpression ipAddressRegExp();
|
static QRegularExpression ipAddressRegExp();
|
||||||
static QRegularExpression ipAddressPortRegExp();
|
static QRegularExpression ipAddressPortRegExp();
|
||||||
|
|
|
@ -35,8 +35,10 @@ class Daemon : public QObject {
|
||||||
virtual QJsonObject getStatus();
|
virtual QJsonObject getStatus();
|
||||||
|
|
||||||
// Callback before any Activating measure is done
|
// Callback before any Activating measure is done
|
||||||
virtual void prepareActivation(const InterfaceConfig& config){
|
virtual void prepareActivation(const InterfaceConfig& config, int inetAdapterIndex = 0) {
|
||||||
Q_UNUSED(config)};
|
Q_UNUSED(config) };
|
||||||
|
virtual void activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) {
|
||||||
|
Q_UNUSED(config) };
|
||||||
|
|
||||||
QString logs();
|
QString logs();
|
||||||
void cleanLogs();
|
void cleanLogs();
|
||||||
|
|
|
@ -117,6 +117,9 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||||
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
|
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
|
||||||
QJsonArray splitTunnelSites = rawConfig.value("splitTunnelSites").toArray();
|
QJsonArray splitTunnelSites = rawConfig.value("splitTunnelSites").toArray();
|
||||||
|
|
||||||
|
int appSplitTunnelType = rawConfig.value(amnezia::config_key::appSplitTunnelType).toInt();
|
||||||
|
QJsonArray splitTunnelApps = rawConfig.value(amnezia::config_key::splitTunnelApps).toArray();
|
||||||
|
|
||||||
QJsonObject wgConfig = rawConfig.value(protocolName + "_config_data").toObject();
|
QJsonObject wgConfig = rawConfig.value(protocolName + "_config_data").toObject();
|
||||||
|
|
||||||
QJsonObject json;
|
QJsonObject json;
|
||||||
|
@ -217,12 +220,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||||
|
|
||||||
json.insert("excludedAddresses", jsExcludedAddresses);
|
json.insert("excludedAddresses", jsExcludedAddresses);
|
||||||
|
|
||||||
|
json.insert("vpnDisabledApps", splitTunnelApps);
|
||||||
// QJsonArray splitTunnelApps;
|
|
||||||
// for (const auto& uri : hop.m_vpnDisabledApps) {
|
|
||||||
// splitTunnelApps.append(QJsonValue(uri));
|
|
||||||
// }
|
|
||||||
// json.insert("vpnDisabledApps", splitTunnelApps);
|
|
||||||
|
|
||||||
if (protocolName == amnezia::config_key::awg) {
|
if (protocolName == amnezia::config_key::awg) {
|
||||||
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
|
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QQmlFile>
|
#include <QQmlFile>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
#include <android/bitmap.h>
|
||||||
|
|
||||||
#include "android_controller.h"
|
#include "android_controller.h"
|
||||||
#include "android_utils.h"
|
#include "android_utils.h"
|
||||||
|
@ -214,6 +217,46 @@ void AndroidController::minimizeApp()
|
||||||
callActivityMethod("minimizeApp", "()V");
|
callActivityMethod("minimizeApp", "()V");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QJsonArray AndroidController::getAppList()
|
||||||
|
{
|
||||||
|
QJniObject appList = callActivityMethod<jstring>("getAppList", "()Ljava/lang/String;");
|
||||||
|
QJsonArray jsonAppList = QJsonDocument::fromJson(appList.toString().toUtf8()).array();
|
||||||
|
return jsonAppList;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap AndroidController::getAppIcon(const QString &package, QSize *size, const QSize &requestedSize)
|
||||||
|
{
|
||||||
|
QJniObject bitmap = callActivityMethod<jobject>("getAppIcon", "(Ljava/lang/String;II)Landroid/graphics/Bitmap;",
|
||||||
|
QJniObject::fromString(package).object<jstring>(),
|
||||||
|
requestedSize.width(), requestedSize.height());
|
||||||
|
|
||||||
|
QJniEnvironment env;
|
||||||
|
AndroidBitmapInfo info;
|
||||||
|
if (AndroidBitmap_getInfo(env.jniEnv(), bitmap.object(), &info) != ANDROID_BITMAP_RESULT_SUCCESS) return {};
|
||||||
|
|
||||||
|
void *pixels;
|
||||||
|
if (AndroidBitmap_lockPixels(env.jniEnv(), bitmap.object(), &pixels) != ANDROID_BITMAP_RESULT_SUCCESS) return {};
|
||||||
|
|
||||||
|
int width = info.width;
|
||||||
|
int height = info.height;
|
||||||
|
|
||||||
|
size->setWidth(width);
|
||||||
|
size->setHeight(height);
|
||||||
|
|
||||||
|
QImage image(width, height, QImage::Format_RGBA8888);
|
||||||
|
if (info.stride == uint32_t(image.bytesPerLine())) {
|
||||||
|
memcpy((void *) image.constBits(), pixels, info.stride * height);
|
||||||
|
} else {
|
||||||
|
auto *bmpPtr = static_cast<uchar *>(pixels);
|
||||||
|
for (int i = 0; i < height; i++, bmpPtr += info.stride)
|
||||||
|
memcpy((void *) image.constScanLine(i), bmpPtr, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AndroidBitmap_unlockPixels(env.jniEnv(), bitmap.object()) != ANDROID_BITMAP_RESULT_SUCCESS) return {};
|
||||||
|
|
||||||
|
return QPixmap::fromImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
// Moving log processing to the Android side
|
// Moving log processing to the Android side
|
||||||
jclass AndroidController::log;
|
jclass AndroidController::log;
|
||||||
jmethodID AndroidController::logDebug;
|
jmethodID AndroidController::logDebug;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#define ANDROID_CONTROLLER_H
|
#define ANDROID_CONTROLLER_H
|
||||||
|
|
||||||
#include <QJniObject>
|
#include <QJniObject>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
#include "protocols/vpnprotocol.h"
|
#include "protocols/vpnprotocol.h"
|
||||||
|
|
||||||
|
@ -41,6 +42,8 @@ public:
|
||||||
void clearLogs();
|
void clearLogs();
|
||||||
void setScreenshotsEnabled(bool enabled);
|
void setScreenshotsEnabled(bool enabled);
|
||||||
void minimizeApp();
|
void minimizeApp();
|
||||||
|
QJsonArray getAppList();
|
||||||
|
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
|
||||||
|
|
||||||
static bool initLogging();
|
static bool initLogging();
|
||||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
#include "dnsutilswindows.h"
|
#include "dnsutilswindows.h"
|
||||||
#include "leakdetector.h"
|
#include "leakdetector.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
#include "core/networkUtilities.h"
|
||||||
#include "platforms/windows/windowscommons.h"
|
#include "platforms/windows/windowscommons.h"
|
||||||
#include "platforms/windows/windowsservicemanager.h"
|
#include "platforms/windows/windowsservicemanager.h"
|
||||||
#include "windowsfirewall.h"
|
#include "windowsfirewall.h"
|
||||||
|
@ -43,11 +44,24 @@ WindowsDaemon::~WindowsDaemon() {
|
||||||
logger.debug() << "Daemon released";
|
logger.debug() << "Daemon released";
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsDaemon::prepareActivation(const InterfaceConfig& config) {
|
void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAdapterIndex) {
|
||||||
// Before creating the interface we need to check which adapter
|
// Before creating the interface we need to check which adapter
|
||||||
// routes to the server endpoint
|
// routes to the server endpoint
|
||||||
auto serveraddr = QHostAddress(config.m_serverIpv4AddrIn);
|
if (inetAdapterIndex == 0) {
|
||||||
m_inetAdapterIndex = WindowsCommons::AdapterIndexTo(serveraddr);
|
auto serveraddr = QHostAddress(config.m_serverIpv4AddrIn);
|
||||||
|
m_inetAdapterIndex = NetworkUtilities::AdapterIndexTo(serveraddr);
|
||||||
|
} else {
|
||||||
|
m_inetAdapterIndex = inetAdapterIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
|
||||||
|
if (config.m_vpnDisabledApps.length() > 0) {
|
||||||
|
m_splitTunnelManager.start(m_inetAdapterIndex, vpnAdapterIndex);
|
||||||
|
m_splitTunnelManager.setRules(config.m_vpnDisabledApps);
|
||||||
|
} else {
|
||||||
|
m_splitTunnelManager.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
|
bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
|
||||||
|
@ -64,12 +78,8 @@ bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.m_vpnDisabledApps.length() > 0) {
|
activateSplitTunnel(config);
|
||||||
m_splitTunnelManager.start(m_inetAdapterIndex);
|
|
||||||
m_splitTunnelManager.setRules(config.m_vpnDisabledApps);
|
|
||||||
} else {
|
|
||||||
m_splitTunnelManager.stop();
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,8 @@ class WindowsDaemon final : public Daemon {
|
||||||
WindowsDaemon();
|
WindowsDaemon();
|
||||||
~WindowsDaemon();
|
~WindowsDaemon();
|
||||||
|
|
||||||
void prepareActivation(const InterfaceConfig& config) override;
|
void prepareActivation(const InterfaceConfig& config, int inetAdapterIndex = 0) override;
|
||||||
|
void activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool run(Op op, const InterfaceConfig& config) override;
|
bool run(Op op, const InterfaceConfig& config) override;
|
||||||
|
|
|
@ -134,7 +134,7 @@ void WindowsSplitTunnel::setRules(const QStringList& appPaths) {
|
||||||
logger.debug() << "New Configuration applied: " << getState();
|
logger.debug() << "New Configuration applied: " << getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsSplitTunnel::start(int inetAdapterIndex) {
|
void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) {
|
||||||
// To Start we need to send 2 things:
|
// To Start we need to send 2 things:
|
||||||
// Network info (what is vpn what is network)
|
// Network info (what is vpn what is network)
|
||||||
logger.debug() << "Starting SplitTunnel";
|
logger.debug() << "Starting SplitTunnel";
|
||||||
|
@ -171,7 +171,7 @@ void WindowsSplitTunnel::start(int inetAdapterIndex) {
|
||||||
}
|
}
|
||||||
logger.debug() << "Driver is ready || new State:" << getState();
|
logger.debug() << "Driver is ready || new State:" << getState();
|
||||||
|
|
||||||
auto config = generateIPConfiguration(inetAdapterIndex);
|
auto config = generateIPConfiguration(inetAdapterIndex, vpnAdapterIndex);
|
||||||
auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_IP_ADDRESSES, &config[0],
|
auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_IP_ADDRESSES, &config[0],
|
||||||
(DWORD)config.size(), nullptr, 0, &bytesReturned,
|
(DWORD)config.size(), nullptr, 0, &bytesReturned,
|
||||||
nullptr);
|
nullptr);
|
||||||
|
@ -270,14 +270,19 @@ std::vector<uint8_t> WindowsSplitTunnel::generateAppConfiguration(
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> WindowsSplitTunnel::generateIPConfiguration(
|
std::vector<uint8_t> WindowsSplitTunnel::generateIPConfiguration(
|
||||||
int inetAdapterIndex) {
|
int inetAdapterIndex, int vpnAdapterIndex) {
|
||||||
std::vector<uint8_t> out(sizeof(IP_ADDRESSES_CONFIG));
|
std::vector<uint8_t> out(sizeof(IP_ADDRESSES_CONFIG));
|
||||||
|
|
||||||
auto config = reinterpret_cast<IP_ADDRESSES_CONFIG*>(&out[0]);
|
auto config = reinterpret_cast<IP_ADDRESSES_CONFIG*>(&out[0]);
|
||||||
|
|
||||||
auto ifaces = QNetworkInterface::allInterfaces();
|
auto ifaces = QNetworkInterface::allInterfaces();
|
||||||
|
|
||||||
|
if (vpnAdapterIndex == 0) {
|
||||||
|
vpnAdapterIndex = WindowsCommons::VPNAdapterIndex();
|
||||||
|
}
|
||||||
|
|
||||||
// Always the VPN
|
// Always the VPN
|
||||||
getAddress(WindowsCommons::VPNAdapterIndex(), &config->TunnelIpv4,
|
getAddress(vpnAdapterIndex, &config->TunnelIpv4,
|
||||||
&config->TunnelIpv6);
|
&config->TunnelIpv6);
|
||||||
// 2nd best route
|
// 2nd best route
|
||||||
getAddress(inetAdapterIndex, &config->InternetIpv4, &config->InternetIpv6);
|
getAddress(inetAdapterIndex, &config->InternetIpv4, &config->InternetIpv6);
|
||||||
|
|
|
@ -132,7 +132,7 @@ class WindowsSplitTunnel final : public QObject {
|
||||||
void setRules(const QStringList& appPaths);
|
void setRules(const QStringList& appPaths);
|
||||||
|
|
||||||
// Fetches and Pushed needed info to move to engaged mode
|
// Fetches and Pushed needed info to move to engaged mode
|
||||||
void start(int inetAdapterIndex);
|
void start(int inetAdapterIndex, int vpnAdapterIndex = 0);
|
||||||
// Deletes Rules and puts the driver into passive mode
|
// Deletes Rules and puts the driver into passive mode
|
||||||
void stop();
|
void stop();
|
||||||
// Resets the Whole Driver
|
// Resets the Whole Driver
|
||||||
|
@ -164,7 +164,7 @@ class WindowsSplitTunnel final : public QObject {
|
||||||
// Generates a Configuration for Each APP
|
// Generates a Configuration for Each APP
|
||||||
std::vector<uint8_t> generateAppConfiguration(const QStringList& appPaths);
|
std::vector<uint8_t> generateAppConfiguration(const QStringList& appPaths);
|
||||||
// Generates a Configuration which IP's are VPN and which network
|
// Generates a Configuration which IP's are VPN and which network
|
||||||
std::vector<uint8_t> generateIPConfiguration(int inetAdapterIndex);
|
std::vector<uint8_t> generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0);
|
||||||
std::vector<uint8_t> generateProcessBlob();
|
std::vector<uint8_t> generateProcessBlob();
|
||||||
|
|
||||||
void getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6);
|
void getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6);
|
||||||
|
|
|
@ -88,24 +88,6 @@ QString WindowsCommons::tunnelLogFile() {
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// static
|
|
||||||
int WindowsCommons::AdapterIndexTo(const QHostAddress& dst) {
|
|
||||||
logger.debug() << "Getting Current Internet Adapter that routes to"
|
|
||||||
<< logger.sensitive(dst.toString());
|
|
||||||
quint32_be ipBigEndian;
|
|
||||||
quint32 ip = dst.toIPv4Address();
|
|
||||||
qToBigEndian(ip, &ipBigEndian);
|
|
||||||
_MIB_IPFORWARDROW routeInfo;
|
|
||||||
auto result = GetBestRoute(ipBigEndian, 0, &routeInfo);
|
|
||||||
if (result != NO_ERROR) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
auto adapter =
|
|
||||||
QNetworkInterface::interfaceFromIndex(routeInfo.dwForwardIfIndex);
|
|
||||||
logger.debug() << "Internet Adapter:" << adapter.name();
|
|
||||||
return routeInfo.dwForwardIfIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// static
|
// static
|
||||||
int WindowsCommons::VPNAdapterIndex() {
|
int WindowsCommons::VPNAdapterIndex() {
|
||||||
// For someReason QNetworkInterface::fromName(MozillaVPN) does not work >:(
|
// For someReason QNetworkInterface::fromName(MozillaVPN) does not work >:(
|
||||||
|
|
|
@ -19,8 +19,7 @@ class WindowsCommons final {
|
||||||
|
|
||||||
// Returns the Interface Index of the VPN Adapter
|
// Returns the Interface Index of the VPN Adapter
|
||||||
static int VPNAdapterIndex();
|
static int VPNAdapterIndex();
|
||||||
// Returns the Interface Index that could Route to dst
|
|
||||||
static int AdapterIndexTo(const QHostAddress& dst);
|
|
||||||
// Returns the Path of the Current process
|
// Returns the Path of the Current process
|
||||||
static QString getCurrentPath();
|
static QString getCurrentPath();
|
||||||
};
|
};
|
||||||
|
|
|
@ -331,13 +331,16 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line)
|
||||||
m_vpnLocalAddress = l.split(" ").at(1);
|
m_vpnLocalAddress = l.split(" ").at(1);
|
||||||
m_vpnGateway = l.split(" ").at(2);
|
m_vpnGateway = l.split(" ").at(2);
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
|
QThread::msleep(300);
|
||||||
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
|
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
|
||||||
for (int i = 0; i < netInterfaces.size(); i++) {
|
for (int i = 0; i < netInterfaces.size(); i++) {
|
||||||
for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++)
|
for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++)
|
||||||
{
|
{
|
||||||
if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
|
if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
|
||||||
IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index());
|
IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index());
|
||||||
|
m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index());
|
||||||
m_configData.insert("vpnGateway", m_vpnGateway);
|
m_configData.insert("vpnGateway", m_vpnGateway);
|
||||||
|
m_configData.insert("vpnServer", m_configData.value(amnezia::config_key::hostName).toString());
|
||||||
IpcClient::Interface()->enablePeerTraffic(m_configData);
|
IpcClient::Interface()->enablePeerTraffic(m_configData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,9 @@ namespace amnezia
|
||||||
constexpr char splitTunnelSites[] = "splitTunnelSites";
|
constexpr char splitTunnelSites[] = "splitTunnelSites";
|
||||||
constexpr char splitTunnelType[] = "splitTunnelType";
|
constexpr char splitTunnelType[] = "splitTunnelType";
|
||||||
|
|
||||||
|
constexpr char splitTunnelApps[] = "splitTunnelApps";
|
||||||
|
constexpr char appSplitTunnelType[] = "appSplitTunnelType";
|
||||||
|
|
||||||
constexpr char crc[] = "crc";
|
constexpr char crc[] = "crc";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ ErrorCode XrayProtocol::startTun2Sock()
|
||||||
|
|
||||||
m_t2sProcess->setProgram(PermittedProcess::Tun2Socks);
|
m_t2sProcess->setProgram(PermittedProcess::Tun2Socks);
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
|
m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress)));
|
||||||
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up",
|
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up",
|
||||||
QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255").arg(amnezia::protocols::xray::defaultLocalAddr)});
|
QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255").arg(amnezia::protocols::xray::defaultLocalAddr)});
|
||||||
#endif
|
#endif
|
||||||
|
@ -166,7 +167,9 @@ ErrorCode XrayProtocol::startTun2Sock()
|
||||||
{
|
{
|
||||||
if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
|
if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
|
||||||
IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index());
|
IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index());
|
||||||
|
m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index());
|
||||||
m_configData.insert("vpnGateway", m_vpnGateway);
|
m_configData.insert("vpnGateway", m_vpnGateway);
|
||||||
|
m_configData.insert("vpnServer", m_remoteAddress);
|
||||||
IpcClient::Interface()->enablePeerTraffic(m_configData);
|
IpcClient::Interface()->enablePeerTraffic(m_configData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,6 +234,8 @@
|
||||||
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
||||||
<file>images/controls/split-tunneling.svg</file>
|
<file>images/controls/split-tunneling.svg</file>
|
||||||
<file>ui/qml/Controls2/DrawerType2.qml</file>
|
<file>ui/qml/Controls2/DrawerType2.qml</file>
|
||||||
|
<file>ui/qml/Pages2/PageSettingsAppSplitTunneling.qml</file>
|
||||||
|
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
||||||
<file>images/controls/alert-circle.svg</file>
|
<file>images/controls/alert-circle.svg</file>
|
||||||
<file>images/controls/file-check-2.svg</file>
|
<file>images/controls/file-check-2.svg</file>
|
||||||
<file>ui/qml/Controls2/WarningType.qml</file>
|
<file>ui/qml/Controls2/WarningType.qml</file>
|
||||||
|
|
|
@ -355,6 +355,54 @@ void Settings::clearSettings()
|
||||||
emit settingsCleared();
|
emit settingsCleared();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Settings::appsRouteModeString(AppsRouteMode mode) const
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case VpnAllApps: return "AllApps";
|
||||||
|
case VpnOnlyForwardApps: return "ForwardApps";
|
||||||
|
case VpnAllExceptApps: return "ExceptApps";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings::AppsRouteMode Settings::getAppsRouteMode() const
|
||||||
|
{
|
||||||
|
return static_cast<AppsRouteMode>(value("Conf/appsRouteMode", 0).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setAppsRouteMode(AppsRouteMode mode)
|
||||||
|
{
|
||||||
|
setValue("Conf/appsRouteMode", mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<InstalledAppInfo> Settings::getVpnApps(AppsRouteMode mode) const
|
||||||
|
{
|
||||||
|
QVector<InstalledAppInfo> apps;
|
||||||
|
auto appsArray = value("Conf/" + appsRouteModeString(mode)).toJsonArray();
|
||||||
|
for (const auto &app : appsArray) {
|
||||||
|
InstalledAppInfo appInfo;
|
||||||
|
appInfo.appName = app.toObject().value("appName").toString();
|
||||||
|
appInfo.packageName = app.toObject().value("packageName").toString();
|
||||||
|
appInfo.appPath = app.toObject().value("appPath").toString();
|
||||||
|
|
||||||
|
apps.push_back(appInfo);
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setVpnApps(AppsRouteMode mode, const QVector<InstalledAppInfo> &apps)
|
||||||
|
{
|
||||||
|
QJsonArray appsArray;
|
||||||
|
for (const auto &app : apps) {
|
||||||
|
QJsonObject appInfo;
|
||||||
|
appInfo.insert("appName", app.appName);
|
||||||
|
appInfo.insert("packageName", app.packageName);
|
||||||
|
appInfo.insert("appPath", app.appPath);
|
||||||
|
appsArray.push_back(appInfo);
|
||||||
|
}
|
||||||
|
setValue("Conf/" + appsRouteModeString(mode), appsArray);
|
||||||
|
m_settings.sync();
|
||||||
|
}
|
||||||
|
|
||||||
ServerCredentials Settings::defaultServerCredentials() const
|
ServerCredentials Settings::defaultServerCredentials() const
|
||||||
{
|
{
|
||||||
return serverCredentials(defaultServerIndex());
|
return serverCredentials(defaultServerIndex());
|
||||||
|
|
|
@ -193,6 +193,21 @@ public:
|
||||||
|
|
||||||
void clearSettings();
|
void clearSettings();
|
||||||
|
|
||||||
|
enum AppsRouteMode {
|
||||||
|
VpnAllApps,
|
||||||
|
VpnOnlyForwardApps,
|
||||||
|
VpnAllExceptApps
|
||||||
|
};
|
||||||
|
Q_ENUM(AppsRouteMode)
|
||||||
|
|
||||||
|
QString appsRouteModeString(AppsRouteMode mode) const;
|
||||||
|
|
||||||
|
AppsRouteMode getAppsRouteMode() const;
|
||||||
|
void setAppsRouteMode(AppsRouteMode mode);
|
||||||
|
|
||||||
|
QVector<InstalledAppInfo> getVpnApps(AppsRouteMode mode) const;
|
||||||
|
void setVpnApps(AppsRouteMode mode, const QVector<InstalledAppInfo> &apps);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void saveLogsChanged(bool enabled);
|
void saveLogsChanged(bool enabled);
|
||||||
void screenshotsEnabledChanged(bool enabled);
|
void screenshotsEnabledChanged(bool enabled);
|
||||||
|
|
50
client/ui/controllers/appSplitTunnelingController.cpp
Normal file
50
client/ui/controllers/appSplitTunnelingController.cpp
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#include "appSplitTunnelingController.h"
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include "core/defs.h"
|
||||||
|
|
||||||
|
AppSplitTunnelingController::AppSplitTunnelingController(const std::shared_ptr<Settings> &settings,
|
||||||
|
const QSharedPointer<AppSplitTunnelingModel> &appSplitTunnelingModel, QObject *parent)
|
||||||
|
: QObject(parent), m_settings(settings), m_appSplitTunnelingModel(appSplitTunnelingModel)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSplitTunnelingController::addApp(const QString &appPath)
|
||||||
|
{
|
||||||
|
|
||||||
|
InstalledAppInfo appInfo { "", "", appPath };
|
||||||
|
if (!appPath.isEmpty()) {
|
||||||
|
QFileInfo fileInfo(appPath);
|
||||||
|
appInfo.appName = fileInfo.fileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_appSplitTunnelingModel->addApp(appInfo)) {
|
||||||
|
emit finished(tr("Application added: %1").arg(appInfo.appName));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
emit errorOccurred(tr("The application has already been added"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSplitTunnelingController::addApps(QVector<QPair<QString, QString>> apps)
|
||||||
|
{
|
||||||
|
qDebug() << apps;
|
||||||
|
for (const auto &app : apps) {
|
||||||
|
InstalledAppInfo appInfo { app.first, app.second, "" };
|
||||||
|
|
||||||
|
m_appSplitTunnelingModel->addApp(appInfo);
|
||||||
|
}
|
||||||
|
emit finished(tr("The selected applications have been added"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSplitTunnelingController::removeApp(const int index)
|
||||||
|
{
|
||||||
|
auto modelIndex = m_appSplitTunnelingModel->index(index);
|
||||||
|
auto appPath = m_appSplitTunnelingModel->data(modelIndex, AppSplitTunnelingModel::Roles::AppPathRole).toString();
|
||||||
|
m_appSplitTunnelingModel->removeApp(modelIndex);
|
||||||
|
|
||||||
|
QFileInfo fileInfo(appPath);
|
||||||
|
|
||||||
|
emit finished(tr("Application removed: %1").arg(fileInfo.fileName()));
|
||||||
|
}
|
31
client/ui/controllers/appSplitTunnelingController.h
Normal file
31
client/ui/controllers/appSplitTunnelingController.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#ifndef APPSPLITTUNNELINGCONTROLLER_H
|
||||||
|
#define APPSPLITTUNNELINGCONTROLLER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "settings.h"
|
||||||
|
#include "ui/models/appSplitTunnelingModel.h"
|
||||||
|
|
||||||
|
class AppSplitTunnelingController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AppSplitTunnelingController(const std::shared_ptr<Settings> &settings,
|
||||||
|
const QSharedPointer<AppSplitTunnelingModel> &sitesModel, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void addApp(const QString &appPath);
|
||||||
|
void addApps(QVector<QPair<QString, QString>> apps);
|
||||||
|
void removeApp(const int index);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void errorOccurred(const QString &errorMessage);
|
||||||
|
void finished(const QString &message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
|
||||||
|
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // APPSPLITTUNNELINGCONTROLLER_H
|
|
@ -29,6 +29,7 @@ namespace PageLoader
|
||||||
PageSettingsAbout,
|
PageSettingsAbout,
|
||||||
PageSettingsLogging,
|
PageSettingsLogging,
|
||||||
PageSettingsSplitTunneling,
|
PageSettingsSplitTunneling,
|
||||||
|
PageSettingsAppSplitTunneling,
|
||||||
|
|
||||||
PageServiceSftpSettings,
|
PageServiceSftpSettings,
|
||||||
PageServiceTorWebsiteSettings,
|
PageServiceTorWebsiteSettings,
|
||||||
|
|
101
client/ui/models/appSplitTunnelingModel.cpp
Normal file
101
client/ui/models/appSplitTunnelingModel.cpp
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
#include "appSplitTunnelingModel.h"
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
AppSplitTunnelingModel::AppSplitTunnelingModel(std::shared_ptr<Settings> settings, QObject *parent)
|
||||||
|
: QAbstractListModel(parent), m_settings(settings)
|
||||||
|
{
|
||||||
|
auto routeMode = m_settings->getAppsRouteMode();
|
||||||
|
if (routeMode == Settings::AppsRouteMode::VpnAllApps) {
|
||||||
|
m_isSplitTunnelingEnabled = false;
|
||||||
|
m_currentRouteMode = Settings::AppsRouteMode::VpnAllExceptApps;
|
||||||
|
} else {
|
||||||
|
m_isSplitTunnelingEnabled = true;
|
||||||
|
m_currentRouteMode = routeMode;
|
||||||
|
}
|
||||||
|
m_apps = m_settings->getVpnApps(m_currentRouteMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AppSplitTunnelingModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent)
|
||||||
|
return m_apps.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant AppSplitTunnelingModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
|
||||||
|
return QVariant();
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case AppPathRole: {
|
||||||
|
return m_apps.at(index.row()).appName;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSplitTunnelingModel::addApp(const InstalledAppInfo &appInfo)
|
||||||
|
{
|
||||||
|
if (m_apps.contains(appInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginInsertRows(QModelIndex(), rowCount(), rowCount());
|
||||||
|
m_apps.append(appInfo);
|
||||||
|
m_settings->setVpnApps(m_currentRouteMode, m_apps);
|
||||||
|
endInsertRows();
|
||||||
|
|
||||||
|
qDebug() << "app added " << appInfo.appName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSplitTunnelingModel::removeApp(QModelIndex index)
|
||||||
|
{
|
||||||
|
beginRemoveRows(QModelIndex(), index.row(), index.row());
|
||||||
|
m_apps.removeAt(index.row());
|
||||||
|
m_settings->setVpnApps(m_currentRouteMode, m_apps);
|
||||||
|
endRemoveRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
int AppSplitTunnelingModel::getRouteMode()
|
||||||
|
{
|
||||||
|
return m_currentRouteMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSplitTunnelingModel::setRouteMode(int routeMode)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_settings->setAppsRouteMode(static_cast<Settings::AppsRouteMode>(routeMode));
|
||||||
|
m_currentRouteMode = m_settings->getAppsRouteMode();
|
||||||
|
m_apps = m_settings->getVpnApps(m_currentRouteMode);
|
||||||
|
endResetModel();
|
||||||
|
emit routeModeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSplitTunnelingModel::isSplitTunnelingEnabled()
|
||||||
|
{
|
||||||
|
return m_isSplitTunnelingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSplitTunnelingModel::toggleSplitTunneling(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled) {
|
||||||
|
setRouteMode(m_currentRouteMode);
|
||||||
|
} else {
|
||||||
|
m_settings->setAppsRouteMode(Settings::AppsRouteMode::VpnAllApps);
|
||||||
|
}
|
||||||
|
m_isSplitTunnelingEnabled = enabled;
|
||||||
|
emit splitTunnelingToggled();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> AppSplitTunnelingModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles[AppPathRole] = "appPath";
|
||||||
|
return roles;
|
||||||
|
}
|
55
client/ui/models/appSplitTunnelingModel.h
Normal file
55
client/ui/models/appSplitTunnelingModel.h
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#ifndef APPSPLITTUNNELINGMODEL_H
|
||||||
|
#define APPSPLITTUNNELINGMODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
#include "settings.h"
|
||||||
|
#include "core/defs.h"
|
||||||
|
|
||||||
|
class AppSplitTunnelingModel: public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
AppPathRole = Qt::UserRole + 1,
|
||||||
|
PackageAppNameRole,
|
||||||
|
PackageAppIconRole
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit AppSplitTunnelingModel(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
Q_PROPERTY(int routeMode READ getRouteMode WRITE setRouteMode NOTIFY routeModeChanged)
|
||||||
|
Q_PROPERTY(bool isTunnelingEnabled READ isSplitTunnelingEnabled NOTIFY splitTunnelingToggled)
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
bool addApp(const InstalledAppInfo &appInfo);
|
||||||
|
void removeApp(QModelIndex index);
|
||||||
|
|
||||||
|
int getRouteMode();
|
||||||
|
void setRouteMode(int routeMode);
|
||||||
|
|
||||||
|
bool isSplitTunnelingEnabled();
|
||||||
|
void toggleSplitTunneling(bool enabled);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void routeModeChanged();
|
||||||
|
void splitTunnelingToggled();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
|
||||||
|
bool m_isSplitTunnelingEnabled;
|
||||||
|
Settings::AppsRouteMode m_currentRouteMode;
|
||||||
|
|
||||||
|
QVector<InstalledAppInfo> m_apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // APPSPLITTUNNELINGMODEL_H
|
96
client/ui/models/installedAppsModel.cpp
Normal file
96
client/ui/models/installedAppsModel.cpp
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
#include "installedAppsModel.h"
|
||||||
|
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
#include "platforms/android/android_controller.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
InstalledAppsModel::InstalledAppsModel(QObject *parent) : QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int InstalledAppsModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent)
|
||||||
|
return m_installedApps.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant InstalledAppsModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
|
||||||
|
return QVariant();
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case AppNameRole: {
|
||||||
|
auto appName = m_installedApps.at(index.row()).toObject().value("name").toString();
|
||||||
|
auto packageName = m_installedApps.at(index.row()).toObject().value("package").toString();
|
||||||
|
if (appName.isEmpty()) {
|
||||||
|
appName = packageName;
|
||||||
|
}
|
||||||
|
return appName;
|
||||||
|
}
|
||||||
|
case AppIconRole: {
|
||||||
|
return m_installedApps.at(index.row()).toObject().value("package").toString();
|
||||||
|
}
|
||||||
|
case PackageNameRole: {
|
||||||
|
return m_installedApps.at(index.row()).toObject().value("package");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstalledAppsModel::selectedStateChanged(const int index, const bool selected)
|
||||||
|
{
|
||||||
|
if (selected) {
|
||||||
|
m_selectedAppIndexes.insert(index);
|
||||||
|
} else {
|
||||||
|
m_selectedAppIndexes.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QPair<QString, QString>> InstalledAppsModel::getSelectedAppsInfo()
|
||||||
|
{
|
||||||
|
QVector<QPair<QString, QString>> appsInfo;
|
||||||
|
for (const auto i : m_selectedAppIndexes) {
|
||||||
|
QString packageName = data(index(i, 0), PackageNameRole).toString();
|
||||||
|
QString appName = data(index(i, 0), AppNameRole).toString();
|
||||||
|
if (appName.isEmpty()) {
|
||||||
|
appName = packageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
appsInfo.push_back({ appName, packageName });
|
||||||
|
}
|
||||||
|
|
||||||
|
return appsInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstalledAppsModel::updateModel()
|
||||||
|
{
|
||||||
|
QFuture<void> future = QtConcurrent::run([this]() {
|
||||||
|
beginResetModel();
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
m_installedApps = AndroidController::instance()->getAppList();
|
||||||
|
#endif
|
||||||
|
endResetModel();
|
||||||
|
});
|
||||||
|
|
||||||
|
QFutureWatcher<void> watcher;
|
||||||
|
QEventLoop wait;
|
||||||
|
connect(&watcher, &QFutureWatcher<void>::finished, &wait, &QEventLoop::quit);
|
||||||
|
watcher.setFuture(future);
|
||||||
|
wait.exec();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> InstalledAppsModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles[AppNameRole] = "appName";
|
||||||
|
roles[AppIconRole] = "appIcon";
|
||||||
|
roles[PackageNameRole] = "packageName";
|
||||||
|
return roles;
|
||||||
|
}
|
38
client/ui/models/installedAppsModel.h
Normal file
38
client/ui/models/installedAppsModel.h
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#ifndef INSTALLEDAPPSMODEL_H
|
||||||
|
#define INSTALLEDAPPSMODEL_H
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
class InstalledAppsModel: public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
AppNameRole= Qt::UserRole + 1,
|
||||||
|
PackageNameRole,
|
||||||
|
AppIconRole
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit InstalledAppsModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void selectedStateChanged(const int index, const bool selected);
|
||||||
|
QVector<QPair<QString, QString>> getSelectedAppsInfo();
|
||||||
|
|
||||||
|
void updateModel();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QJsonArray m_installedApps;
|
||||||
|
QSet<int> m_selectedAppIndexes;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INSTALLEDAPPSMODEL_H
|
|
@ -57,7 +57,7 @@ DrawerType2 {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
|
|
||||||
enabled: ! ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi")
|
enabled: !ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi")
|
||||||
|
|
||||||
text: qsTr("Site-based split tunneling")
|
text: qsTr("Site-based split tunneling")
|
||||||
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||||
|
@ -80,7 +80,7 @@ DrawerType2 {
|
||||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
// PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling)
|
||||||
root.close()
|
root.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
136
client/ui/qml/Components/InstalledAppsDrawer.qml
Normal file
136
client/ui/qml/Components/InstalledAppsDrawer.qml
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import "../Controls2"
|
||||||
|
import "../Controls2/TextTypes"
|
||||||
|
|
||||||
|
import InstalledAppsModel 1.0
|
||||||
|
|
||||||
|
DrawerType2 {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
expandedHeight: parent.height * 0.9
|
||||||
|
|
||||||
|
onAboutToShow: {
|
||||||
|
PageController.showBusyIndicator(true)
|
||||||
|
installedAppsModel.updateModel()
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstalledAppsModel {
|
||||||
|
id: installedAppsModel
|
||||||
|
}
|
||||||
|
|
||||||
|
expandedContent: Item {
|
||||||
|
id: container
|
||||||
|
|
||||||
|
implicitHeight: expandedHeight
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: backButton
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: addButton.top
|
||||||
|
anchors.topMargin: 16
|
||||||
|
|
||||||
|
BackButtonType {
|
||||||
|
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||||
|
backButtonFunction: function() {
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header2Type {
|
||||||
|
id: header
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("Choose application")
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
interactive: true
|
||||||
|
|
||||||
|
model: installedAppsModel
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
id: scrollBar
|
||||||
|
policy: ScrollBar.AlwaysOn
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonGroup {
|
||||||
|
id: buttonGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
implicitWidth: root.width
|
||||||
|
implicitHeight: delegateContent.implicitHeight
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: delegateContent
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
CheckBoxType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
text: appName
|
||||||
|
|
||||||
|
onCheckedChanged: {
|
||||||
|
listView.model.selectedStateChanged(index, checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "image://installedAppImage/" + appIcon
|
||||||
|
|
||||||
|
sourceSize.width: 24
|
||||||
|
sourceSize.height: 24
|
||||||
|
|
||||||
|
Layout.rightMargin: 48
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DividerType {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicButtonType {
|
||||||
|
id: addButton
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
|
||||||
|
text: qsTr("Add selected")
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
PageController.showBusyIndicator(true)
|
||||||
|
AppSplitTunnelingController.addApps(listView.model.getSelectedAppsInfo())
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,12 +90,12 @@ CheckBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
implicitWidth: content.implicitWidth
|
anchors.left: parent.left
|
||||||
implicitHeight: content.implicitHeight
|
anchors.right: parent.right
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 8 + background.width
|
anchors.leftMargin: 8 + background.width
|
||||||
|
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
|
|
|
@ -126,32 +126,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicButtonType {
|
|
||||||
visible: (root.buttonText !== "") || (root.buttonImageSource !== "")
|
|
||||||
|
|
||||||
// defaultColor: "transparent"
|
|
||||||
// hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
|
||||||
// pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
|
||||||
// disabledColor: "#878B91"
|
|
||||||
// textColor: "#D7D8DB"
|
|
||||||
// borderWidth: 0
|
|
||||||
|
|
||||||
focusPolicy: Qt.NoFocus
|
|
||||||
text: root.buttonText
|
|
||||||
imageSource: root.buttonImageSource
|
|
||||||
|
|
||||||
// Layout.rightMargin: 24
|
|
||||||
Layout.preferredHeight: content.implicitHeight
|
|
||||||
Layout.preferredWidth: content.implicitHeight
|
|
||||||
squareLeftSide: true
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
|
||||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
|
||||||
root.clickedFunc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +161,28 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BasicButtonType {
|
||||||
|
visible: (root.buttonText !== "") || (root.buttonImageSource !== "")
|
||||||
|
|
||||||
|
focusPolicy: Qt.NoFocus
|
||||||
|
text: root.buttonText
|
||||||
|
imageSource: root.buttonImageSource
|
||||||
|
|
||||||
|
anchors.top: content.top
|
||||||
|
anchors.bottom: content.bottom
|
||||||
|
anchors.right: content.right
|
||||||
|
|
||||||
|
height: content.implicitHeight
|
||||||
|
width: content.implicitHeight
|
||||||
|
squareLeftSide: true
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||||
|
root.clickedFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getBackgroundBorderColor(noneFocusedColor) {
|
function getBackgroundBorderColor(noneFocusedColor) {
|
||||||
return textField.focus ? root.borderFocusedColor : noneFocusedColor
|
return textField.focus ? root.borderFocusedColor : noneFocusedColor
|
||||||
}
|
}
|
||||||
|
|
256
client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml
Normal file
256
client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
|
import PageEnum 1.0
|
||||||
|
import ProtocolEnum 1.0
|
||||||
|
import ContainerProps 1.0
|
||||||
|
|
||||||
|
import "./"
|
||||||
|
import "../Controls2"
|
||||||
|
import "../Controls2/TextTypes"
|
||||||
|
import "../Config"
|
||||||
|
import "../Components"
|
||||||
|
|
||||||
|
PageType {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: routeMode
|
||||||
|
property int allApps: 0
|
||||||
|
property int onlyForwardApps: 1
|
||||||
|
property int allExceptApps: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
property list<QtObject> routeModesModel: [
|
||||||
|
onlyForwardApps,
|
||||||
|
allExceptApps
|
||||||
|
]
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: onlyForwardApps
|
||||||
|
property string name: qsTr("Only the Apps listed here will be accessed through the VPN")
|
||||||
|
property int type: routeMode.onlyForwardApps
|
||||||
|
}
|
||||||
|
QtObject {
|
||||||
|
id: allExceptApps
|
||||||
|
property string name: qsTr("Apps from the list should not be accessed via VPN")
|
||||||
|
property int type: routeMode.allExceptApps
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRouteModesModelIndex() {
|
||||||
|
var currentRouteMode = AppSplitTunnelingModel.routeMode
|
||||||
|
if ((routeMode.onlyForwardApps === currentRouteMode) || (routeMode.allApps === currentRouteMode)) {
|
||||||
|
return 0
|
||||||
|
} else if (routeMode.allExceptApps === currentRouteMode) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
anchors.topMargin: 20
|
||||||
|
|
||||||
|
BackButtonType {
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
HeaderType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("App split tunneling")
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitcherType {
|
||||||
|
id: switcher
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
|
checked: AppSplitTunnelingModel.isTunnelingEnabled
|
||||||
|
onToggled: {
|
||||||
|
AppSplitTunnelingModel.toggleSplitTunneling(checked)
|
||||||
|
selector.text = root.routeModesModel[getRouteModesModelIndex()].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DropDownType {
|
||||||
|
id: selector
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 32
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
|
drawerHeight: 0.4375
|
||||||
|
drawerParent: root
|
||||||
|
|
||||||
|
headerText: qsTr("Mode")
|
||||||
|
|
||||||
|
enabled: Qt.platform.os === "android"
|
||||||
|
|
||||||
|
listView: ListViewWithRadioButtonType {
|
||||||
|
rootWidth: root.width
|
||||||
|
|
||||||
|
model: root.routeModesModel
|
||||||
|
|
||||||
|
currentIndex: getRouteModesModelIndex()
|
||||||
|
|
||||||
|
clickedFunction: function() {
|
||||||
|
selector.text = selectedText
|
||||||
|
selector.close()
|
||||||
|
if (AppSplitTunnelingModel.routeMode !== root.routeModesModel[currentIndex].type) {
|
||||||
|
AppSplitTunnelingModel.routeMode = root.routeModesModel[currentIndex].type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.routeModesModel[currentIndex].type === AppSplitTunnelingModel.routeMode) {
|
||||||
|
selector.text = selectedText
|
||||||
|
} else {
|
||||||
|
selector.text = root.routeModesModel[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: AppSplitTunnelingModel
|
||||||
|
function onRouteModeChanged() {
|
||||||
|
currentIndex = getRouteModesModelIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlickableType {
|
||||||
|
anchors.top: header.bottom
|
||||||
|
anchors.topMargin: 16
|
||||||
|
contentHeight: col.implicitHeight + addAppButton.implicitHeight + addAppButton.anchors.bottomMargin + addAppButton.anchors.topMargin
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: col
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: apps
|
||||||
|
width: parent.width
|
||||||
|
height: apps.contentItem.height
|
||||||
|
|
||||||
|
model: SortFilterProxyModel {
|
||||||
|
id: proxyAppSplitTunnelingModel
|
||||||
|
sourceModel: AppSplitTunnelingModel
|
||||||
|
filters: RegExpFilter {
|
||||||
|
roleName: "appPath"
|
||||||
|
pattern: ".*" + searchField.textField.text + ".*"
|
||||||
|
caseSensitivity: Qt.CaseInsensitive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
interactive: false
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
implicitWidth: apps.width
|
||||||
|
implicitHeight: delegateContent.implicitHeight
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: delegateContent
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
LabelWithButtonType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
text: appPath
|
||||||
|
rightImageSource: "qrc:/images/controls/trash.svg"
|
||||||
|
rightImageColor: "#D7D8DB"
|
||||||
|
|
||||||
|
clickedFunction: function() {
|
||||||
|
var headerText = qsTr("Remove ") + appPath + "?"
|
||||||
|
var yesButtonText = qsTr("Continue")
|
||||||
|
var noButtonText = qsTr("Cancel")
|
||||||
|
|
||||||
|
var yesButtonFunction = function() {
|
||||||
|
AppSplitTunnelingController.removeApp(proxyAppSplitTunnelingModel.mapToSource(index))
|
||||||
|
}
|
||||||
|
var noButtonFunction = function() {
|
||||||
|
}
|
||||||
|
|
||||||
|
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DividerType {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: addAppButton
|
||||||
|
anchors.bottomMargin: -24
|
||||||
|
color: "#0E0E11"
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: addAppButton
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 24
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.bottomMargin: 24
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
textFieldPlaceholderText: qsTr("application name")
|
||||||
|
buttonImageSource: "qrc:/images/controls/plus.svg"
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
searchField.focus = false
|
||||||
|
PageController.showBusyIndicator(true)
|
||||||
|
|
||||||
|
if (Qt.platform.os === "windows") {
|
||||||
|
var fileName = SystemController.getFileName(qsTr("Open executable file"),
|
||||||
|
qsTr("Executable file (*.*)"))
|
||||||
|
if (fileName !== "") {
|
||||||
|
AppSplitTunnelingController.addApp(fileName)
|
||||||
|
}
|
||||||
|
} else if (Qt.platform.os === "android"){
|
||||||
|
installedAppDrawer.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InstalledAppsDrawer {
|
||||||
|
id: installedAppDrawer
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ import "../Config"
|
||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool isAppSplitTinnelingEnabled: Qt.platform.os === "windows" || Qt.platform.os === "android"
|
||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
|
|
||||||
|
@ -73,8 +75,6 @@ PageType {
|
||||||
DividerType {}
|
DividerType {}
|
||||||
|
|
||||||
LabelWithButtonType {
|
LabelWithButtonType {
|
||||||
visible: true
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
text: qsTr("Site-based split tunneling")
|
text: qsTr("Site-based split tunneling")
|
||||||
|
@ -87,11 +87,11 @@ PageType {
|
||||||
}
|
}
|
||||||
|
|
||||||
DividerType {
|
DividerType {
|
||||||
visible: GC.isDesktop()
|
visible: root.isAppSplitTinnelingEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
LabelWithButtonType {
|
LabelWithButtonType {
|
||||||
visible: false
|
visible: root.isAppSplitTinnelingEnabled
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
@ -100,11 +100,12 @@ PageType {
|
||||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
|
PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DividerType {
|
DividerType {
|
||||||
visible: false
|
visible: root.isAppSplitTinnelingEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ PageType {
|
||||||
|
|
||||||
property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi")
|
property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi")
|
||||||
|
|
||||||
defaultActiveFocusItem: website_ip_field.textField
|
defaultActiveFocusItem: searchField.textField
|
||||||
|
|
||||||
property bool pageEnabled: {
|
property bool pageEnabled: {
|
||||||
return !ConnectionController.isConnected && !isServerFromApi
|
return !ConnectionController.isConnected && !isServerFromApi
|
||||||
|
@ -188,7 +188,24 @@ PageType {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: sites.contentItem.height
|
height: sites.contentItem.height
|
||||||
|
|
||||||
model: SitesModel
|
model: SortFilterProxyModel {
|
||||||
|
id: proxySitesModel
|
||||||
|
sourceModel: SitesModel
|
||||||
|
filters: [
|
||||||
|
AnyOf {
|
||||||
|
RegExpFilter {
|
||||||
|
roleName: "url"
|
||||||
|
pattern: ".*" + searchField.textField.text + ".*"
|
||||||
|
caseSensitivity: Qt.CaseInsensitive
|
||||||
|
}
|
||||||
|
RegExpFilter {
|
||||||
|
roleName: "ip"
|
||||||
|
pattern: ".*" + searchField.textField.text + ".*"
|
||||||
|
caseSensitivity: Qt.CaseInsensitive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
interactive: false
|
interactive: false
|
||||||
|
@ -218,7 +235,7 @@ PageType {
|
||||||
var noButtonText = qsTr("Cancel")
|
var noButtonText = qsTr("Cancel")
|
||||||
|
|
||||||
var yesButtonFunction = function() {
|
var yesButtonFunction = function() {
|
||||||
SitesController.removeSite(index)
|
SitesController.removeSite(proxySitesModel.mapToSource(index))
|
||||||
}
|
}
|
||||||
var noButtonFunction = function() {
|
var noButtonFunction = function() {
|
||||||
}
|
}
|
||||||
|
@ -255,7 +272,7 @@ PageType {
|
||||||
anchors.bottomMargin: 24
|
anchors.bottomMargin: 24
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
TextFieldWithHeaderType {
|
||||||
id: website_ip_field
|
id: searchField
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
@ -430,8 +447,4 @@ PageType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QuestionDrawer {
|
|
||||||
id: questionDrawer
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -428,6 +428,17 @@ void VpnConnection::appendSplitTunnelingConfig()
|
||||||
|
|
||||||
m_vpnConfiguration.insert(config_key::splitTunnelType, routeMode);
|
m_vpnConfiguration.insert(config_key::splitTunnelType, routeMode);
|
||||||
m_vpnConfiguration.insert(config_key::splitTunnelSites, sitesJsonArray);
|
m_vpnConfiguration.insert(config_key::splitTunnelSites, sitesJsonArray);
|
||||||
|
|
||||||
|
auto appsRouteMode = m_settings->getAppsRouteMode();
|
||||||
|
auto apps = m_settings->getVpnApps(appsRouteMode);
|
||||||
|
|
||||||
|
QJsonArray appsJsonArray;
|
||||||
|
for (const auto &app : apps) {
|
||||||
|
appsJsonArray.append(app.appPath.isEmpty() ? app.packageName : app.appPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode);
|
||||||
|
m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
#include "tapcontroller_win.h"
|
#include "tapcontroller_win.h"
|
||||||
#include "../client/platforms/windows/daemon/windowsfirewall.h"
|
#include "../client/platforms/windows/daemon/windowsfirewall.h"
|
||||||
|
#include "../client/platforms/windows/daemon/windowsdaemon.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_LINUX
|
#ifdef Q_OS_LINUX
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
|
|
||||||
IpcServer::IpcServer(QObject *parent):
|
IpcServer::IpcServer(QObject *parent):
|
||||||
IpcInterfaceSource(parent)
|
IpcInterfaceSource(parent)
|
||||||
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
int IpcServer::createPrivilegedProcess()
|
int IpcServer::createPrivilegedProcess()
|
||||||
|
@ -313,12 +315,13 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr)
|
||||||
config.m_dnsServer = configStr.value(amnezia::config_key::dns1).toString();
|
config.m_dnsServer = configStr.value(amnezia::config_key::dns1).toString();
|
||||||
config.m_serverPublicKey = "openvpn";
|
config.m_serverPublicKey = "openvpn";
|
||||||
config.m_serverIpv4Gateway = configStr.value("vpnGateway").toString();
|
config.m_serverIpv4Gateway = configStr.value("vpnGateway").toString();
|
||||||
|
config.m_serverIpv4AddrIn = configStr.value("vpnServer").toString();
|
||||||
|
int vpnAdapterIndex = configStr.value("vpnAdapterIndex").toInt();
|
||||||
|
int inetAdapterIndex = configStr.value("inetAdapterIndex").toInt();
|
||||||
|
|
||||||
int splitTunnelType = configStr.value("splitTunnelType").toInt();
|
int splitTunnelType = configStr.value("splitTunnelType").toInt();
|
||||||
QJsonArray splitTunnelSites = configStr.value("splitTunnelSites").toArray();
|
QJsonArray splitTunnelSites = configStr.value("splitTunnelSites").toArray();
|
||||||
|
|
||||||
qDebug() << "splitTunnelType " << splitTunnelType << "splitTunnelSites " << splitTunnelSites;
|
|
||||||
|
|
||||||
QStringList AllowedIPAddesses;
|
QStringList AllowedIPAddesses;
|
||||||
|
|
||||||
// Use APP split tunnel
|
// Use APP split tunnel
|
||||||
|
@ -332,7 +335,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr)
|
||||||
if (splitTunnelType == 1) {
|
if (splitTunnelType == 1) {
|
||||||
for (auto v : splitTunnelSites) {
|
for (auto v : splitTunnelSites) {
|
||||||
QString ipRange = v.toString();
|
QString ipRange = v.toString();
|
||||||
if (ipRange.split('/').size() > 1){
|
if (ipRange.split('/').size() > 1) {
|
||||||
config.m_allowedIPAddressRanges.append(
|
config.m_allowedIPAddressRanges.append(
|
||||||
IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit())));
|
IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit())));
|
||||||
} else {
|
} else {
|
||||||
|
@ -350,7 +353,17 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return WindowsFirewall::instance()->enablePeerTraffic(config);
|
for (const QJsonValue& i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) {
|
||||||
|
if (!i.isString()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
config.m_vpnDisabledApps.append(i.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsFirewall::instance()->enablePeerTraffic(config);
|
||||||
|
WindowsDaemon::instance()->prepareActivation(config, inetAdapterIndex);
|
||||||
|
WindowsDaemon::instance()->activateSplitTunnel(config, vpnAdapterIndex);
|
||||||
|
return true;
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue