Compare commits

...

4 Commits

Author SHA1 Message Date
cd-amn
5b82fd6019 fix: DNS unresponsive during connect 2026-04-27 13:13:58 +04:00
cd-amn
76d9bf468d build: auto-generate pf rules based on the build type 2026-04-21 21:03:43 +04:00
cd-amn
6da1c678e9 fix: outbound freedom for xray on macOS 2026-04-17 15:25:40 +04:00
cd-amn
b8629032d6 fix: outbound freedom for xray on linux 2026-04-14 18:08:33 +04:00
18 changed files with 238 additions and 8 deletions

1
.gitignore vendored
View File

@@ -81,6 +81,7 @@ client/.DS_Store
._.DS_Store
._*
*.dmg
deploy/data/macos/pf/amn.400.allowPIA.conf
# tmp files
*.*~

View File

@@ -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)

View File

@@ -15,6 +15,7 @@
#include <QTextStream>
#include <QtGlobal>
#include "linuxfirewall.h"
#include "leakdetector.h"
#include "logger.h"
@@ -50,3 +51,17 @@ LinuxDaemon* LinuxDaemon::instance() {
Q_ASSERT(s_daemon);
return s_daemon;
}
bool LinuxDaemon::run(Op op, const InterfaceConfig& config) {
if (!config.m_killSwitchEnabled || !LinuxFirewall::isInstalled()) {
return true;
}
if (op == Up) {
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true);
} else if (op == Down) {
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), false);
}
return true;
}

View File

@@ -21,6 +21,7 @@ class LinuxDaemon final : public Daemon {
static LinuxDaemon* instance();
protected:
bool run(Op op, const InterfaceConfig& config) override;
WireguardUtils* wgutils() const override { return m_wgutils; }
DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; }

View File

@@ -32,6 +32,7 @@
#include "linuxfirewall.h"
#include "logger.h"
#include "xray_defs.h"
#include <QProcess>
#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"));

View File

@@ -479,7 +479,7 @@ void WireguardUtilsLinux::applyFirewallRules(FirewallParams& params)
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), false);
LinuxFirewall::updateDNSServers(params.dnsServers);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true);

View File

@@ -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());
}
}

View File

@@ -12,6 +12,8 @@
<true/>
<key>RunAtLoad</key>
<true/>
<key>GroupName</key>
<string>amnvpn</string>
<key>Sockets</key>
<dict>
<key>Listeners</key>

View File

@@ -1,2 +0,0 @@
# Allow traffic by privileged group (used by daemon)
pass out proto { tcp, udp } group { amnvpn } flags any no state

View File

@@ -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/"

View File

@@ -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
# -----------------------------------------------------------

View File

@@ -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

View File

@@ -81,6 +81,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);
@@ -93,6 +94,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);
@@ -115,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();
}
@@ -140,6 +143,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
@@ -276,15 +280,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());
}
@@ -310,6 +317,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);
@@ -375,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;
}

View File

@@ -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";

View File

@@ -34,6 +34,8 @@ public:
bool deleteTun(const QString &dev);
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& 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

View File

@@ -1,5 +1,8 @@
#include "xray.h"
#include "core/networkUtilities.h"
#ifdef Q_OS_MAC
#include "router_mac.h"
#endif
#include <QDebug>
#include <QNetworkInterface>
@@ -25,18 +28,34 @@
#endif
#ifdef Q_OS_LINUX
#include <sys/socket.h>
#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
}

View File

@@ -31,6 +31,11 @@ private:
#else
int m_defaultIfaceIdx;
#endif
#ifdef Q_OS_MAC
QString m_uplinkIfaceName;
QString m_uplinkGateway;
#endif
};
#endif // XRAY_H

View File

@@ -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