Compare commits

...

7 Commits

Author SHA1 Message Date
NickVs2015
213377a38c fix: IPC security review fixes 2026-08-06 23:30:39 +03:00
NickVs2015
f03d419e4d fix: remove UID check from IPC server 2026-08-06 13:29:10 +03:00
NickVs2015
711699d830 fix: review changes next 2026-08-06 12:23:30 +03:00
NickVs2015
a3dda0a27b fix: review changes 2026-08-05 18:37:05 +03:00
NickVs2015
041cdf39b4 fix: suppress linux firewall errors and add missing 400.allowPIA anchor 2026-07-21 19:06:33 +03:00
NickVs2015
35acbadd43 fix: extend IPC security validation to macOS firewall 2026-07-20 22:34:20 +03:00
NickVs2015
5d774e6c38 fix: resolve critical IPC security vulnerabilities
- Validate IP/CIDR values from IPC before passing to Linux firewall
- Replace shell interpolation with direct execve in firewall update functions
- Block dangerous OpenVPN/WireGuard arguments in sanitizeArguments()
- Add programId bounds check in IpcServerProcess::setProgram()
- Add SO_PEERCRED peer authentication for IPC connections on Linux
2026-07-20 22:34:20 +03:00
6 changed files with 171 additions and 77 deletions

View File

