Compare commits

...

14 Commits

Author SHA1 Message Date
aiamnezia
fcef264559 feat: remove DoH address from killswitch exceptions after resolving 2025-10-24 16:41:07 +04:00
aiamnezia
f4a9bdd367 chore: remove unnecessary debug 2025-10-07 16:32:20 +04:00
aiamnezia
28424a9360 chore: remove unnecessary debug 2025-10-07 16:28:55 +04:00
aiamnezia
590412050f feat: Add Quad9 DoH fallback 2025-10-07 16:00:55 +04:00
aiamnezia
a1f865ddc7 chore: fix win build 2025-10-03 17:27:12 +04:00
aiamnezia
f394bdb271 fix: send request directly to gateway ip in case of strickt killswitch 2025-10-03 17:22:59 +04:00
aiamnezia
cab23b8e2e chore: add debug 2025-10-03 16:45:05 +04:00
aiamnezia
6bd13dccaa chore: small refactor 2025-10-03 16:36:43 +04:00
aiamnezia
3abca41fe8 chore: fix build issue 2025-10-03 16:15:58 +04:00
aiamnezia
c66fa0c9ca feat: add dns resolve via DoH 2025-10-03 15:28:20 +04:00
aiamnezia
2be594a2fe add gateway request delay 2025-08-15 15:36:04 +04:00
aiamnezia
e3271f0bc9 Add debug output 2025-08-15 13:04:06 +04:00
aiamnezia
166b45f5d0 Enable strictr mode 2025-08-13 16:54:20 +04:00
aiamnezia
e51af609ab Fix strict mode RadioButton 2025-08-11 18:04:11 +04:00
8 changed files with 414 additions and 12 deletions

View File

