diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp
index 3997ed02c..6d59e6d01 100644
--- a/client/vpnconnection.cpp
+++ b/client/vpnconnection.cpp
@@ -88,8 +88,14 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
+#ifdef Q_OS_MACOS
+ if (!m_settings->isSitesSplitTunnelingEnabled() || m_settings->routeMode() != Settings::VpnAllExceptSites) {
+#endif
// TODO: add error code handling for all routeAddList (or rework the code below)
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
+#ifdef Q_OS_MACOS
+ }
+#endif
if (m_settings->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
@@ -102,6 +108,9 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
+#ifdef Q_OS_MACOS
+ iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << dns1 << dns2);
+#endif
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
}
}
diff --git a/deploy/data/macos/AmneziaVPN.plist b/deploy/data/macos/AmneziaVPN.plist
index 0627024c9..394d263cb 100644
--- a/deploy/data/macos/AmneziaVPN.plist
+++ b/deploy/data/macos/AmneziaVPN.plist
@@ -12,6 +12,8 @@
RunAtLoad
+ GroupName
+ amnvpn
Sockets
Listeners
diff --git a/deploy/data/macos/post_install.sh b/deploy/data/macos/post_install.sh
index 6a01bdbd3..2d5fe0857 100755
--- a/deploy/data/macos/post_install.sh
+++ b/deploy/data/macos/post_install.sh
@@ -1,6 +1,7 @@
#!/bin/bash
APP_NAME=AmneziaVPN
+SERVICE_GROUP=amnvpn
PLIST_NAME=$APP_NAME.plist
LAUNCH_DAEMONS_PLIST_NAME=/Library/LaunchDaemons/$PLIST_NAME
LOG_FOLDER=/var/log/$APP_NAME
@@ -34,6 +35,18 @@ fi
run_cmd launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
+# Add separate group for xray filtering
+if dscl . -read "/Groups/$SERVICE_GROUP" >/dev/null 2>&1; then
+ log "Group $SERVICE_GROUP already exists"
+ return 0
+else
+ local next_gid
+ next_gid=$(dscl . -list /Groups PrimaryGroupID 2>/dev/null | awk '{print $2}' | sort -n | awk '$1>=500{g=$1} END{print (g?g+1:501)}')
+ run_cmd dscl . -create "/Groups/$SERVICE_GROUP"
+ run_cmd dscl . -create "/Groups/$SERVICE_GROUP" PrimaryGroupID "$next_gid"
+ run_cmd dscl . -create "/Groups/$SERVICE_GROUP" RealName "Amnezia VPN Service Group"
+fi
+
run_cmd sudo chmod -R a-w "$APP_PATH/"
run_cmd sudo chown -R root "$APP_PATH/"
run_cmd sudo chgrp -R wheel "$APP_PATH/"
diff --git a/deploy/data/macos/post_uninstall.sh b/deploy/data/macos/post_uninstall.sh
index c9336b8af..b5c8c0fe7 100755
--- a/deploy/data/macos/post_uninstall.sh
+++ b/deploy/data/macos/post_uninstall.sh
@@ -8,6 +8,7 @@ USER_APP_SUPPORT="$HOME/Library/Application Support/$APP_NAME"
SYSTEM_APP_SUPPORT="/Library/Application Support/$APP_NAME"
LOG_FOLDER="/var/log/$APP_NAME"
CACHES_FOLDER="$HOME/Library/Caches/$APP_NAME"
+SERVICE_GROUP="amnvpn"
# Attempt to quit the GUI application if it's currently running
if pgrep -x "$APP_NAME" > /dev/null; then
@@ -81,4 +82,20 @@ if sudo pfctl -s info 2>/dev/null | grep -q '^Status: Enabled' && \
sudo pfctl -d 2>/dev/null || true
fi
+# Remove amnvpn group if it's not referenced by users
+if dscl . -read "/Groups/$SERVICE_GROUP" >/dev/null 2>&1; then
+ group_gid=$(dscl . -read "/Groups/$SERVICE_GROUP" PrimaryGroupID 2>/dev/null | awk '{print $2}')
+ users_with_primary_gid=""
+ if [ -n "$group_gid" ]; then
+ users_with_primary_gid=$(dscl . -list /Users PrimaryGroupID 2>/dev/null | awk -v gid="$group_gid" '$2 == gid {print $1}')
+ fi
+
+ if [ -z "$users_with_primary_gid" ]; then
+ echo "Removing group $SERVICE_GROUP"
+ sudo dscl . -delete "/Groups/$SERVICE_GROUP" || true
+ else
+ echo "Keeping group $SERVICE_GROUP (still used by users): $users_with_primary_gid"
+ fi
+fi
+
# -----------------------------------------------------------
diff --git a/service/server/killswitch.cpp b/service/server/killswitch.cpp
index a2799d59f..be8fe54e9 100644
--- a/service/server/killswitch.cpp
+++ b/service/server/killswitch.cpp
@@ -117,6 +117,7 @@ bool KillSwitch::disableKillSwitch() {
MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), false);
MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), false);
MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), false);
+ MacOSFirewall::setAnchorEnabled(QStringLiteral("400.allowPIA"), false);
} else {
MacOSFirewall::uninstall();
}
@@ -382,6 +383,7 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true);
MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), dnsServers);
+ MacOSFirewall::setAnchorEnabled(QStringLiteral("400.allowPIA"), true);
#endif
return true;
}
diff --git a/service/server/router_mac.cpp b/service/server/router_mac.cpp
index f56e3f9f6..ea15c9a65 100644
--- a/service/server/router_mac.cpp
+++ b/service/server/router_mac.cpp
@@ -162,6 +162,96 @@ bool RouterMac::restoreResolvers() {
return m_dnsUtil->restoreResolvers();
}
+bool RouterMac::routeAddXray(const QString& ifname, const QString& gateway)
+{
+ if (ifname.isEmpty() || gateway.isEmpty()) {
+ qWarning().noquote() << "routeAddXray: invalid iface/gateway:" << ifname << gateway;
+ return false;
+ }
+
+ QString cmd = QString("route add -net 0.0.0.0/1 %1 -ifscope %2").arg(gateway).arg(ifname);
+ QStringList parts = cmd.split(" ");
+
+ int argc = parts.size();
+ char **argv = new char*[argc];
+ for (int i = 0; i < argc; i++) {
+ argv[i] = new char[parts.at(i).toStdString().length() + 1];
+ strcpy(argv[i], parts.at(i).toStdString().c_str());
+ }
+ mainRouteIface(argc, argv);
+ for (int i = 0; i < argc; i++) {
+ delete [] argv[i];
+ }
+ delete[] argv;
+
+ cmd = QString("route add -net 128.0.0.0/1 %1 -ifscope %2").arg(gateway).arg(ifname);
+ parts = cmd.split(" ");
+
+ argc = parts.size();
+ argv = new char*[argc];
+ for (int i = 0; i < argc; i++) {
+ argv[i] = new char[parts.at(i).toStdString().length() + 1];
+ strcpy(argv[i], parts.at(i).toStdString().c_str());
+ }
+ mainRouteIface(argc, argv);
+ for (int i = 0; i < argc; i++) {
+ delete [] argv[i];
+ }
+ delete[] argv;
+
+ qDebug().noquote() << "Installed xray routes via" << gateway << "on" << ifname;
+ return true;
+}
+
+bool RouterMac::routeDeleteXray(const QString& ifname, const QString& gateway)
+{
+ if (ifname.isEmpty()) {
+ return false;
+ }
+
+ QString cmd;
+ if (!gateway.isEmpty()) {
+ cmd = QString("route delete -net 0.0.0.0/1 %1 -ifscope %2").arg(gateway).arg(ifname);
+ } else {
+ cmd = QString("route delete -net 0.0.0.0/1 -ifscope %1").arg(ifname);
+ }
+ QStringList parts = cmd.split(" ");
+
+ int argc = parts.size();
+ char **argv = new char*[argc];
+ for (int i = 0; i < argc; i++) {
+ argv[i] = new char[parts.at(i).toStdString().length() + 1];
+ strcpy(argv[i], parts.at(i).toStdString().c_str());
+ }
+ mainRouteIface(argc, argv);
+ for (int i = 0; i < argc; i++) {
+ delete [] argv[i];
+ }
+ delete[] argv;
+
+ if (!gateway.isEmpty()) {
+ cmd = QString("route delete -net 128.0.0.0/1 %1 -ifscope %2").arg(gateway).arg(ifname);
+ } else {
+ cmd = QString("route delete -net 128.0.0.0/1 -ifscope %1").arg(ifname);
+ }
+ parts = cmd.split(" ");
+
+ argc = parts.size();
+ argv = new char*[argc];
+ for (int i = 0; i < argc; i++) {
+ argv[i] = new char[parts.at(i).toStdString().length() + 1];
+ strcpy(argv[i], parts.at(i).toStdString().c_str());
+ }
+ mainRouteIface(argc, argv);
+ for (int i = 0; i < argc; i++) {
+ delete [] argv[i];
+ }
+ delete[] argv;
+
+ qDebug().noquote() << "Removed xray routes on" << ifname;
+ return true;
+}
+
bool RouterMac::deleteTun(const QString &dev)
{
qDebug().noquote() << "deleteTun start";
diff --git a/service/server/router_mac.h b/service/server/router_mac.h
index 72beb7ea8..bcfca8fe3 100644
--- a/service/server/router_mac.h
+++ b/service/server/router_mac.h
@@ -34,6 +34,8 @@ public:
bool deleteTun(const QString &dev);
bool updateResolvers(const QString& ifname, const QList& resolvers);
bool restoreResolvers();
+ bool routeAddXray(const QString& ifname, const QString& gateway);
+ bool routeDeleteXray(const QString& ifname, const QString& gateway);
public slots:
@@ -47,4 +49,3 @@ private:
};
#endif // ROUTERMAC_H
-
diff --git a/service/server/xray.cpp b/service/server/xray.cpp
index 487a46435..831699899 100644
--- a/service/server/xray.cpp
+++ b/service/server/xray.cpp
@@ -1,5 +1,8 @@
#include "xray.h"
#include "core/networkUtilities.h"
+#ifdef Q_OS_MAC
+#include "router_mac.h"
+#endif
#include
#include
@@ -31,13 +34,28 @@
bool Xray::startXray(const QString &cfg)
{
qDebug() << "Xray::startXray()";
-
- auto defaultIface = NetworkUtilities::getGatewayAndIface().second;
+ const auto gatewayAndIface = NetworkUtilities::getGatewayAndIface();
+ const QString defaultGateway = gatewayAndIface.first;
+ const QNetworkInterface defaultIface = gatewayAndIface.second;
#ifdef Q_OS_LINUX
m_defaultIfaceName = defaultIface.name().toUtf8();
#else
m_defaultIfaceIdx = defaultIface.index();
#endif
+ if (defaultIface.index() > 0) {
+ qDebug() << "[xray] using uplink interface:" << defaultIface.name() << "(" << defaultIface.index() << ")";
+ }
+
+#ifdef Q_OS_MAC
+ m_uplinkIfaceName = defaultIface.name();
+ m_uplinkGateway = defaultGateway;
+ if (!m_uplinkIfaceName.isEmpty()) {
+ const bool installed = RouterMac::Instance().routeAddXray(m_uplinkIfaceName, m_uplinkGateway);
+ if (!installed) {
+ qWarning() << "[xray] failed to install xray routes on" << m_uplinkIfaceName;
+ }
+ }
+#endif
if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) {
qDebug() << "[xray] sockopt failed: " << err;
@@ -66,13 +84,22 @@ bool Xray::startXray(const QString &cfg)
bool Xray::stopXray()
{
qDebug() << "Xray::stopXray()";
+ bool success = true;
if (auto err = amnezia_xray_stop(); err != nullptr) {
qDebug() << "[xray] failed to stop: " << err;
amnezia_xray_free(err);
- return false;
+ success = false;
}
- return true;
+#ifdef Q_OS_MAC
+ if (!m_uplinkIfaceName.isEmpty()) {
+ RouterMac::Instance().routeDeleteXray(m_uplinkIfaceName, m_uplinkGateway);
+ }
+ m_uplinkIfaceName.clear();
+ m_uplinkGateway.clear();
+#endif
+
+ return success;
}
void Xray::logHandler(char* str)
diff --git a/service/server/xray.h b/service/server/xray.h
index f54d9902f..45704137a 100644
--- a/service/server/xray.h
+++ b/service/server/xray.h
@@ -31,6 +31,11 @@ private:
#else
int m_defaultIfaceIdx;
#endif
+
+#ifdef Q_OS_MAC
+ QString m_uplinkIfaceName;
+ QString m_uplinkGateway;
+#endif
};
#endif // XRAY_H