@@ -33,6 +33,7 @@
#include "linuxfirewall.h"
#include "logger.h"
#include "xray_defs.h"
#include <QFileInfo>
#include <QProcess>
#define BRAND_CODE "amn"
@@ -109,7 +110,7 @@ int LinuxFirewall::linkChain(LinuxFirewall::IPVersion ip, const QString& chain,
// (we can't safely delete all rules at once since rule numbers change)
// TODO: occasionally this script results in warnings in logs "Bad rule (does a matching rule exist in the chain?)" - this happens when
// the e.g OUTPUT chain is empty but this script attempts to delete things from it anyway. It doesn't cause any problems, but we should still fix at some point..
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs -r %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
}
else
return execute(QStringLiteral("if ! %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -A %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName));
@@ -188,40 +189,6 @@ void LinuxFirewall::uninstallAnchor(LinuxFirewall::IPVersion ip, const QString&
deleteChain(ip, actualChain, tableName);
}
QStringList LinuxFirewall::getDNSRules(const QStringList& servers)
{
QStringList result;
for (const QString& server : servers)
{
result << QStringLiteral("-o amn0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o amn0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun2+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun2+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
}
return result;
}
QStringList LinuxFirewall::getAllowRule(const QStringList& servers)
{
QStringList result;
for (const QString& server : servers)
{
result << QStringLiteral("-d %1 -j ACCEPT").arg(server);
}
return result;
}
QStringList LinuxFirewall::getBlockRule(const QStringList& servers)
{
QStringList result;
for (const QString& server : servers)
{
result << QStringLiteral("-d %1 -j REJECT").arg(server);
}
return result;
}
void LinuxFirewall::install()
@@ -291,6 +258,8 @@ void LinuxFirewall::install()
installAnchor(IPv4, QStringLiteral("110.allowNets"), {});
installAnchor(Both, QStringLiteral("400.allowPIA"), {});
installAnchor(Both, QStringLiteral("100.blockAll"), {
QStringLiteral("-j REJECT"),
});
@@ -454,16 +423,33 @@ void LinuxFirewall::updateDNSServers(const QStringList& servers)
static QStringList existingServers {};
existingServers = servers;
execute(QStringLiteral("iptables -F %1.320.allowDNS").arg(kAnchorName));
for (const QString& rule : getDNSRules(servers))
execute(QStringLiteral("iptables -A %1.320.allowDNS %2").arg(kAnchorName, rule));
const QString chain = QStringLiteral("%1.320.allowDNS").arg(kAnchorName);
executeIptables(QStringLiteral("iptables"), {QStringLiteral("-F"), chain});
const QStringList ifaces = {
QStringLiteral("amn0+"), QStringLiteral("tun0+"), QStringLiteral("tun2+")
};
for (const QString& server : servers) {
for (const QString& iface : ifaces) {
executeIptables(QStringLiteral("iptables"),
{QStringLiteral("-A"), chain, QStringLiteral("-o"), iface,
QStringLiteral("-d"), server, QStringLiteral("-p"), QStringLiteral("udp"),
QStringLiteral("--dport"), QStringLiteral("53"), QStringLiteral("-j"), QStringLiteral("ACCEPT")});
executeIptables(QStringLiteral("iptables"),
{QStringLiteral("-A"), chain, QStringLiteral("-o"), iface,
QStringLiteral("-d"), server, QStringLiteral("-p"), QStringLiteral("tcp"),
QStringLiteral("--dport"), QStringLiteral("53"), QStringLiteral("-j"), QStringLiteral("ACCEPT")});
}
}
}
void LinuxFirewall::updateAllowNets(const QStringList& servers)
{
execute(QStringLiteral("iptables -F %1.110.allowNets").arg(kAnchorName));
for (const QString& rule : getAllowRule(servers))
execute(QStringLiteral("iptables -A %1.110.allowNets %2").arg(kAnchorName, rule));
const QString chain = QStringLiteral("%1.110.allowNets").arg(kAnchorName);
executeIptables(QStringLiteral("iptables"), {QStringLiteral("-F"), chain});
for (const QString& server : servers)
executeIptables(QStringLiteral("iptables"),
{QStringLiteral("-A"), chain, QStringLiteral("-d"), server,
QStringLiteral("-j"), QStringLiteral("ACCEPT")});
}
void LinuxFirewall::updateBlockNets(const QStringList& servers)
@@ -471,9 +457,12 @@ void LinuxFirewall::updateBlockNets(const QStringList& servers)
static QStringList existingServers {};
existingServers = servers;
execute(QStringLiteral("iptables -F %1.120.blockNets").arg(kAnchorName));
for (const QString& rule : getBlockRule(servers))
execute(QStringLiteral("iptables -A %1.120.blockNets %2").arg(kAnchorName, rule));
const QString chain = QStringLiteral("%1.120.blockNets").arg(kAnchorName);
executeIptables(QStringLiteral("iptables"), {QStringLiteral("-F"), chain});
for (const QString& server : servers)
executeIptables(QStringLiteral("iptables"),
{QStringLiteral("-A"), chain, QStringLiteral("-d"), server,
QStringLiteral("-j"), QStringLiteral("REJECT")});
}
int waitForExitCode(QProcess& process)
@@ -506,10 +495,39 @@ int LinuxFirewall::execute(const QString &command, bool ignoreErrors)
return exitCode;
}
int LinuxFirewall::executeIptables(const QString &program, const QStringList &args, bool ignoreErrors)
{
QProcess p;
p.start(program, args, QProcess::ReadOnly);
p.closeWriteChannel();
int exitCode = waitForExitCode(p);
auto out = p.readAllStandardOutput().trimmed();
auto err = p.readAllStandardError().trimmed();
if ((exitCode != 0 || !err.isEmpty()) && !ignoreErrors)
logger.warning() << "(" << exitCode << ") $ " << program << args.join(QLatin1Char(' '));
if (!out.isEmpty())
logger.info() << out;
if (!err.isEmpty())
logger.warning() << err;
return exitCode;
}
void LinuxFirewall::setupTrafficSplitting()
{
const QString cgroupBase = QStringLiteral("/sys/fs/cgroup/net_cls");
if (!QFileInfo::exists(cgroupBase)) {
logger.warning() << "net_cls cgroup v1 not available, traffic splitting disabled";
return;
}
execute(QStringLiteral(
"if ! grep -qE '^[0-9]+[[:space:]]+%1$' /etc/iproute2/rt_tables 2>/dev/null ; then "
"echo '200 %1' >> /etc/iproute2/rt_tables ; fi"
).arg(kRtableName));
auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/";
logger.info() << "Should be setting up cgroup in" << cGroupDir << "for traffic splitting";
logger.info() << "Setting up cgroup in" << cGroupDir << "for traffic splitting";
execute(QStringLiteral("if [ ! -d %1 ] ; then mkdir %1 ; sleep 0.1 ; echo %2 > %1/net_cls.classid ; fi").arg(cGroupDir).arg(kCGroupId));
// Set a rule with priority 100 (lower priority than local but higher than main/default, 0 is highest priority)
execute(QStringLiteral("if ! ip rule list | grep -q %1 ; then ip rule add from all fwmark %1 lookup %2 pri 100 ; fi").arg(kPacketTag, kRtableName));
@@ -518,7 +536,7 @@ void LinuxFirewall::setupTrafficSplitting()
void LinuxFirewall::teardownTrafficSplitting()
{
logger.info() << "Tearing down cgroup and routing rules";
execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2> /dev/null ; fi").arg(kPacketTag, kRtableName));
execute(QStringLiteral("ip route flush table %1").arg(kRtableName));
execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2>/dev/null ; fi").arg(kPacketTag, kRtableName));
execute(QStringLiteral("ip route flush table %1 2>/dev/null || true").arg(kRtableName));
execute(QStringLiteral("ip route flush cache"));
}

View File

@@ -79,12 +79,10 @@ private:
static int unlinkChain(IPVersion ip, const QString& chain, const QString& parent, const QString& tableName = kFilterTable);
static void installAnchor(IPVersion ip, const QString& anchor, const QStringList& rules, const QString& tableName = kFilterTable, const FilterCallbackFunc& enableFunc = {}, const FilterCallbackFunc& disableFunc = {});
static void uninstallAnchor(IPVersion ip, const QString& anchor, const QString& tableName = kFilterTable);
static QStringList getDNSRules(const QStringList& servers);
static QStringList getAllowRule(const QStringList& servers);
static QStringList getBlockRule(const QStringList& servers);
static void setupTrafficSplitting();
static void teardownTrafficSplitting();
static int execute(const QString& command, bool ignoreErrors = false);
static int executeIptables(const QString& program, const QStringList& args, bool ignoreErrors = false);
private:
// Chain names
static QString kOutputChain, kRootChain, kPostRoutingChain, kPreRoutingChain;

View File

@@ -3,6 +3,8 @@
#include <QObject>
#include <QString>
#include <QRegularExpression>
#include <QSet>
#include "../client/core/utils/utilities.h"
@@ -15,7 +17,8 @@ enum PermittedProcess {
OpenVPN,
Wireguard,
Tun2Socks,
CertUtil
CertUtil,
PermittedProcessCount
};
inline QString permittedProcessPath(PermittedProcess pid)
@@ -57,16 +60,27 @@ inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &a
QList<Validator> positionalArgs;
switch (proc) {
case OpenVPN: {
namedArgs["--config"] = [](const QString& v) { return !v.isEmpty(); };
namedArgs["--management"] = [](const QString& v) { return !v.isEmpty(); };
namedArgs["--management-client"] = nullptr;
positionalArgs.append([](const QString& v) {
bool ok;
int port = v.toInt(&ok);
return ok && port > 0 && port <= 65535;
});
break;
}
case Tun2Socks:
namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); };
namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); };
break;
default:
//FIXME
case CertUtil:
return args;
default:
return {};
}
QStringList sanitized;
for (int i = 0, pos = 0; i < args.size(); i++) {

View File

@@ -48,8 +48,9 @@ int IpcServer::createPrivilegedProcess()
// Make sure any connections are handed to QtRO
QObject::connect(pd.localServer.data(), &QLocalServer::newConnection, this, [pd]() {
qDebug() << "IpcServer new connection";
QLocalSocket *conn = pd.localServer->nextPendingConnection();
if (pd.serverNode) {
pd.serverNode->addHostSideConnection(pd.localServer->nextPendingConnection());
pd.serverNode->addHostSideConnection(conn);
pd.serverNode->enableRemoting(pd.ipcProcess.data());
}
});

View File

@@ -77,6 +77,8 @@ void IpcServerProcess::setProcessChannelMode(QProcess::ProcessChannelMode mode)
void IpcServerProcess::setProgram(int programId)
{
Q_ASSERT(programId > static_cast<int>(amnezia::PermittedProcess::Invalid) &&
programId < static_cast<int>(amnezia::PermittedProcess::PermittedProcessCount));
m_program = static_cast<amnezia::PermittedProcess>(programId);
m_process->setProgram(amnezia::permittedProcessPath(m_program));
m_process->setArguments({});

View File

@@ -3,6 +3,8 @@
#include <QApplication>
#include <QHostAddress>
#include <QRegularExpression>
#include <algorithm>
#include "../client/core/utils/protocolEnum.h"
#include "../client/core/protocols/protocolUtils.h"
@@ -11,6 +13,27 @@
#include "qjsonarray.h"
#include "version.h"
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
static bool isValidIpOrCidr(const QString &value) {
static const QRegularExpression re(
QStringLiteral(R"(^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$)"));
if (!re.match(value).hasMatch()) return false;
const QStringList ipParts = value.split(QLatin1Char('/'))[0].split(QLatin1Char('.'));
for (const QString &part : ipParts) {
bool ok;
int octet = part.toInt(&ok);
if (!ok || octet < 0 || octet > 255) return false;
}
if (value.contains(QLatin1Char('/'))) {
bool ok;
int prefix = value.split(QLatin1Char('/'))[1].toInt(&ok);
if (!ok || prefix < 0 || prefix > 32) return false;
}
return true;
}
#endif
#ifdef Q_OS_WIN
#include "../client/platforms/windows/daemon/windowsfirewall.h"
#include "../client/platforms/windows/daemon/windowsdaemon.h"
@@ -166,7 +189,15 @@ bool KillSwitch::disableAllTraffic() {
bool KillSwitch::resetAllowedRange(const QStringList &ranges) {
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
if (!std::all_of(ranges.cbegin(), ranges.cend(), isValidIpOrCidr)) {
qCritical() << "IPC: invalid IP/CIDR in ranges, rejecting resetAllowedRange";
return false;
}
m_allowedRanges = ranges;
#else
m_allowedRanges = ranges;
#endif
#ifdef Q_OS_LINUX
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), true);
@@ -189,6 +220,12 @@ bool KillSwitch::resetAllowedRange(const QStringList &ranges) {
}
bool KillSwitch::addAllowedRange(const QStringList &ranges) {
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
if (!std::all_of(ranges.cbegin(), ranges.cend(), isValidIpOrCidr)) {
qCritical() << "IPC: invalid IP/CIDR in ranges, rejecting addAllowedRange";
return false;
}
#endif
for (const QString &range : ranges) {
if (!range.isEmpty() && !m_allowedRanges.contains(range)) {
m_allowedRanges.append(range);
@@ -286,6 +323,15 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
bool allowMarkedXray = 0;
QStringList allownets;
QStringList blocknets;
QStringList allowedDnsServers;
for (const QJsonValue &dns : configStr.value(amnezia::configKey::allowedDnsServers).toArray()) {
if (!dns.isString()) break;
const QString dnsStr = dns.toString();
if (isValidIpOrCidr(dnsStr))
allowedDnsServers.append(dnsStr);
else if (!dnsStr.isEmpty())
qWarning() << "IPC: rejected invalid allowedDnsServer:" << dnsStr;
}
if (splitTunnelType == 0) {
blockAll = true;
@@ -317,6 +363,11 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), blockAll);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), allowNets);
if (!std::all_of(allownets.cbegin(), allownets.cend(), isValidIpOrCidr) ||
!std::all_of(blocknets.cbegin(), blocknets.cend(), isValidIpOrCidr)) {
qCritical() << "IPC: invalid IP/CIDR in allownets/blocknets, rejecting enableKillSwitch";
return false;
}
LinuxFirewall::updateAllowNets(allownets);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), blockAll);
LinuxFirewall::updateBlockNets(blocknets);
@@ -328,23 +379,25 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true);
QStringList dnsServers;
dnsServers.append(configStr.value(amnezia::configKey::dns1).toString());
const QString dns1 = configStr.value(amnezia::configKey::dns1).toString();
if (isValidIpOrCidr(dns1))
dnsServers.append(dns1);
else if (!dns1.isEmpty())
qWarning() << "IPC: rejected invalid dns1:" << dns1;
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (!configStr.value(amnezia::configKey::dns1).toString().contains(amnezia::protocols::dns::amneziaDnsIp)) {
dnsServers.append(configStr.value(amnezia::configKey::dns2).toString());
if (!dns1.contains(amnezia::protocols::dns::amneziaDnsIp)) {
const QString dns2 = configStr.value(amnezia::configKey::dns2).toString();
if (isValidIpOrCidr(dns2))
dnsServers.append(dns2);
else if (!dns2.isEmpty())
qWarning() << "IPC: rejected invalid dns2:" << dns2;
}
dnsServers.append("127.0.0.1");
dnsServers.append("127.0.0.53");
for (auto dns : configStr.value(amnezia::configKey::allowedDnsServers).toArray()) {
if (!dns.isString()) {
break;
}
dnsServers.append(dns.toString());
}
dnsServers.append(allowedDnsServers);
LinuxFirewall::updateDNSServers(dnsServers);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true);
@@ -357,6 +410,11 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
MacOSFirewall::install();
MacOSFirewall::ensureRootAnchorPriority();
if (!std::all_of(allownets.cbegin(), allownets.cend(), isValidIpOrCidr) ||
!std::all_of(blocknets.cbegin(), blocknets.cend(), isValidIpOrCidr)) {
qCritical() << "IPC: invalid IP/CIDR in allownets/blocknets, rejecting enableKillSwitch";
return false;
}
MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true);
MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), blockAll);
MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), allowNets);
@@ -370,20 +428,23 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true);
QStringList dnsServers;
dnsServers.append(configStr.value(amnezia::configKey::dns1).toString());
const QString dns1 = configStr.value(amnezia::configKey::dns1).toString();
if (isValidIpOrCidr(dns1))
dnsServers.append(dns1);
else if (!dns1.isEmpty())
qWarning() << "IPC: rejected invalid dns1:" << dns1;
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (!configStr.value(amnezia::configKey::dns1).toString().contains(amnezia::protocols::dns::amneziaDnsIp)) {
dnsServers.append(configStr.value(amnezia::configKey::dns2).toString());
if (!dns1.contains(amnezia::protocols::dns::amneziaDnsIp)) {
const QString dns2 = configStr.value(amnezia::configKey::dns2).toString();
if (isValidIpOrCidr(dns2))
dnsServers.append(dns2);
else if (!dns2.isEmpty())
qWarning() << "IPC: rejected invalid dns2:" << dns2;
}
for (auto dns : configStr.value(amnezia::configKey::allowedDnsServers).toArray()) {
if (!dns.isString()) {
break;
}
dnsServers.append(dns.toString());
}
dnsServers.append(allowedDnsServers);
MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true);
MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), dnsServers);
MacOSFirewall::setAnchorEnabled(QStringLiteral("400.allowPIA"), true);