@@ -3,11 +3,22 @@
#include <algorithm>
#include <random>
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMetaObject>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRandomGenerator>
#include <QDataStream>
#include <QSslConfiguration>
#include <QSslSocket>
#include <QRemoteObjectPendingReply>
#include <QThread>
#include <QUrl>
#include <QtEndian>
#include <QDebug>
#include "QBlockCipher.h"
#include "QRsa.h"
@@ -66,10 +77,15 @@ ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBo
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
const QUrl originalUrl = request.url();
const QString originalHost = originalUrl.host();
const QString resolvedIp = addKillSwitchExceptionForUrl(originalUrl);
if (!resolvedIp.isEmpty() && resolvedIp != originalHost) {
QUrl ipUrl = originalUrl;
ipUrl.setHost(resolvedIp);
request.setUrl(ipUrl);
request.setPeerVerifyName(originalHost);
request.setRawHeader("Host", originalHost.toUtf8());
}
}
#endif
@@ -128,10 +144,15 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
const QUrl originalUrl = request.url();
const QString originalHost = originalUrl.host();
const QString resolvedIp = addKillSwitchExceptionForUrl(originalUrl);
if (!resolvedIp.isEmpty() && resolvedIp != originalHost) {
QUrl ipUrl = originalUrl;
ipUrl.setHost(resolvedIp);
request.setUrl(ipUrl);
request.setPeerVerifyName(originalHost);
request.setRawHeader("Host", originalHost.toUtf8());
}
}
#endif
@@ -362,3 +383,344 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
}
}
}
QString GatewayController::addKillSwitchExceptionForUrl(const QUrl &url)
{
#ifdef AMNEZIA_DESKTOP
const QString host = url.host();
if (host.isEmpty()) {
return {};
}
const QString resolvedIp = resolveHost(host);
if (resolvedIp.isEmpty()) {
qWarning() << "Failed to resolve host for KillSwitch exception" << host;
return {};
}
if (!addKillSwitchException(QStringList { resolvedIp })) {
qWarning() << "Failed to add KillSwitch exception" << resolvedIp;
return {};
}
return resolvedIp;
#else
Q_UNUSED(url);
return {};
#endif
}
QString GatewayController::resolveHost(const QString &host)
{
#ifdef AMNEZIA_DESKTOP
if (!m_isStrictKillSwitchEnabled) {
return NetworkUtilities::getIPAddress(host);
}
QString resolvedIp = NetworkUtilities::getIPAddress(host);
if (!resolvedIp.isEmpty()) {
return resolvedIp;
}
qDebug() << "resolveHost: falling back to resolveHostViaOpenDns" << host;
resolvedIp = resolveHostViaOpenDns(host);
if (!resolvedIp.isEmpty()) {
return resolvedIp;
}
qWarning() << "OpenDNS fallback failed" << host;
qDebug() << "resolveHost: falling back to resolveHostViaQuad9" << host;
resolvedIp = resolveHostViaQuad9(host);
if (resolvedIp.isEmpty()) {
qWarning() << "Quad9 fallback failed" << host;
}
return resolvedIp;
#else
return NetworkUtilities::getIPAddress(host);
#endif
}
#ifdef AMNEZIA_DESKTOP
bool GatewayController::addKillSwitchException(const QStringList &ranges)
{
auto ipcInterface = IpcClient::Interface();
if (!ipcInterface) {
qWarning() << "IPC interface is null, cannot add KillSwitch exception";
return false;
}
const auto waitForReply = [](QRemoteObjectPendingReply<bool> reply) -> bool {
if (!reply.waitForFinished()) {
qWarning() << "Timed out waiting for KillSwitch exception reply";
return false;
}
return reply.returnValue();
};
QRemoteObjectPendingReply<bool> reply;
if (ipcInterface->thread() == QThread::currentThread()) {
reply = ipcInterface->addKillSwitchAllowedRange(ranges);
} else {
const bool invoked = QMetaObject::invokeMethod(ipcInterface.data(),
[&reply, ipcInterface, ranges]() {
reply = ipcInterface->addKillSwitchAllowedRange(ranges);
},
Qt::BlockingQueuedConnection);
if (!invoked) {
qWarning() << "Failed to invoke KillSwitch exception update via queued connection";
return false;
}
}
const bool result = waitForReply(reply);
return result;
}
bool GatewayController::removeKillSwitchException(const QStringList &ranges)
{
auto ipcInterface = IpcClient::Interface();
if (!ipcInterface) {
qWarning() << "IPC interface is null, cannot remove KillSwitch exception";
return false;
}
const auto waitForReply = [](QRemoteObjectPendingReply<bool> reply) -> bool {
if (!reply.waitForFinished()) {
qWarning() << "Timed out waiting for KillSwitch removal reply";
return false;
}
return reply.returnValue();
};
QRemoteObjectPendingReply<bool> reply;
if (ipcInterface->thread() == QThread::currentThread()) {
reply = ipcInterface->removeKillSwitchAllowedRange(ranges);
} else {
const bool invoked = QMetaObject::invokeMethod(ipcInterface.data(),
[&reply, ipcInterface, ranges]() {
reply = ipcInterface->removeKillSwitchAllowedRange(ranges);
},
Qt::BlockingQueuedConnection);
if (!invoked) {
qWarning() << "Failed to invoke KillSwitch removal via queued connection";
return false;
}
}
const bool result = waitForReply(reply);
return result;
}
QString GatewayController::resolveHostViaOpenDns(const QString &host)
{
const QString fallbackIp = QStringLiteral("146.112.41.2");
const QString dohHostname = QStringLiteral("doh.opendns.com");
const QUrl dohEndpoint(QStringLiteral("https://%1/dns-query").arg(fallbackIp));
if (!addKillSwitchException(QStringList { fallbackIp })) {
qWarning() << "Failed to add fallback KillSwitch exception" << fallbackIp;
}
QNetworkRequest request(dohEndpoint);
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/dns-message"));
request.setRawHeader("Accept", "application/dns-message");
request.setRawHeader("Host", dohHostname.toUtf8());
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setPeerVerifyName(dohHostname);
QByteArray payload = buildDnsQuery(host);
QNetworkReply *reply = amnApp->networkManager()->post(request, payload);
if (!reply) {
qWarning() << "Failed to create DoH request" << host;
return {};
}
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
QByteArray dnsResponse;
if (reply->error() == QNetworkReply::NoError) {
dnsResponse = reply->readAll();
} else {
qWarning() << "DoH request failed" << host << reply->errorString();
}
reply->deleteLater();
if (dnsResponse.isEmpty()) {
return {};
}
const QString resolvedIp = parseDnsResponse(dnsResponse);
return resolvedIp;
}
QString GatewayController::resolveHostViaQuad9(const QString &host)
{
const QString dohHostname = QStringLiteral("dns.quad9.net");
const QString fallbackIp = QStringLiteral("149.112.112.112");
QByteArray payload = buildDnsQuery(host);
const QUrl dohEndpoint(QStringLiteral("https://%1/dns-query").arg(fallbackIp));
if (!addKillSwitchException(QStringList { fallbackIp })) {
qWarning() << "resolveHostViaQuad9: failed to add KillSwitch exception" << fallbackIp;
}
QNetworkRequest request(dohEndpoint);
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/dns-message"));
request.setRawHeader("Accept", "application/dns-message");
request.setRawHeader("Host", dohHostname.toUtf8());
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setPeerVerifyName(dohHostname);
QNetworkReply *reply = amnApp->networkManager()->post(request, payload);
if (!reply) {
qWarning() << "resolveHostViaQuad9: failed to create DoH request" << host << fallbackIp;
return {};
}
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
QByteArray dnsResponse;
if (reply->error() == QNetworkReply::NoError) {
dnsResponse = reply->readAll();
} else {
qWarning() << "resolveHostViaQuad9: DoH request failed" << host << fallbackIp << reply->errorString();
}
reply->deleteLater();
if (dnsResponse.isEmpty()) {
return {};
}
const QString resolvedIp = parseDnsResponse(dnsResponse);
return resolvedIp;
}
QByteArray GatewayController::buildDnsQuery(const QString &host) const
{
QByteArray query;
QDataStream stream(&query, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian);
quint16 transactionId = QRandomGenerator::system()->generate();
stream << transactionId;
stream << static_cast<quint16>(0x0100); // standard query with recursion desired
stream << static_cast<quint16>(1); // QDCOUNT
stream << static_cast<quint16>(0); // ANCOUNT
stream << static_cast<quint16>(0); // NSCOUNT
stream << static_cast<quint16>(0); // ARCOUNT
const QByteArray hostUtf8 = host.toUtf8();
const QList<QByteArray> labels = hostUtf8.split('.');
for (const QByteArray &label : labels) {
stream << static_cast<quint8>(label.size());
stream.writeRawData(label.constData(), label.size());
}
stream << static_cast<quint8>(0); // end of QNAME
stream << static_cast<quint16>(1); // QTYPE A
stream << static_cast<quint16>(1); // QCLASS IN
return query;
}
QString GatewayController::parseDnsResponse(const QByteArray &response) const
{
if (response.size() < 12) {
qWarning() << "DNS response too short" << response.size();
return {};
}
QDataStream stream(response);
stream.setByteOrder(QDataStream::BigEndian);
quint16 transactionId;
quint16 flags;
quint16 qdCount;
quint16 anCount;
quint16 nsCount;
quint16 arCount;
stream >> transactionId >> flags >> qdCount >> anCount >> nsCount >> arCount;
if ((flags & 0x000F) != 0) {
qWarning() << "DNS response contains error" << flags;
return {};
}
int offset = 12;
for (int i = 0; i < qdCount; ++i) {
offset = skipDnsName(response, offset);
if (offset < 0 || offset + 4 > response.size()) {
qWarning() << "Invalid DNS question section";
return {};
}
offset += 4;
}
const uchar *data = reinterpret_cast<const uchar *>(response.constData());
for (int i = 0; i < anCount; ++i) {
int nameOffset = skipDnsName(response, offset);
if (nameOffset < 0 || nameOffset + 10 > response.size()) {
qWarning() << "Invalid DNS answer section";
return {};
}
offset = nameOffset;
quint16 type = qFromBigEndian<quint16>(data + offset);
quint16 dnsClass = qFromBigEndian<quint16>(data + offset + 2);
quint32 ttl = qFromBigEndian<quint32>(data + offset + 4);
Q_UNUSED(ttl);
quint16 rdLength = qFromBigEndian<quint16>(data + offset + 8);
offset += 10;
if (offset + rdLength > response.size()) {
qWarning() << "Invalid RDATA length" << rdLength;
return {};
}
if (type == 1 && dnsClass == 1 && rdLength == 4) {
const quint8 b1 = data[offset];
const quint8 b2 = data[offset + 1];
const quint8 b3 = data[offset + 2];
const quint8 b4 = data[offset + 3];
return QStringLiteral("%1.%2.%3.%4").arg(b1).arg(b2).arg(b3).arg(b4);
}
offset += rdLength;
}
return {};
}
int GatewayController::skipDnsName(const QByteArray &message, int offset) const
{
while (offset < message.size()) {
quint8 length = static_cast<quint8>(message.at(offset));
if (length == 0) {
return offset + 1;
}
if ((length & 0xC0) == 0xC0) {
if (offset + 2 > message.size()) {
return -1;
}
return offset + 2;
}
++offset;
offset += length;
if (offset > message.size()) {
return -1;
}
}
return -1;
}
#endif

