From adab30fc81b387455f4ce1ebdc5f103c8bfaa181 Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 1 Apr 2024 18:45:00 +0700 Subject: [PATCH] feature/app-split-tunneling (#702) App Split Tunneling for Windows and Android --- client/amnezia_application.cpp | 15 +- client/amnezia_application.h | 4 + client/android/AndroidManifest.xml | 4 +- client/android/awg/src/main/kotlin/Awg.kt | 1 + .../amnezia/vpn/protocol/openvpn/OpenVpn.kt | 1 + .../protocolApi/src/main/kotlin/Protocol.kt | 21 ++ .../src/main/kotlin/ProtocolConfig.kt | 6 + .../src/org/amnezia/vpn/AmneziaActivity.kt | 24 ++ .../src/org/amnezia/vpn/AppListProvider.kt | 73 +++++ .../vpn/protocol/wireguard/Wireguard.kt | 1 + client/cmake/android.cmake | 4 +- client/core/defs.h | 14 + client/core/installedAppsImageProvider.cpp | 12 + client/core/installedAppsImageProvider.h | 15 + client/core/networkUtilities.cpp | 23 ++ client/core/networkUtilities.h | 4 + client/daemon/daemon.h | 6 +- client/mozilla/localsocketcontroller.cpp | 10 +- .../platforms/android/android_controller.cpp | 43 +++ client/platforms/android/android_controller.h | 3 + .../windows/daemon/windowsdaemon.cpp | 28 +- .../platforms/windows/daemon/windowsdaemon.h | 3 +- .../windows/daemon/windowssplittunnel.cpp | 13 +- .../windows/daemon/windowssplittunnel.h | 4 +- client/platforms/windows/windowscommons.cpp | 18 -- client/platforms/windows/windowscommons.h | 3 +- client/protocols/openvpnprotocol.cpp | 3 + client/protocols/protocols_defs.h | 3 + client/protocols/xrayprotocol.cpp | 3 + client/resources.qrc | 2 + client/settings.cpp | 48 ++++ client/settings.h | 15 + .../appSplitTunnelingController.cpp | 50 ++++ .../controllers/appSplitTunnelingController.h | 31 +++ client/ui/controllers/pageController.h | 1 + client/ui/models/appSplitTunnelingModel.cpp | 101 +++++++ client/ui/models/appSplitTunnelingModel.h | 55 ++++ client/ui/models/installedAppsModel.cpp | 96 +++++++ client/ui/models/installedAppsModel.h | 38 +++ .../Components/HomeSplitTunnelingDrawer.qml | 4 +- .../ui/qml/Components/InstalledAppsDrawer.qml | 136 ++++++++++ client/ui/qml/Controls2/CheckBoxType.qml | 8 +- .../qml/Controls2/TextFieldWithHeaderType.qml | 48 ++-- .../Pages2/PageSettingsAppSplitTunneling.qml | 256 ++++++++++++++++++ .../ui/qml/Pages2/PageSettingsConnection.qml | 11 +- .../qml/Pages2/PageSettingsSplitTunneling.qml | 29 +- client/vpnconnection.cpp | 11 + ipc/ipcserver.cpp | 21 +- 48 files changed, 1225 insertions(+), 98 deletions(-) create mode 100644 client/android/src/org/amnezia/vpn/AppListProvider.kt create mode 100644 client/core/installedAppsImageProvider.cpp create mode 100644 client/core/installedAppsImageProvider.h create mode 100644 client/ui/controllers/appSplitTunnelingController.cpp create mode 100644 client/ui/controllers/appSplitTunnelingController.h create mode 100644 client/ui/models/appSplitTunnelingModel.cpp create mode 100644 client/ui/models/appSplitTunnelingModel.h create mode 100644 client/ui/models/installedAppsModel.cpp create mode 100644 client/ui/models/installedAppsModel.h create mode 100644 client/ui/qml/Components/InstalledAppsDrawer.qml create mode 100644 client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 0fd9a394..c288feb4 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -14,10 +14,12 @@ #include "logger.h" #include "version.h" +#include "ui/models/installedAppsModel.h" #include "platforms/ios/QRCodeReaderBase.h" #if defined(Q_OS_ANDROID) #include "platforms/android/android_controller.h" + #include "core/installedAppsImageProvider.h" #endif #include "protocols/qml_register_protocols.h" @@ -124,8 +126,12 @@ void AmneziaApplication::init() m_importController->extractConfigFromData(data); m_pageController->goToPageViewConfig(); }); + + m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); #endif + + #ifdef Q_OS_IOS IosController::Instance()->initialize(); 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, "ContainersModelFilters"); - // + qmlRegisterType("InstalledAppsModel", 1, 0, "InstalledAppsModel"); + Vpn::declareQmlVpnConnectionStateEnum(); PageLoader::declareQmlPageEnum(); } @@ -325,6 +332,9 @@ void AmneziaApplication::initModels() m_sitesModel.reset(new SitesModel(m_settings, this)); 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_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_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_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 51e5a929..9e073d3f 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -25,6 +25,7 @@ #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" #include "ui/controllers/apiController.h" +#include "ui/controllers/appSplitTunnelingController.h" #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" #include "ui/models/protocols/cloakConfigModel.h" @@ -42,6 +43,7 @@ #include "ui/models/services/sftpConfigModel.h" #include "ui/models/sites_model.h" #include "ui/models/clientManagementModel.h" +#include "ui/models/appSplitTunnelingModel.h" #define amnApp (static_cast(QCoreApplication::instance())) @@ -98,6 +100,7 @@ private: QSharedPointer m_languageModel; QSharedPointer m_protocolsModel; QSharedPointer m_sitesModel; + QSharedPointer m_appSplitTunnelingModel; QSharedPointer m_clientManagementModel; QScopedPointer m_openVpnConfigModel; @@ -125,6 +128,7 @@ private: QScopedPointer m_sitesController; QScopedPointer m_systemController; QScopedPointer m_apiController; + QScopedPointer m_appSplitTunnelingController; }; #endif // AMNEZIA_APPLICATION_H diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 548c9bb9..9637b029 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -24,9 +24,7 @@ - - - + ::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) diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt index 78a24e82..5731de6c 100644 --- a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt +++ b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt @@ -16,6 +16,7 @@ open class ProtocolConfig protected constructor( val excludedRoutes: Set, val includedAddresses: Set, val excludedAddresses: Set, + val includedApplications: Set, val excludedApplications: Set, 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 = hashSetOf() internal val includedAddresses: MutableSet = hashSetOf() internal val excludedAddresses: MutableSet = hashSetOf() + internal val includedApplications: MutableSet = hashSetOf() internal val excludedApplications: MutableSet = 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) = apply { this.excludedAddresses += addresses } + fun includeApplication(application: String) = apply { this.includedApplications += application } + fun includeApplications(applications: Collection) = apply { this.includedApplications += applications } + fun excludeApplication(application: String) = apply { this.excludedApplications += application } fun excludeApplications(applications: Collection) = apply { this.excludedApplications += applications } diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index d909431e..b24c2024 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -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) + } } diff --git a/client/android/src/org/amnezia/vpn/AppListProvider.kt b/client/android/src/org/amnezia/vpn/AppListProvider.kt new file mode 100644 index 00000000..16d17e03 --- /dev/null +++ b/client/android/src/org/amnezia/vpn/AppListProvider.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 9b54cfb5..09482918 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -95,6 +95,7 @@ open class Wireguard : Protocol() { return WireguardConfig.build { configWireguard(configData, configDataJson) configSplitTunneling(config) + configAppSplitTunneling(config) } } diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 20fc617d..aa22456f 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -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 # 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) @@ -30,6 +30,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h + ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h ) 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/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp ) foreach(abi IN ITEMS ${QT_ANDROID_ABIS}) diff --git a/client/core/defs.h b/client/core/defs.h index 060499ef..7d179ea6 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -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 { // General error codes NoError = 0, diff --git a/client/core/installedAppsImageProvider.cpp b/client/core/installedAppsImageProvider.cpp new file mode 100644 index 00000000..cd34693c --- /dev/null +++ b/client/core/installedAppsImageProvider.cpp @@ -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); +} diff --git a/client/core/installedAppsImageProvider.h b/client/core/installedAppsImageProvider.h new file mode 100644 index 00000000..dfc46dd4 --- /dev/null +++ b/client/core/installedAppsImageProvider.h @@ -0,0 +1,15 @@ +#ifndef INSTALLEDAPPSIMAGEPROVIDER_H +#define INSTALLEDAPPSIMAGEPROVIDER_H + +#include +#include + +class InstalledAppsImageProvider : public QQuickImageProvider +{ +public: + InstalledAppsImageProvider(); + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +}; + +#endif // INSTALLEDAPPSIMAGEPROVIDER_H diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index 3dfab733..7ffd4c41 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -10,6 +10,8 @@ #include #include #include + #include + #include "qendian.h" #endif #ifdef Q_OS_LINUX #include @@ -159,6 +161,27 @@ bool NetworkUtilities::checkIpSubnetFormat(const QString &ip) 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 DWORD GetAdaptersAddressesWrapper(const ULONG Family, const ULONG Flags, diff --git a/client/core/networkUtilities.h b/client/core/networkUtilities.h index 57c6cb6f..3057b852 100644 --- a/client/core/networkUtilities.h +++ b/client/core/networkUtilities.h @@ -4,6 +4,8 @@ #include #include #include +#include + class NetworkUtilities : public QObject { @@ -14,6 +16,8 @@ public: static bool checkIPv4Format(const QString &ip); static bool checkIpSubnetFormat(const QString &ip); static QString getGatewayAndIface(); + // Returns the Interface Index that could Route to dst + static int AdapterIndexTo(const QHostAddress& dst); static QRegularExpression ipAddressRegExp(); static QRegularExpression ipAddressPortRegExp(); diff --git a/client/daemon/daemon.h b/client/daemon/daemon.h index 7e323ccd..d3d8c34d 100644 --- a/client/daemon/daemon.h +++ b/client/daemon/daemon.h @@ -35,8 +35,10 @@ class Daemon : public QObject { virtual QJsonObject getStatus(); // Callback before any Activating measure is done - virtual void prepareActivation(const InterfaceConfig& config){ - Q_UNUSED(config)}; + virtual void prepareActivation(const InterfaceConfig& config, int inetAdapterIndex = 0) { + Q_UNUSED(config) }; + virtual void activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) { + Q_UNUSED(config) }; QString logs(); void cleanLogs(); diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 3e4309eb..420cabee 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -117,6 +117,9 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { int splitTunnelType = rawConfig.value("splitTunnelType").toInt(); 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 json; @@ -217,12 +220,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { json.insert("excludedAddresses", jsExcludedAddresses); - - // QJsonArray splitTunnelApps; - // for (const auto& uri : hop.m_vpnDisabledApps) { - // splitTunnelApps.append(QJsonValue(uri)); - // } - // json.insert("vpnDisabledApps", splitTunnelApps); + json.insert("vpnDisabledApps", splitTunnelApps); if (protocolName == amnezia::config_key::awg) { json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount)); diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 328ceebc..ce2aeb4c 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -2,6 +2,9 @@ #include #include #include +#include + +#include #include "android_controller.h" #include "android_utils.h" @@ -214,6 +217,46 @@ void AndroidController::minimizeApp() callActivityMethod("minimizeApp", "()V"); } +QJsonArray AndroidController::getAppList() +{ + QJniObject appList = callActivityMethod("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("getAppIcon", "(Ljava/lang/String;II)Landroid/graphics/Bitmap;", + QJniObject::fromString(package).object(), + 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(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 jclass AndroidController::log; jmethodID AndroidController::logDebug; diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index d6eedf85..15de0ccc 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -2,6 +2,7 @@ #define ANDROID_CONTROLLER_H #include +#include #include "protocols/vpnprotocol.h" @@ -41,6 +42,8 @@ public: void clearLogs(); void setScreenshotsEnabled(bool enabled); void minimizeApp(); + QJsonArray getAppList(); + QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); diff --git a/client/platforms/windows/daemon/windowsdaemon.cpp b/client/platforms/windows/daemon/windowsdaemon.cpp index b697a3b0..00435f0b 100644 --- a/client/platforms/windows/daemon/windowsdaemon.cpp +++ b/client/platforms/windows/daemon/windowsdaemon.cpp @@ -18,6 +18,7 @@ #include "dnsutilswindows.h" #include "leakdetector.h" #include "logger.h" +#include "core/networkUtilities.h" #include "platforms/windows/windowscommons.h" #include "platforms/windows/windowsservicemanager.h" #include "windowsfirewall.h" @@ -43,11 +44,24 @@ WindowsDaemon::~WindowsDaemon() { 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 // routes to the server endpoint - auto serveraddr = QHostAddress(config.m_serverIpv4AddrIn); - m_inetAdapterIndex = WindowsCommons::AdapterIndexTo(serveraddr); + if (inetAdapterIndex == 0) { + 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) { @@ -64,12 +78,8 @@ bool WindowsDaemon::run(Op op, const InterfaceConfig& config) { } } - if (config.m_vpnDisabledApps.length() > 0) { - m_splitTunnelManager.start(m_inetAdapterIndex); - m_splitTunnelManager.setRules(config.m_vpnDisabledApps); - } else { - m_splitTunnelManager.stop(); - } + activateSplitTunnel(config); + return true; } diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h index 7a1b3059..782c6814 100644 --- a/client/platforms/windows/daemon/windowsdaemon.h +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -20,7 +20,8 @@ class WindowsDaemon final : public Daemon { 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: bool run(Op op, const InterfaceConfig& config) override; diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp index 26d22ae8..eb93bd26 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.cpp +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -134,7 +134,7 @@ void WindowsSplitTunnel::setRules(const QStringList& appPaths) { 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: // Network info (what is vpn what is network) logger.debug() << "Starting SplitTunnel"; @@ -171,7 +171,7 @@ void WindowsSplitTunnel::start(int inetAdapterIndex) { } 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], (DWORD)config.size(), nullptr, 0, &bytesReturned, nullptr); @@ -270,14 +270,19 @@ std::vector WindowsSplitTunnel::generateAppConfiguration( } std::vector WindowsSplitTunnel::generateIPConfiguration( - int inetAdapterIndex) { + int inetAdapterIndex, int vpnAdapterIndex) { std::vector out(sizeof(IP_ADDRESSES_CONFIG)); auto config = reinterpret_cast(&out[0]); auto ifaces = QNetworkInterface::allInterfaces(); + + if (vpnAdapterIndex == 0) { + vpnAdapterIndex = WindowsCommons::VPNAdapterIndex(); + } + // Always the VPN - getAddress(WindowsCommons::VPNAdapterIndex(), &config->TunnelIpv4, + getAddress(vpnAdapterIndex, &config->TunnelIpv4, &config->TunnelIpv6); // 2nd best route getAddress(inetAdapterIndex, &config->InternetIpv4, &config->InternetIpv6); diff --git a/client/platforms/windows/daemon/windowssplittunnel.h b/client/platforms/windows/daemon/windowssplittunnel.h index 10a96f11..efe84395 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.h +++ b/client/platforms/windows/daemon/windowssplittunnel.h @@ -132,7 +132,7 @@ class WindowsSplitTunnel final : public QObject { void setRules(const QStringList& appPaths); // 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 void stop(); // Resets the Whole Driver @@ -164,7 +164,7 @@ class WindowsSplitTunnel final : public QObject { // Generates a Configuration for Each APP std::vector generateAppConfiguration(const QStringList& appPaths); // Generates a Configuration which IP's are VPN and which network - std::vector generateIPConfiguration(int inetAdapterIndex); + std::vector generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0); std::vector generateProcessBlob(); void getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6); diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp index dd9583d6..395859aa 100644 --- a/client/platforms/windows/windowscommons.cpp +++ b/client/platforms/windows/windowscommons.cpp @@ -88,24 +88,6 @@ QString WindowsCommons::tunnelLogFile() { 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 int WindowsCommons::VPNAdapterIndex() { // For someReason QNetworkInterface::fromName(MozillaVPN) does not work >:( diff --git a/client/platforms/windows/windowscommons.h b/client/platforms/windows/windowscommons.h index f529e375..eb3e675c 100644 --- a/client/platforms/windows/windowscommons.h +++ b/client/platforms/windows/windowscommons.h @@ -19,8 +19,7 @@ class WindowsCommons final { // Returns the Interface Index of the VPN Adapter 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 static QString getCurrentPath(); }; diff --git a/client/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index 05d33ccb..61db69fc 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -331,13 +331,16 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) m_vpnLocalAddress = l.split(" ").at(1); m_vpnGateway = l.split(" ").at(2); #ifdef Q_OS_WIN + QThread::msleep(300); QList netInterfaces = QNetworkInterface::allInterfaces(); for (int i = 0; i < netInterfaces.size(); i++) { for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++) { if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { 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("vpnServer", m_configData.value(amnezia::config_key::hostName).toString()); IpcClient::Interface()->enablePeerTraffic(m_configData); } } diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index 3d6da3be..8cf7cfd2 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -89,6 +89,9 @@ namespace amnezia constexpr char splitTunnelSites[] = "splitTunnelSites"; constexpr char splitTunnelType[] = "splitTunnelType"; + constexpr char splitTunnelApps[] = "splitTunnelApps"; + constexpr char appSplitTunnelType[] = "appSplitTunnelType"; + constexpr char crc[] = "crc"; } diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 8f4800bc..6bc92dd2 100644 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -112,6 +112,7 @@ ErrorCode XrayProtocol::startTun2Sock() m_t2sProcess->setProgram(PermittedProcess::Tun2Socks); #ifdef Q_OS_WIN + m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress))); 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)}); #endif @@ -166,7 +167,9 @@ ErrorCode XrayProtocol::startTun2Sock() { if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { 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("vpnServer", m_remoteAddress); IpcClient::Interface()->enablePeerTraffic(m_configData); } } diff --git a/client/resources.qrc b/client/resources.qrc index 35bbb6de..49fd66d3 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -234,6 +234,8 @@ ui/qml/Components/HomeSplitTunnelingDrawer.qml images/controls/split-tunneling.svg ui/qml/Controls2/DrawerType2.qml + ui/qml/Pages2/PageSettingsAppSplitTunneling.qml + ui/qml/Components/InstalledAppsDrawer.qml images/controls/alert-circle.svg images/controls/file-check-2.svg ui/qml/Controls2/WarningType.qml diff --git a/client/settings.cpp b/client/settings.cpp index fc5591e9..3523bcfe 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -355,6 +355,54 @@ void Settings::clearSettings() 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(value("Conf/appsRouteMode", 0).toInt()); +} + +void Settings::setAppsRouteMode(AppsRouteMode mode) +{ + setValue("Conf/appsRouteMode", mode); +} + +QVector Settings::getVpnApps(AppsRouteMode mode) const +{ + QVector 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 &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 { return serverCredentials(defaultServerIndex()); diff --git a/client/settings.h b/client/settings.h index f15ba51e..59c6b13c 100644 --- a/client/settings.h +++ b/client/settings.h @@ -193,6 +193,21 @@ public: void clearSettings(); + enum AppsRouteMode { + VpnAllApps, + VpnOnlyForwardApps, + VpnAllExceptApps + }; + Q_ENUM(AppsRouteMode) + + QString appsRouteModeString(AppsRouteMode mode) const; + + AppsRouteMode getAppsRouteMode() const; + void setAppsRouteMode(AppsRouteMode mode); + + QVector getVpnApps(AppsRouteMode mode) const; + void setVpnApps(AppsRouteMode mode, const QVector &apps); + signals: void saveLogsChanged(bool enabled); void screenshotsEnabledChanged(bool enabled); diff --git a/client/ui/controllers/appSplitTunnelingController.cpp b/client/ui/controllers/appSplitTunnelingController.cpp new file mode 100644 index 00000000..ea652a67 --- /dev/null +++ b/client/ui/controllers/appSplitTunnelingController.cpp @@ -0,0 +1,50 @@ +#include "appSplitTunnelingController.h" + +#include + +#include "core/defs.h" + +AppSplitTunnelingController::AppSplitTunnelingController(const std::shared_ptr &settings, + const QSharedPointer &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> 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())); +} diff --git a/client/ui/controllers/appSplitTunnelingController.h b/client/ui/controllers/appSplitTunnelingController.h new file mode 100644 index 00000000..da1009ec --- /dev/null +++ b/client/ui/controllers/appSplitTunnelingController.h @@ -0,0 +1,31 @@ +#ifndef APPSPLITTUNNELINGCONTROLLER_H +#define APPSPLITTUNNELINGCONTROLLER_H + +#include + +#include "settings.h" +#include "ui/models/appSplitTunnelingModel.h" + +class AppSplitTunnelingController : public QObject +{ + Q_OBJECT +public: + explicit AppSplitTunnelingController(const std::shared_ptr &settings, + const QSharedPointer &sitesModel, QObject *parent = nullptr); + +public slots: + void addApp(const QString &appPath); + void addApps(QVector> apps); + void removeApp(const int index); + +signals: + void errorOccurred(const QString &errorMessage); + void finished(const QString &message); + +private: + std::shared_ptr m_settings; + + QSharedPointer m_appSplitTunnelingModel; +}; + +#endif // APPSPLITTUNNELINGCONTROLLER_H diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 3924ea84..0b456a0d 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -29,6 +29,7 @@ namespace PageLoader PageSettingsAbout, PageSettingsLogging, PageSettingsSplitTunneling, + PageSettingsAppSplitTunneling, PageServiceSftpSettings, PageServiceTorWebsiteSettings, diff --git a/client/ui/models/appSplitTunnelingModel.cpp b/client/ui/models/appSplitTunnelingModel.cpp new file mode 100644 index 00000000..6aba5606 --- /dev/null +++ b/client/ui/models/appSplitTunnelingModel.cpp @@ -0,0 +1,101 @@ +#include "appSplitTunnelingModel.h" + +#include + +AppSplitTunnelingModel::AppSplitTunnelingModel(std::shared_ptr 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(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(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 AppSplitTunnelingModel::roleNames() const +{ + QHash roles; + roles[AppPathRole] = "appPath"; + return roles; +} diff --git a/client/ui/models/appSplitTunnelingModel.h b/client/ui/models/appSplitTunnelingModel.h new file mode 100644 index 00000000..b96c0472 --- /dev/null +++ b/client/ui/models/appSplitTunnelingModel.h @@ -0,0 +1,55 @@ +#ifndef APPSPLITTUNNELINGMODEL_H +#define APPSPLITTUNNELINGMODEL_H + +#include + +#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, 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 roleNames() const override; + +private: + std::shared_ptr m_settings; + + bool m_isSplitTunnelingEnabled; + Settings::AppsRouteMode m_currentRouteMode; + + QVector m_apps; +}; + +#endif // APPSPLITTUNNELINGMODEL_H diff --git a/client/ui/models/installedAppsModel.cpp b/client/ui/models/installedAppsModel.cpp new file mode 100644 index 00000000..7255b4cc --- /dev/null +++ b/client/ui/models/installedAppsModel.cpp @@ -0,0 +1,96 @@ +#include "installedAppsModel.h" + +#include +#include + +#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(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> InstalledAppsModel::getSelectedAppsInfo() +{ + QVector> 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 future = QtConcurrent::run([this]() { + beginResetModel(); +#ifdef Q_OS_ANDROID + m_installedApps = AndroidController::instance()->getAppList(); +#endif + endResetModel(); + }); + + QFutureWatcher watcher; + QEventLoop wait; + connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); + watcher.setFuture(future); + wait.exec(); + + return; +} + +QHash InstalledAppsModel::roleNames() const +{ + QHash roles; + roles[AppNameRole] = "appName"; + roles[AppIconRole] = "appIcon"; + roles[PackageNameRole] = "packageName"; + return roles; +} diff --git a/client/ui/models/installedAppsModel.h b/client/ui/models/installedAppsModel.h new file mode 100644 index 00000000..32f67630 --- /dev/null +++ b/client/ui/models/installedAppsModel.h @@ -0,0 +1,38 @@ +#ifndef INSTALLEDAPPSMODEL_H +#define INSTALLEDAPPSMODEL_H + +#include +#include + +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> getSelectedAppsInfo(); + + void updateModel(); + +protected: + QHash roleNames() const override; + +private: + QJsonArray m_installedApps; + QSet m_selectedAppIndexes; +}; + +#endif // INSTALLEDAPPSMODEL_H diff --git a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml index bc1f1008..9cc1983e 100644 --- a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml +++ b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml @@ -57,7 +57,7 @@ DrawerType2 { Layout.fillWidth: true Layout.topMargin: 16 - enabled: ! ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi") + enabled: !ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi") text: qsTr("Site-based split tunneling") descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") @@ -80,7 +80,7 @@ DrawerType2 { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { -// PageController.goToPage(PageEnum.PageSetupWizardConfigSource) + PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) root.close() } } diff --git a/client/ui/qml/Components/InstalledAppsDrawer.qml b/client/ui/qml/Components/InstalledAppsDrawer.qml new file mode 100644 index 00000000..b4e45e1a --- /dev/null +++ b/client/ui/qml/Components/InstalledAppsDrawer.qml @@ -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() + } + } + } +} diff --git a/client/ui/qml/Controls2/CheckBoxType.qml b/client/ui/qml/Controls2/CheckBoxType.qml index 1ad3b412..962c7fbd 100644 --- a/client/ui/qml/Controls2/CheckBoxType.qml +++ b/client/ui/qml/Controls2/CheckBoxType.qml @@ -90,12 +90,12 @@ CheckBox { } contentItem: Item { - implicitWidth: content.implicitWidth - implicitHeight: content.implicitHeight - - anchors.fill: parent + anchors.left: parent.left + anchors.right: parent.right anchors.leftMargin: 8 + background.width + implicitHeight: content.implicitHeight + ColumnLayout { id: content diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 6069bc38..48f93f13 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -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) { return textField.focus ? root.borderFocusedColor : noneFocusedColor } diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml new file mode 100644 index 00000000..e787d87c --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -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 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 + } +} diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index 4d88b397..9ec49231 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -11,6 +11,8 @@ import "../Config" PageType { id: root + property bool isAppSplitTinnelingEnabled: Qt.platform.os === "windows" || Qt.platform.os === "android" + BackButtonType { id: backButton @@ -73,8 +75,6 @@ PageType { DividerType {} LabelWithButtonType { - visible: true - Layout.fillWidth: true text: qsTr("Site-based split tunneling") @@ -87,11 +87,11 @@ PageType { } DividerType { - visible: GC.isDesktop() + visible: root.isAppSplitTinnelingEnabled } LabelWithButtonType { - visible: false + visible: root.isAppSplitTinnelingEnabled Layout.fillWidth: true @@ -100,11 +100,12 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) } } DividerType { - visible: false + visible: root.isAppSplitTinnelingEnabled } } } diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 1ce3cd64..f50cda1d 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -22,7 +22,7 @@ PageType { property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi") - defaultActiveFocusItem: website_ip_field.textField + defaultActiveFocusItem: searchField.textField property bool pageEnabled: { return !ConnectionController.isConnected && !isServerFromApi @@ -188,7 +188,24 @@ PageType { width: parent.width 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 interactive: false @@ -218,7 +235,7 @@ PageType { var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { - SitesController.removeSite(index) + SitesController.removeSite(proxySitesModel.mapToSource(index)) } var noButtonFunction = function() { } @@ -255,7 +272,7 @@ PageType { anchors.bottomMargin: 24 TextFieldWithHeaderType { - id: website_ip_field + id: searchField Layout.fillWidth: true @@ -430,8 +447,4 @@ PageType { } } } - - QuestionDrawer { - id: questionDrawer - } } diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index be5c2f50..b8184053 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -428,6 +428,17 @@ void VpnConnection::appendSplitTunnelingConfig() m_vpnConfiguration.insert(config_key::splitTunnelType, routeMode); 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 diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 18a82ef8..2f4e6296 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -12,6 +12,7 @@ #ifdef Q_OS_WIN #include "tapcontroller_win.h" #include "../client/platforms/windows/daemon/windowsfirewall.h" +#include "../client/platforms/windows/daemon/windowsdaemon.h" #endif #ifdef Q_OS_LINUX @@ -24,6 +25,7 @@ IpcServer::IpcServer(QObject *parent): IpcInterfaceSource(parent) + {} 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_serverPublicKey = "openvpn"; 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(); QJsonArray splitTunnelSites = configStr.value("splitTunnelSites").toArray(); - qDebug() << "splitTunnelType " << splitTunnelType << "splitTunnelSites " << splitTunnelSites; - QStringList AllowedIPAddesses; // Use APP split tunnel @@ -332,7 +335,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) if (splitTunnelType == 1) { for (auto v : splitTunnelSites) { QString ipRange = v.toString(); - if (ipRange.split('/').size() > 1){ + if (ipRange.split('/').size() > 1) { config.m_allowedIPAddressRanges.append( IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); } 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 return true; }