Compare commits

...
Author SHA1 Message Date
yp 2ce0317840 merge dev 2026-09-30 09:50:23 +03:00
yp 65c7327a67 reset files 2026-09-14 10:14:19 +03:00
yp 4b57f16788 add support include split tunnel AmneziaWG 2026-09-11 18:35:57 +03:00
yp 9bfc90b69e file rollback 2026-09-11 11:20:31 +03:00
ypandCursor 91fd02b6f6 Limit Windows include split tunneling to XRay
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 02:47:00 +03:00
ypandCursor 482f6d8828 Feat: add Windows app include split tunneling
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 02:42:03 +03:00
yp f5b391adf0 reset files 2026-09-11 02:08:55 +03:00
yp 835ff3639e reset cmake file service 2026-09-11 01:41:32 +03:00
yp dc4df33465 reset file 2026-09-10 23:35:02 +03:00
yp a966db46be app split tunneling via default route metric 2026-09-10 19:27:55 +03:00
yp 0f2244e92d Fix: Win xray app split tunneling 2026-09-10 16:57:47 +03:00
25 changed files with 428 additions and 80 deletions

No files matched your search

@@ -148,10 +148,6 @@ ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
newConfigData.value("Conf/appsSplitTunnelingEnabled").toVariant().toString().toLower() == "true";
emit appSplitTunnelingRouteModeChanged(static_cast<AppsRouteMode>(appSplitTunnelingRouteMode));
#if defined(Q_OS_WINDOWS)
emit appSplitTunnelingRouteModeChanged(AppsRouteMode::VpnAllExceptApps);
#endif
if (newConfigData.contains("AppPlatform")) {
if (newConfigData.value("AppPlatform").toString() != getPlatform()) {
emit appSplitTunnelingClearAppsList();
+43 -18
View File
@@ -9,6 +9,7 @@
#include "ipc.h"
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QTimer>
#include <QJsonObject>
@@ -121,6 +122,12 @@ void XrayProtocol::stop()
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
#ifdef Q_OS_WIN
auto routeDeleteDefault = iface->routeDeleteDefault(tunName);
if (!routeDeleteDefault.waitForFinished() || !routeDeleteDefault.returnValue())
qWarning() << "Failed to delete the default route for TUN";
#endif
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
@@ -254,6 +261,12 @@ ErrorCode XrayProtocol::setupRouting()
[this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
#ifdef Q_OS_WIN
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
const bool hasSplitTunnelApps =
!m_rawConfig.value(amnezia::configKey::splitTunnelApps).toArray().isEmpty();
const bool includeOnlyApps =
hasSplitTunnelApps &&
m_rawConfig.value(amnezia::configKey::appSplitTunnelType).toInt() ==
static_cast<int>(amnezia::AppsRouteMode::VpnOnlyForwardApps);
#endif
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
if (!createTun.waitForFinished() || !createTun.returnValue()) {
@@ -294,23 +307,6 @@ ErrorCode XrayProtocol::setupRouting()
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
}
if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
return ErrorCode::InternalError;
}
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
@@ -324,9 +320,38 @@ ErrorCode XrayProtocol::setupRouting()
qCritical() << "Failed to enable peer traffic";
return ErrorCode::InternalError;
}
} else
} else {
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
if (includeOnlyApps) {
return ErrorCode::InternalError;
}
}
#endif
if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
#ifdef Q_OS_WIN
auto routeAddDefault = iface->routeAddDefault(tunName);
if (!routeAddDefault.waitForFinished() || !routeAddDefault.returnValue()) {
qCritical() << "Failed to set the default route for TUN";
return ErrorCode::InternalError;
}
#else
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
return ErrorCode::InternalError;
}
#endif
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
return ErrorCode::InternalError;
}
return ErrorCode::NoError;
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
+1
View File
@@ -386,6 +386,7 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
if (!parseStringList(obj, "vpnDisabledApps", config.m_vpnDisabledApps)) {
return false;
}
config.m_appSplitTunnelType = obj.value("appSplitTunnelType").toInt();
if (!parseStringList(obj, "allowedDnsServers", config.m_allowedDnsServers)) {
return false;
}
+2 -2
View File
@@ -39,8 +39,8 @@ class Daemon : public QObject {
// Callback before any Activating measure is done
virtual void prepareActivation(const InterfaceConfig& config, int inetAdapterIndex = 0) {
Q_UNUSED(config) };
virtual void activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) {
Q_UNUSED(config) };
virtual bool activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) {
Q_UNUSED(config) Q_UNUSED(vpnAdapterIndex) return true; };
QString logs();
void cleanLogs();
+1
View File
@@ -64,6 +64,7 @@ QJsonObject InterfaceConfig::toJson() const {
disabledApps.append(QJsonValue(i));
}
json.insert("vpnDisabledApps", disabledApps);
json.insert("appSplitTunnelType", m_appSplitTunnelType);
return json;
}
+1
View File
@@ -40,6 +40,7 @@ class InterfaceConfig {
QList<IPAddress> m_allowedIPAddressRanges;
QStringList m_excludedAddresses;
QStringList m_vpnDisabledApps;
int m_appSplitTunnelType = 0;
QStringList m_allowedDnsServers;
bool m_killSwitchEnabled;
#if defined(MZ_ANDROID) || defined(MZ_IOS)
+12 -5
View File
@@ -29,6 +29,7 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/routeModes.h"
// How many times do we try to reconnect.
constexpr int MAX_CONNECTION_RETRY = 10;
@@ -199,6 +200,9 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
} else {
// Use APP split tunnel
const bool includeOnlyApps =
appSplitTunnelType == static_cast<int>(amnezia::AppsRouteMode::VpnOnlyForwardApps)
&& !splitTunnelApps.isEmpty();
if (splitTunnelType == 0 || splitTunnelType == 2) {
QJsonObject range_ipv4;
range_ipv4.insert("address", "0.0.0.0");
@@ -206,11 +210,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
range_ipv4.insert("isIpv6", false);
jsAllowedIPAddesses.append(range_ipv4);
QJsonObject range_ipv6;
range_ipv6.insert("address", "::");
range_ipv6.insert("range", 0);
range_ipv6.insert("isIpv6", true);
jsAllowedIPAddesses.append(range_ipv6);
if (!includeOnlyApps) {
QJsonObject range_ipv6;
range_ipv6.insert("address", "::");
range_ipv6.insert("range", 0);
range_ipv6.insert("isIpv6", true);
jsAllowedIPAddesses.append(range_ipv6);
}
}
if (splitTunnelType == 1) {
@@ -247,6 +253,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert("excludedAddresses", jsExcludedAddresses);
json.insert("vpnDisabledApps", splitTunnelApps);
json.insert(amnezia::configKey::appSplitTunnelType, appSplitTunnelType);
json.insert("allowedDnsServers", allowedDns);
@@ -25,6 +25,8 @@
#include "windowsfirewall.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/routeModes.h"
#include "../windowscommons.h"
namespace {
Logger logger("WindowsDaemon");
@@ -61,33 +63,41 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
}
}
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
bool WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
const bool includeOnly =
config.m_appSplitTunnelType ==
static_cast<int>(amnezia::AppsRouteMode::VpnOnlyForwardApps);
if (m_splitTunnelManager == nullptr) {
if (config.m_vpnDisabledApps.length() > 0) {
logger.error() << "Split tunnel manager is not initialized";
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE);
return false;
}
return;
return true;
}
if (config.m_vpnDisabledApps.length() > 0) {
if (!m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex)) {
if (!m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex,
includeOnly)) {
logger.error() << "Failed to start split tunnel";
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
return;
return false;
}
if (!m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps)) {
logger.error() << "Failed to apply split tunnel app exclusions";
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE);
return;
return false;
}
if (!m_splitTunnelManager->isRunning()) {
logger.error() << "Split tunnel did not reach running state";
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
return false;
}
} else {
m_splitTunnelManager->stop();
}
return true;
}
bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
@@ -108,7 +118,14 @@ bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
return true;
}
if (config.m_vpnDisabledApps.length() > 0) {
if (!m_splitTunnelManager->start(m_inetAdapterIndex)) {
const bool includeOnly =
config.m_appSplitTunnelType ==
static_cast<int>(amnezia::AppsRouteMode::VpnOnlyForwardApps);
int vpnAdapterIndex = WindowsCommons::VPNAdapterIndex();
if (vpnAdapterIndex < 0) {
vpnAdapterIndex = 0;
}
if (!m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex, includeOnly)) {
logger.error() << "Split tunnel start failed";
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
return false;
@@ -24,7 +24,7 @@ class WindowsDaemon final : public Daemon {
~WindowsDaemon();
void prepareActivation(const InterfaceConfig& config, int inetAdapterIndex = 0) override;
void activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) override;
bool activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex = 0) override;
protected:
bool run(Op op, const InterfaceConfig& config) override;
@@ -173,7 +173,7 @@ bool WindowsFirewall::initSublayer() {
L"DNS filters for split tunneling");
}
bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
bool WindowsFirewall::enableInterface(int vpnAdapterIndex, bool blockDns) {
// Checks if the FW_Rule was enabled succesfully,
// disables the whole killswitch and returns false if not.
#define FW_OK(rule) \
@@ -216,7 +216,9 @@ bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
FW_OK(allowHyperVTraffic(MAX_WEIGHT, "Allow Hyper-V Traffic"));
FW_OK(allowTrafficForAppOnAll(getCurrentPath(), MAX_WEIGHT,
"Allow all for AmneziaVPN.exe"));
FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS"));
if (blockDns) {
FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS"));
}
FW_OK(allowLoopbackTraffic(MED_WEIGHT,
"Allow Loopback traffic on device %1"));
@@ -287,6 +289,8 @@ bool WindowsFirewall::allowTrafficRange(const QStringList& ranges) {
bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
const bool includeOnly =
config.m_appSplitTunnelType == 1 && !config.m_vpnDisabledApps.isEmpty();
// Start the firewall transaction
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
if (result != ERROR_SUCCESS) {
@@ -301,9 +305,11 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
// Build the firewall rules for this peer.
logger.info() << "Enabling traffic for peer"
<< config.m_serverPublicKey;
if (!blockTrafficTo(config.m_allowedIPAddressRanges, LOW_WEIGHT,
"Block Internet", config.m_serverPublicKey)) {
return false;
if (!includeOnly) {
if (!blockTrafficTo(config.m_allowedIPAddressRanges, LOW_WEIGHT,
"Block Internet", config.m_serverPublicKey)) {
return false;
}
}
if (!config.m_primaryDnsServer.isEmpty()) {
if (!allowTrafficTo(QHostAddress(config.m_primaryDnsServer), 53, HIGH_WEIGHT,
@@ -38,7 +38,7 @@ class WindowsFirewall final : public QObject {
static WindowsFirewall* create(QObject* parent);
~WindowsFirewall() override;
bool enableInterface(int vpnAdapterIndex);
bool enableInterface(int vpnAdapterIndex, bool blockDns = true);
bool enableLanBypass(const QList<IPAddress>& ranges);
bool enablePeerTraffic(const InterfaceConfig& config);
bool disablePeerTraffic(const QString& pubkey);
@@ -319,7 +319,8 @@ bool WindowsSplitTunnel::excludeApps(const QStringList& appPaths) {
return true;
}
bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) {
bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex,
bool invertTunnelInternet) {
// To Start we need to send 2 things:
// Network info (what is vpn what is network)
logger.debug() << "Starting SplitTunnel";
@@ -361,7 +362,8 @@ bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) {
}
logger.debug() << "Driver is ready || new State:" << stateString();
auto config = generateIPConfiguration(inetAdapterIndex, vpnAdapterIndex);
auto config = generateIPConfiguration(inetAdapterIndex, vpnAdapterIndex,
invertTunnelInternet);
if (config.empty()) {
logger.error() << "Network configuration blob is empty. Internet adapter:"
<< inetAdapterIndex << "VPN adapter:" << vpnAdapterIndex;
@@ -480,7 +482,7 @@ std::vector<uint8_t> WindowsSplitTunnel::generateAppConfiguration(
}
std::vector<std::byte> WindowsSplitTunnel::generateIPConfiguration(
int inetAdapterIndex, int vpnAdapterIndex) {
int inetAdapterIndex, int vpnAdapterIndex, bool invertTunnelInternet) {
std::vector<std::byte> out(sizeof(IP_ADDRESSES_CONFIG));
auto config = reinterpret_cast<IP_ADDRESSES_CONFIG*>(&out[0]);
@@ -488,46 +490,68 @@ std::vector<std::byte> WindowsSplitTunnel::generateIPConfiguration(
if (vpnAdapterIndex == 0) {
vpnAdapterIndex = WindowsCommons::VPNAdapterIndex();
}
// Always the VPN
if (!getAddress(vpnAdapterIndex, &config->TunnelIpv4,
&config->TunnelIpv6)) {
IN_ADDR vpnIpv4{};
IN6_ADDR vpnIpv6{};
if (!getAddress(vpnAdapterIndex, &vpnIpv4, &vpnIpv6)) {
return {};
}
// 2nd best route is usually the internet adapter
if (!getAddress(inetAdapterIndex, &config->InternetIpv4,
&config->InternetIpv6)) {
IN_ADDR internetIpv4{};
IN6_ADDR internetIpv6{};
if (!getAddress(inetAdapterIndex, &internetIpv4, &internetIpv6)) {
return {};
};
}
if (invertTunnelInternet) {
config->TunnelIpv4 = internetIpv4;
config->InternetIpv4 = vpnIpv4;
if (!IN6_IS_ADDR_UNSPECIFIED(&vpnIpv6)) {
config->TunnelIpv6 = internetIpv6;
config->InternetIpv6 = vpnIpv6;
}
} else {
config->TunnelIpv4 = vpnIpv4;
config->TunnelIpv6 = vpnIpv6;
config->InternetIpv4 = internetIpv4;
config->InternetIpv6 = internetIpv6;
}
return out;
}
bool WindowsSplitTunnel::getAddress(int adapterIndex, IN_ADDR* out_ipv4,
IN6_ADDR* out_ipv6) {
QNetworkInterface target =
QNetworkInterface::interfaceFromIndex(adapterIndex);
logger.debug() << "Getting adapter info for:" << target.humanReadableName();
auto get = [&target](QAbstractSocket::NetworkLayerProtocol protocol) {
for (auto address : target.addressEntries()) {
if (address.ip().protocol() != protocol) {
for (const QNetworkAddressEntry& entry : target.addressEntries()) {
const QHostAddress address = entry.ip();
if (address.protocol() != protocol) {
continue;
}
return address.ip().toString().toStdWString();
if (address.isNull() || address.isLoopback() || address.isLinkLocal() ||
address.isBroadcast() || address.isMulticast()) {
continue;
}
return address.toString().split('%').first().toStdWString();
}
return std::wstring{};
};
auto ipv4 = get(QAbstractSocket::IPv4Protocol);
auto ipv6 = get(QAbstractSocket::IPv6Protocol);
if (InetPtonW(AF_INET, ipv4.c_str(), out_ipv4) != 1) {
logger.debug() << "Ipv4 Conversation error" << WSAGetLastError();
if (ipv4.empty()) {
logger.error() << "No routable IPv4 address on adapter" << adapterIndex;
return false;
}
if (ipv6.empty()) {
std::memset(out_ipv6, 0x00, sizeof(IN6_ADDR));
return true;
if (InetPtonW(AF_INET, ipv4.c_str(), out_ipv4) != 1) {
logger.error() << "Ipv4 conversion error" << WSAGetLastError();
return false;
}
if (InetPtonW(AF_INET6, ipv6.c_str(), out_ipv6) != 1) {
logger.debug() << "Ipv6 Conversation error" << WSAGetLastError();
std::memset(out_ipv6, 0x00, sizeof(IN6_ADDR));
if (!ipv6.empty() && InetPtonW(AF_INET6, ipv6.c_str(), out_ipv6) != 1) {
logger.error() << "Ipv6 conversion error" << WSAGetLastError();
std::memset(out_ipv6, 0x00, sizeof(IN6_ADDR));
}
return true;
}
@@ -49,7 +49,8 @@ class WindowsSplitTunnel final {
bool excludeApps(const QStringList& appPaths);
// Fetches and Pushed needed info to move to engaged mode
bool start(int inetAdapterIndex, int vpnAdapterIndex = 0);
bool start(int inetAdapterIndex, int vpnAdapterIndex = 0,
bool invertTunnelInternet = false);
// Deletes Rules and puts the driver into passive mode
void stop();
@@ -85,7 +86,9 @@ class WindowsSplitTunnel final {
// Generates a Configuration for Each APP
std::vector<uint8_t> generateAppConfiguration(const QStringList& appPaths);
// Generates a Configuration which IP's are VPN and which network
std::vector<std::byte> generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0);
std::vector<std::byte> generateIPConfiguration(
int inetAdapterIndex, int vpnAdapterIndex = 0,
bool invertTunnelInternet = false);
std::vector<uint8_t> generateProcessBlob();
[[nodiscard]] bool getAddress(int adapterIndex, IN_ADDR* out_ipv4,
@@ -15,11 +15,43 @@
#include "leakdetector.h"
#include "logger.h"
#include "windowsfirewall.h"
#include "core/utils/routeModes.h"
#pragma comment(lib, "iphlpapi.lib")
namespace {
Logger logger("WireguardUtilsWindows");
static constexpr ULONG kIncludeAppsInterfaceMetric = 5000;
static bool setInterfaceMetric(quint64 luidValue, ULONG metric)
{
NET_LUID luid;
luid.Value = luidValue;
MIB_IPINTERFACE_ROW iface;
InitializeIpInterfaceEntry(&iface);
iface.Family = AF_INET;
iface.InterfaceLuid = luid;
DWORD res = GetIpInterfaceEntry(&iface);
if (res != NO_ERROR) {
logger.error() << "GetIpInterfaceEntry failed, error:" << res;
return false;
}
iface.UseAutomaticMetric = FALSE;
iface.Metric = metric;
iface.SitePrefixLength = 0;
res = SetIpInterfaceEntry(&iface);
if (res != NO_ERROR) {
logger.error() << "SetIpInterfaceEntry failed, error:" << res;
return false;
}
return true;
}
}; // namespace
std::unique_ptr<WireguardUtilsWindows> WireguardUtilsWindows::create(
@@ -125,13 +157,17 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) {
}
m_luid = luid.Value;
m_routeMonitor = new WindowsRouteMonitor(luid.Value, this);
m_includeOnlyApps =
config.m_appSplitTunnelType ==
static_cast<int>(amnezia::AppsRouteMode::VpnOnlyForwardApps) &&
!config.m_vpnDisabledApps.isEmpty();
if (config.m_killSwitchEnabled) {
// Enable the windows firewall
NET_IFINDEX ifindex;
ConvertInterfaceLuidToIndex(&luid, &ifindex);
m_firewall->allowAllTraffic();
m_firewall->enableInterface(ifindex);
m_firewall->enableInterface(ifindex, !m_includeOnlyApps);
}
logger.debug() << "Registration completed";
@@ -143,6 +179,7 @@ bool WireguardUtilsWindows::deleteInterface() {
m_routeMonitor->deleteLater();
}
m_includeOnlyApps = false;
m_firewall->disableKillSwitch();
m_tunnel.stop();
return true;
@@ -255,7 +292,15 @@ void WireguardUtilsWindows::buildMibForwardRow(const IPAddress& prefix,
}
bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) {
if (m_routeMonitor && (prefix.prefixLength() == 0)) {
if (m_includeOnlyApps && prefix.prefixLength() == 0) {
if (prefix.type() == QAbstractSocket::IPv6Protocol) {
return true;
}
if (!setInterfaceMetric(m_luid, kIncludeAppsInterfaceMetric)) {
logger.error() << "Refusing to install a default route without include metric";
return false;
}
} else if (m_routeMonitor && (prefix.prefixLength() == 0)) {
// If we are setting up a default route, instruct the route monitor to
// capture traffic to all non-excluded destinations
m_routeMonitor->setDetaultRouteCapture(true);
@@ -286,7 +331,11 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) {
}
bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) {
if (m_routeMonitor && (prefix.prefixLength() == 0)) {
if (m_includeOnlyApps && prefix.prefixLength() == 0 &&
prefix.type() == QAbstractSocket::IPv6Protocol) {
return true;
}
if (m_routeMonitor && (prefix.prefixLength() == 0) && !m_includeOnlyApps) {
// Deactivate the route capture feature.
m_routeMonitor->setDetaultRouteCapture(false);
}
@@ -54,6 +54,7 @@ class WireguardUtilsWindows final : public WireguardUtils {
void buildMibForwardRow(const IPAddress& prefix, void* row);
quint64 m_luid = 0;
bool m_includeOnlyApps = false;
WindowsTunnelService m_tunnel;
QPointer<WindowsRouteMonitor> m_routeMonitor;
QPointer<WindowsFirewall> m_firewall;
@@ -111,7 +111,7 @@ PageType {
headerText: qsTr("Mode")
enabled: (Qt.platform.os === "android") && root.pageEnabled
enabled: root.pageEnabled
listView: ListViewWithRadioButtonType {
rootWidth: root.width
@@ -151,10 +151,12 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
textString: qsTr("Only \"Apps from the list should not have access via VPN\" mode is available on Windows")
textString: qsTr("Site-based split tunneling is ignored when only listed apps use the VPN")
iconPath: "qrc:/images/controls/alert-circle.svg"
visible: (Qt.platform.os === "windows") && root.pageEnabled
visible: (Qt.platform.os === "windows")
&& root.pageEnabled
&& AppSplitTunnelingController.routeMode === routeMode.onlyForwardApps
}
}
+28 -4
View File
@@ -136,7 +136,20 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
QString dns1 = m_vpnConfiguration.value(configKey::dns1).toString();
QString dns2 = m_vpnConfiguration.value(configKey::dns2).toString();
#ifdef Q_OS_MACOS
#ifdef Q_OS_WIN
const RouteMode effectiveRouteMode =
static_cast<RouteMode>(m_vpnConfiguration.value(configKey::splitTunnelType).toInt());
const bool isXray = container == DockerContainer::Xray
|| container == DockerContainer::SSXray;
const bool includeOnlyApps =
m_vpnConfiguration.value(configKey::appSplitTunnelType).toInt() ==
static_cast<int>(amnezia::AppsRouteMode::VpnOnlyForwardApps);
if (!isXray || effectiveRouteMode != amnezia::RouteMode::VpnAllSites
|| includeOnlyApps) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
}
#elif defined(Q_OS_MACOS)
if (!m_appSettingsRepository->isSitesSplitTunnelingEnabled() || m_appSettingsRepository->routeMode() != amnezia::RouteMode::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
}
@@ -494,9 +507,6 @@ void VpnConnection::appendSplitTunnelingConfig()
}
}
m_vpnConfiguration.insert(configKey::splitTunnelType, routeMode);
m_vpnConfiguration.insert(configKey::splitTunnelSites, sitesJsonArray);
amnezia::AppsRouteMode appsRouteMode = amnezia::AppsRouteMode::VpnAllApps;
QJsonArray appsJsonArray;
if (m_appSettingsRepository->isAppsSplitTunnelingEnabled()) {
@@ -512,6 +522,20 @@ void VpnConnection::appendSplitTunnelingConfig()
}
}
#ifdef Q_OS_WIN
if (appsRouteMode == amnezia::AppsRouteMode::VpnOnlyForwardApps &&
m_appSettingsRepository->isStrictKillSwitchEnabled()) {
appsRouteMode = amnezia::AppsRouteMode::VpnAllApps;
appsJsonArray = QJsonArray();
}
if (appsRouteMode == amnezia::AppsRouteMode::VpnOnlyForwardApps) {
routeMode = amnezia::RouteMode::VpnAllSites;
sitesJsonArray = QJsonArray();
}
#endif
m_vpnConfiguration.insert(configKey::splitTunnelType, routeMode);
m_vpnConfiguration.insert(configKey::splitTunnelSites, sitesJsonArray);
m_vpnConfiguration.insert(configKey::appSplitTunnelType, appsRouteMode);
m_vpnConfiguration.insert(configKey::splitTunnelApps, appsJsonArray);
+2
View File
@@ -12,6 +12,8 @@ class IpcInterface
SLOT( int routeAddList(const QString &gw, const QStringList &ips) );
SLOT( bool clearSavedRoutes() );
SLOT( bool routeDeleteList(const QString &gw, const QStringList &ip) );
SLOT( bool routeAddDefault(const QString &dev) );
SLOT( bool routeDeleteDefault(const QString &dev) );
SLOT( bool flushDns() );
SLOT( void resetIpStack() );
+18
View File
@@ -91,6 +91,24 @@ bool IpcServer::routeDeleteList(const QString &gw, const QStringList &ips)
return Router::routeDeleteList(gw, ips);
}
bool IpcServer::routeAddDefault(const QString &dev)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::routeAddDefault";
#endif
return Router::routeAddDefault(dev);
}
bool IpcServer::routeDeleteDefault(const QString &dev)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::routeDeleteDefault";
#endif
return Router::routeDeleteDefault(dev);
}
bool IpcServer::flushDns()
{
#ifdef MZ_DEBUG
+2
View File
@@ -22,6 +22,8 @@ public:
virtual int routeAddList(const QString &gw, const QStringList &ips) override;
virtual bool clearSavedRoutes() override;
virtual bool routeDeleteList(const QString &gw, const QStringList &ips) override;
virtual bool routeAddDefault(const QString &dev) override;
virtual bool routeDeleteDefault(const QString &dev) override;
virtual bool flushDns() override;
virtual void resetIpStack() override;
virtual bool checkAndInstallDriver() override;
+31 -3
View File
@@ -34,6 +34,7 @@ static bool isValidIpOrCidr(const QString &value) {
#ifdef Q_OS_WIN
#include "../client/platforms/windows/daemon/windowsfirewall.h"
#include "../client/platforms/windows/daemon/windowsdaemon.h"
#include "router_win.h"
#endif
#ifdef Q_OS_LINUX
@@ -97,7 +98,30 @@ bool KillSwitch::isStrictKillSwitchEnabled()
return m_appSettigns->value("Conf/strictKillSwitchEnabled", false).toBool();
}
#ifdef Q_OS_WIN
static bool isIncludeOnlyApps(const QJsonObject &configStr)
{
return configStr.value(amnezia::configKey::appSplitTunnelType).toInt() == 1
&& !configStr.value(amnezia::configKey::splitTunnelApps).toArray().isEmpty();
}
static void applyDefaultRouteInterfaceMetric(const QJsonObject &configStr)
{
static constexpr ULONG kExcludeAppsMetric = 1;
static constexpr ULONG kIncludeAppsMetric = 5000;
RouterWin::Instance().setDefaultRouteInterfaceMetric(
isIncludeOnlyApps(configStr) ? kIncludeAppsMetric : kExcludeAppsMetric);
}
#endif
bool KillSwitch::disableKillSwitch() {
#ifdef Q_OS_WIN
RouterWin::Instance().setDefaultRouteInterfaceMetric(1);
if (Daemon *daemon = WindowsDaemon::instance()) {
daemon->activateSplitTunnel(InterfaceConfig());
}
#endif
#ifdef Q_OS_LINUX
if (isStrictKillSwitchEnabled()) {
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true);
@@ -272,6 +296,8 @@ bool KillSwitch::enablePeerTraffic(const QJsonObject &configStr) {
}
}
config.m_appSplitTunnelType =
configStr.value(amnezia::configKey::appSplitTunnelType).toInt();
for (const QJsonValue &i : configStr.value(amnezia::configKey::splitTunnelApps).toArray()) {
if (!i.isString()) {
break;
@@ -286,23 +312,25 @@ bool KillSwitch::enablePeerTraffic(const QJsonObject &configStr) {
config.m_allowedDnsServers.append(dns.toString());
}
applyDefaultRouteInterfaceMetric(configStr);
// killSwitch toggle
if (QVariant(configStr.value(amnezia::configKey::killSwitchOption).toString()).toBool()) {
WindowsFirewall::create(this)->enablePeerTraffic(config);
}
WindowsDaemon::instance()->prepareActivation(config, inetAdapterIndex);
WindowsDaemon::instance()->activateSplitTunnel(config, vpnAdapterIndex);
return WindowsDaemon::instance()->activateSplitTunnel(config, vpnAdapterIndex);
#endif
return true;
}
bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) {
#ifdef Q_OS_WIN
if (configStr.value("splitTunnelType").toInt() != 0) {
if (configStr.value("splitTunnelType").toInt() != 0 || isIncludeOnlyApps(configStr)) {
WindowsFirewall::create(this)->allowAllTraffic();
}
return WindowsFirewall::create(this)->enableInterface(vpnAdapterIndex);
return WindowsFirewall::create(this)->enableInterface(vpnAdapterIndex, !isIncludeOnlyApps(configStr));
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
+20
View File
@@ -42,6 +42,26 @@ int Router::routeDeleteList(const QString &gw, const QStringList &ips)
#endif
}
bool Router::routeAddDefault(const QString &dev)
{
#ifdef Q_OS_WIN
return RouterWin::Instance().routeAddDefault(dev);
#else
Q_UNUSED(dev)
return false;
#endif
}
bool Router::routeDeleteDefault(const QString &dev)
{
#ifdef Q_OS_WIN
return RouterWin::Instance().routeDeleteDefault(dev);
#else
Q_UNUSED(dev)
return false;
#endif
}
bool Router::flushDns()
{
#ifdef Q_OS_WIN
+2
View File
@@ -19,6 +19,8 @@ public:
static int routeAddList(const QString &gw, const QStringList &ips);
static bool clearSavedRoutes();
static int routeDeleteList(const QString &gw, const QStringList &ips);
static bool routeAddDefault(const QString &dev);
static bool routeDeleteDefault(const QString &dev);
static bool flushDns();
static void resetIpStack();
static bool createTun(const QString &dev, const QString &subnet);
+115 -1
View File
@@ -12,7 +12,7 @@
LONG (NTAPI * NtSuspendProcess)(HANDLE ProcessHandle) = NULL;
LONG (NTAPI * NtResumeProcess)(HANDLE ProcessHandle) = NULL;
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define STATUS_SUCCESS ((LONG)0x00000000L)
QList<QString> RouterWin::kIpv6Subnets = { "fc00::/7", "2000::/4", "3000::/4" };
@@ -149,8 +149,122 @@ int RouterWin::routeAddList(const QString &gw, const QStringList &ips)
return success_count;
}
bool RouterWin::buildDefaultRow(const QString &dev, MIB_IPFORWARD_ROW2 *entry)
{
NET_LUID luid;
DWORD res = ConvertInterfaceAliasToLuid(reinterpret_cast<const wchar_t *>(dev.utf16()), &luid);
if (res != NO_ERROR) {
qWarning() << "RouterWin: failed to resolve luid for" << dev << ", error:" << res;
return false;
}
InitializeIpForwardEntry(entry);
entry->InterfaceLuid = luid;
entry->DestinationPrefix.Prefix.si_family = AF_INET;
entry->DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET;
entry->DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = INADDR_ANY;
entry->DestinationPrefix.PrefixLength = 0;
entry->NextHop.si_family = AF_INET;
entry->NextHop.Ipv4.sin_family = AF_INET;
entry->NextHop.Ipv4.sin_addr.s_addr = INADDR_ANY;
entry->ValidLifetime = 0xffffffff;
entry->PreferredLifetime = 0xffffffff;
entry->Metric = 0;
entry->Protocol = MIB_IPPROTO_NETMGMT;
entry->Loopback = false;
entry->AutoconfigureAddress = false;
entry->Publish = false;
entry->Immortal = false;
entry->Age = 0;
return true;
}
static constexpr ULONG kTunnelInterfaceMetric = 1;
void RouterWin::setDefaultRouteInterfaceMetric(ULONG metric)
{
m_defaultRouteInterfaceMetric = metric == 0 ? kTunnelInterfaceMetric : metric;
}
static bool setInterfaceMetric(const NET_LUID &luid, ULONG metric)
{
MIB_IPINTERFACE_ROW iface;
InitializeIpInterfaceEntry(&iface);
iface.Family = AF_INET;
iface.InterfaceLuid = luid;
DWORD res = GetIpInterfaceEntry(&iface);
if (res != NO_ERROR) {
qWarning() << "RouterWin: GetIpInterfaceEntry failed, error:" << res;
return false;
}
iface.UseAutomaticMetric = FALSE;
iface.Metric = metric;
iface.SitePrefixLength = 0;
res = SetIpInterfaceEntry(&iface);
if (res != NO_ERROR) {
qWarning() << "RouterWin: SetIpInterfaceEntry failed, error:" << res;
return false;
}
return true;
}
bool RouterWin::routeAddDefault(const QString &dev)
{
MIB_IPFORWARD_ROW2 entry;
if (!buildDefaultRow(dev, &entry)) {
return false;
}
if (!setInterfaceMetric(entry.InterfaceLuid, m_defaultRouteInterfaceMetric)) {
qCritical() << "RouterWin::routeAddDefault: refusing to install a default route on" << dev
<< "without a deterministic interface metric";
return false;
}
DWORD res = CreateIpForwardEntry2(&entry);
if (res != NO_ERROR && res != ERROR_OBJECT_ALREADY_EXISTS) {
qCritical() << "RouterWin::routeAddDefault: failed for" << dev << ", error:" << res;
return false;
}
m_defaultRoutes.insert(dev, entry);
return true;
}
bool RouterWin::routeDeleteDefault(const QString &dev)
{
MIB_IPFORWARD_ROW2 entry;
if (m_defaultRoutes.contains(dev)) {
entry = m_defaultRoutes.take(dev);
} else if (!buildDefaultRow(dev, &entry)) {
return true;
}
DWORD res = DeleteIpForwardEntry2(&entry);
if (res != NO_ERROR && res != ERROR_NOT_FOUND) {
qCritical() << "RouterWin::routeDeleteDefault: failed for" << dev << ", error:" << res;
return false;
}
return true;
}
bool RouterWin::clearSavedRoutes()
{
const QStringList defaultRouteDevices = m_defaultRoutes.keys();
for (const QString &dev : defaultRouteDevices) {
routeDeleteDefault(dev);
}
if (m_ipForwardRows.isEmpty()) return true;
qDebug() << "RouterWin::clearSavedRoutes forward rows size:" << m_ipForwardRows.size();
+6 -1
View File
@@ -15,7 +15,6 @@
#include <WS2tcpip.h>
#include <iphlpapi.h>
#include <IcmpAPI.h>
#include <stdio.h>
#include <stdlib.h>
@@ -39,6 +38,9 @@ public:
int routeAddList(const QString &gw, const QStringList &ips);
bool clearSavedRoutes();
int routeDeleteList(const QString &gw, const QStringList &ips);
bool routeAddDefault(const QString &dev);
bool routeDeleteDefault(const QString &dev);
void setDefaultRouteInterfaceMetric(ULONG metric);
bool flushDns();
void resetIpStack();
@@ -63,10 +65,13 @@ private:
BOOL SuspendProcess(BOOL fSuspend, DWORD dwProcessId);
QNetworkInterface findLoopbackIface();
static bool buildDefaultRow(const QString &dev, MIB_IPFORWARD_ROW2 *entry);
private:
RouterWin() {m_dnsUtil = new DnsUtilsWindows(this);}
QMultiMap<QString, MIB_IPFORWARDROW> m_ipForwardRows;
QMap<QString, MIB_IPFORWARD_ROW2> m_defaultRoutes;
ULONG m_defaultRouteInterfaceMetric = 1;
bool m_suspended = false;
DnsUtilsWindows *m_dnsUtil;
};