View File

@@ -27,6 +27,17 @@ private:
const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
QString addKillSwitchExceptionForUrl(const QUrl &url);
QString resolveHost(const QString &host);
#ifdef AMNEZIA_DESKTOP
bool addKillSwitchException(const QStringList &ranges);
bool removeKillSwitchException(const QStringList &ranges);
QString resolveHostViaOpenDns(const QString &host);
QString resolveHostViaQuad9(const QString &host);
QByteArray buildDnsQuery(const QString &host) const;
QString parseDnsResponse(const QByteArray &response) const;
int skipDnsName(const QByteArray &message, int offset) const;
#endif
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;

View File

@@ -64,12 +64,15 @@ PageType {
enabled: SettingsController.isKillSwitchEnabled && !ConnectionController.isConnected
checked: !SettingsController.strictKillSwitchEnabled
checkable: false
text: qsTr("Soft KillSwitch")
descriptionText: qsTr("Internet access is blocked if the VPN disconnects unexpectedly")
onClicked: function() {
SettingsController.strictKillSwitchEnabled = false
if (SettingsController.strictKillSwitchEnabled) {
SettingsController.strictKillSwitchEnabled = false
}
}
Keys.onEnterPressed: this.clicked()
@@ -84,15 +87,18 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: false
enabled: false
// enabled: SettingsController.isKillSwitchEnabled && !ConnectionController.isConnected
enabled: SettingsController.isKillSwitchEnabled && !ConnectionController.isConnected
checked: SettingsController.strictKillSwitchEnabled
checkable: false
text: qsTr("Strict KillSwitch")
descriptionText: qsTr("Internet connection is blocked even when VPN is turned off manually or hasn't started")
onClicked: function() {
if (SettingsController.strictKillSwitchEnabled) {
return
}
var headerText = qsTr("Just a little heads-up")
var descriptionText = qsTr("If the VPN disconnects or drops while Strict KillSwitch is enabled, internet access will be blocked. To restore access, reconnect VPN or disable/change the KillSwitch.")
var yesButtonText = qsTr("Continue")

View File

@@ -32,6 +32,7 @@ class IpcInterface
SLOT( bool disableAllTraffic() );
SLOT( bool refreshKillSwitch( bool enabled ) );
SLOT( bool addKillSwitchAllowedRange( const QStringList ranges ) );
SLOT( bool removeKillSwitchAllowedRange( const QStringList ranges ) );
SLOT( bool resetKillSwitchAllowedRange( const QStringList ranges ) );
SLOT( bool enablePeerTraffic( const QJsonObject &configStr) );
SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) );

