From c0cae0ff0111026d714870ffef033544a5d9fb0e Mon Sep 17 00:00:00 2001 From: cd-amn Date: Mon, 4 May 2026 15:39:07 +0400 Subject: [PATCH] fix: outbound freedom for xray (#2479) * fix: outbound freedom for xray on linux * fix: outbound freedom for xray on macOS * build: auto-generate pf rules based on the build type --- .gitignore | 1 + CMakeLists.txt | 21 +++++ .../platforms/linux/daemon/linuxfirewall.cpp | 6 ++ client/vpnConnection.cpp | 9 ++ deploy/data/macos/AmneziaVPN.plist | 2 + deploy/data/macos/pf/amn.400.allowPIA.conf | 2 - deploy/data/macos/post_install.sh | 13 +++ deploy/data/macos/post_uninstall.sh | 17 ++++ .../pf-templates/amn.400.allowPIA.conf.in | 2 + service/server/killswitch.cpp | 9 ++ service/server/router_mac.cpp | 90 +++++++++++++++++++ service/server/router_mac.h | 3 +- service/server/xray.cpp | 37 +++++++- service/server/xray.h | 5 ++ service/server/xray_defs.h | 11 +++ 15 files changed, 221 insertions(+), 7 deletions(-) delete mode 100644 deploy/data/macos/pf/amn.400.allowPIA.conf create mode 100644 deploy/data/pf-templates/amn.400.allowPIA.conf.in create mode 100644 service/server/xray_defs.h diff --git a/.gitignore b/.gitignore index a48886bfc..90430b7ee 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ client/.DS_Store ._.DS_Store ._* *.dmg +deploy/data/macos/pf/amn.400.allowPIA.conf # tmp files *.*~ diff --git a/CMakeLists.txt b/CMakeLists.txt index 96aed3ea9..83adcc76a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,27 @@ if(APPLE) endif() endif() +if(APPLE AND NOT IOS) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(AMN_PF_RULE_IDENTITY "user { root }") + else() + set(AMN_PF_RULE_IDENTITY "group { amnvpn }") + endif() + + configure_file( + "${CMAKE_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in" + "${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf" + @ONLY + ) + + file(COPY_FILE + "${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf" + "${CMAKE_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf" + ONLY_IF_DIFFERENT + ) +endif() + + add_subdirectory(client) if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE) diff --git a/client/platforms/linux/daemon/linuxfirewall.cpp b/client/platforms/linux/daemon/linuxfirewall.cpp index de88c9625..c04770f1a 100644 --- a/client/platforms/linux/daemon/linuxfirewall.cpp +++ b/client/platforms/linux/daemon/linuxfirewall.cpp @@ -32,6 +32,7 @@ #include "linuxfirewall.h" #include "logger.h" +#include "xray_defs.h" #include #define BRAND_CODE "amn" @@ -282,6 +283,10 @@ void LinuxFirewall::install() QStringLiteral("-o tun2+ -j ACCEPT"), }); + installAnchor(Both, QStringLiteral("130.allowMarkedXray"), { + QStringLiteral("-m mark --mark %1 -j ACCEPT").arg(amnezia::xray::xrayTrafficMark), + }); + installAnchor(IPv4, QStringLiteral("120.blockNets"), {}); installAnchor(IPv4, QStringLiteral("110.allowNets"), {}); @@ -358,6 +363,7 @@ void LinuxFirewall::uninstall() uninstallAnchor(IPv6, QStringLiteral("250.blockIPv6")); uninstallAnchor(Both, QStringLiteral("200.allowVPN")); uninstallAnchor(IPv4, QStringLiteral("120.blockNets")); + uninstallAnchor(Both, QStringLiteral("130.allowMarkedXray")); uninstallAnchor(IPv4, QStringLiteral("110.allowNets")); uninstallAnchor(Both, QStringLiteral("100.blockAll")); diff --git a/client/vpnConnection.cpp b/client/vpnConnection.cpp index 9da0b254e..ad8ce917d 100644 --- a/client/vpnConnection.cpp +++ b/client/vpnConnection.cpp @@ -92,7 +92,13 @@ 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 + if (!m_appSettingsRepository->isSitesSplitTunnelingEnabled() || m_appSettingsRepository->routeMode() != amnezia::RouteMode::VpnAllExceptSites) { + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); + } +#else iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); +#endif if (m_appSettingsRepository->isSitesSplitTunnelingEnabled()) { iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); @@ -105,6 +111,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(), 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/pf/amn.400.allowPIA.conf b/deploy/data/macos/pf/amn.400.allowPIA.conf deleted file mode 100644 index 7c8a36808..000000000 --- a/deploy/data/macos/pf/amn.400.allowPIA.conf +++ /dev/null @@ -1,2 +0,0 @@ -# Allow traffic by privileged group (used by daemon) -pass out proto { tcp, udp } group { amnvpn } flags any no state 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/deploy/data/pf-templates/amn.400.allowPIA.conf.in b/deploy/data/pf-templates/amn.400.allowPIA.conf.in new file mode 100644 index 000000000..20f7ec9f0 --- /dev/null +++ b/deploy/data/pf-templates/amn.400.allowPIA.conf.in @@ -0,0 +1,2 @@ +# Allow traffic by configured identity (set by CMake) +pass out proto { tcp, udp } @AMN_PF_RULE_IDENTITY@ flags any no state diff --git a/service/server/killswitch.cpp b/service/server/killswitch.cpp index 78b7654f3..a0f68008a 100644 --- a/service/server/killswitch.cpp +++ b/service/server/killswitch.cpp @@ -84,6 +84,7 @@ bool KillSwitch::disableKillSwitch() { LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), false); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("130.allowMarkedXray"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), false); @@ -96,6 +97,7 @@ bool KillSwitch::disableKillSwitch() { LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), false); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("130.allowMarkedXray"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); @@ -118,6 +120,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(); } @@ -143,6 +146,7 @@ bool KillSwitch::disableAllTraffic() { LinuxFirewall::install(); } LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("130.allowMarkedXray"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); #endif @@ -279,15 +283,18 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn bool blockAll = 0; bool allowNets = 0; bool blockNets = 0; + bool allowMarkedXray = 0; QStringList allownets; QStringList blocknets; if (splitTunnelType == 0) { blockAll = true; allowNets = true; + allowMarkedXray = true; allownets.append(configStr.value("vpnServer").toString()); } else if (splitTunnelType == 1) { blockNets = true; + allowMarkedXray = true; for (auto v : splitTunnelSites) { blocknets.append(v.toString()); } @@ -313,6 +320,7 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn LinuxFirewall::updateAllowNets(allownets); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), blockAll); LinuxFirewall::updateBlockNets(blocknets); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("130.allowMarkedXray"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); @@ -378,6 +386,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 e0eb9fd0a..f41e9a724 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 5d324014e..076a4484e 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -1,5 +1,8 @@ #include "xray.h" #include "core/utils/networkUtilities.h" +#ifdef Q_OS_MAC +#include "router_mac.h" +#endif #include #include @@ -25,18 +28,34 @@ #endif #ifdef Q_OS_LINUX #include + #include "xray_defs.h" #endif 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; @@ -65,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) @@ -99,6 +127,7 @@ void Xray::sockCallback(uintptr_t fd) #ifdef Q_OS_LINUX if (!m_defaultIfaceName.isEmpty()) { setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, m_defaultIfaceName.data(), m_defaultIfaceName.size()); + setsockopt(fd, SOL_SOCKET, SO_MARK, &amnezia::xray::xrayTrafficMark, sizeof(amnezia::xray::xrayTrafficMark)); } #endif } 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 diff --git a/service/server/xray_defs.h b/service/server/xray_defs.h new file mode 100644 index 000000000..76864d93e --- /dev/null +++ b/service/server/xray_defs.h @@ -0,0 +1,11 @@ +#ifndef XRAY_DEFS_H +#define XRAY_DEFS_H + +namespace amnezia +{ + namespace xray + { + constexpr unsigned int xrayTrafficMark = 0x82; + } +} +#endif // XRAY_DEFS_H \ No newline at end of file