View File

@@ -189,6 +189,11 @@ bool IpcServer::addKillSwitchAllowedRange(QStringList ranges)
return KillSwitch::instance()->addAllowedRange(ranges);
}
bool IpcServer::removeKillSwitchAllowedRange(QStringList ranges)
{
return KillSwitch::instance()->removeAllowedRange(ranges);
}
bool IpcServer::disableAllTraffic()
{
return KillSwitch::instance()->disableAllTraffic();

View File

@@ -36,6 +36,7 @@ public:
virtual void StopRoutingIpv6() override;
virtual bool disableAllTraffic() override;
virtual bool addKillSwitchAllowedRange(QStringList ranges) override;
virtual bool removeKillSwitchAllowedRange(QStringList ranges) override;
virtual bool resetKillSwitchAllowedRange(QStringList ranges) override;
virtual bool enablePeerTraffic(const QJsonObject &configStr) override;
virtual bool enableKillSwitch(const QJsonObject &excludeAddr, int vpnAdapterIndex) override;

View File

@@ -189,6 +189,21 @@ bool KillSwitch::addAllowedRange(const QStringList &ranges) {
return resetAllowedRange(m_allowedRanges);
}
bool KillSwitch::removeAllowedRange(const QStringList &ranges) {
bool modified = false;
for (const QString &range : ranges) {
if (!range.isEmpty()) {
modified = modified || m_allowedRanges.removeAll(range) > 0;
}
}
if (!modified) {
return true;
}
return resetAllowedRange(m_allowedRanges);
}
bool KillSwitch::enablePeerTraffic(const QJsonObject &configStr) {
#ifdef Q_OS_WIN
InterfaceConfig config;

View File

@@ -19,6 +19,7 @@ public:
bool enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex);
bool resetAllowedRange(const QStringList &ranges);
bool addAllowedRange(const QStringList &ranges);
bool removeAllowedRange(const QStringList &ranges);
bool isStrictKillSwitchEnabled();
private: