Compare commits

...

5 Commits

Author SHA1 Message Date
vkamn
06ea7b9316 Merge branch 'dev' of github-amnezia:amnezia-vpn/amnezia-client into feat/add-system-dns-support 2025-11-13 19:44:27 +08:00
aiamnezia
498607cd16 Add collecting dns from interface settings on Linux 2025-11-03 01:52:46 +04:00
aiamnezia
cd03e11dde fix: Fix bag with endian conversion in DnsResolver 2025-11-03 01:17:42 +04:00
aiamnezia
1f9f71f244 chore: fix Android build 2025-11-02 21:38:41 +04:00
aiamnezia
d230043ead Add system dns toggle 2025-11-02 18:38:49 +04:00
9 changed files with 422 additions and 13 deletions

View File

@@ -13,6 +13,7 @@
#include <QNetworkInterface>
#include "qendian.h"
#include <QSettings>
#pragma comment(lib, "iphlpapi.lib")
#endif
#ifdef Q_OS_LINUX
#include <arpa/inet.h>
@@ -23,6 +24,28 @@
#include <sys/socket.h>
#include <unistd.h>
#endif
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusReply>
#include <QDBusArgument>
#include <QDBusObjectPath>
#include <QVariant>
#include <QVariantMap>
#include <QFile>
#include <QTextStream>
#include <QNetworkInterface>
#include <algorithm>
#include <climits>
#include <limits>
#include "platforms/linux/daemon/dbustypeslinux.h"
constexpr const char* DBUS_RESOLVE_SERVICE = "org.freedesktop.resolve1";
constexpr const char* DBUS_RESOLVE_PATH = "/org/freedesktop/resolve1";
constexpr const char* DBUS_RESOLVE_MANAGER = "org.freedesktop.resolve1.Manager";
constexpr const char* DBUS_PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties";
#endif
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#include <sys/param.h>
#include <sys/sysctl.h>
@@ -30,10 +53,17 @@
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/route.h>
#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SystemConfiguration.h>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#endif
#include <QHostAddress>
#include <QHostInfo>
#include <QPair>
#include "logger.h"
QRegularExpression NetworkUtilities::ipAddressRegExp()
{
@@ -277,7 +307,7 @@ QString NetworkUtilities::getGatewayAndIface()
free(pAdapterAddresses);
return result;
#endif
#ifdef Q_OS_LINUX
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
constexpr int BUFFER_SIZE = 100;
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
int sock = -1, msgseq = 0;
@@ -475,3 +505,310 @@ QString NetworkUtilities::getGatewayAndIface()
return gateway;
#endif
}
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
namespace {
struct LinkInfo {
int ifindex;
bool defaultRoute;
quint32 routeMetric;
QStringList dnsServers;
bool operator<(const LinkInfo& other) const {
// Sort by defaultRoute first (true comes first), then by routeMetric (lower is better)
if (defaultRoute != other.defaultRoute) {
return defaultRoute > other.defaultRoute;
}
return routeMetric < other.routeMetric;
}
};
QStringList extractDnsFromDbusArgument(const QDBusArgument& arg) {
QStringList dnsServers;
QDBusArgument dnsArg = arg;
QList<DnsResolver> resolverList = qdbus_cast<QList<DnsResolver>>(dnsArg);
for (const auto& resolver : resolverList) {
if (resolver.protocol() == QAbstractSocket::IPv4Protocol) {
const QString dnsStr = resolver.toString();
if (NetworkUtilities::checkIPv4Format(dnsStr) && !dnsServers.contains(dnsStr)) {
dnsServers.append(dnsStr);
}
}
}
return dnsServers;
}
QList<LinkInfo> getDnsFromInterfaces(const QDBusConnection& bus) {
QList<LinkInfo> links;
// Get all network interfaces
QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
for (const QNetworkInterface& iface : interfaces) {
int ifindex = iface.index();
if (ifindex <= 0) {
continue;
}
// Call GetLink to get the link object path
QDBusMessage getLinkMsg = QDBusMessage::createMethodCall(
DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, DBUS_RESOLVE_MANAGER, "GetLink");
getLinkMsg << ifindex;
QDBusReply<QDBusObjectPath> linkReply = bus.call(getLinkMsg);
if (!linkReply.isValid()) {
continue;
}
QString linkPath = linkReply.value().path();
// Get properties from the link object
QDBusMessage getPropMsg = QDBusMessage::createMethodCall(
DBUS_RESOLVE_SERVICE, linkPath, DBUS_PROPERTY_INTERFACE, "GetAll");
getPropMsg << QString("org.freedesktop.resolve1.Link");
QDBusReply<QVariantMap> propReply = bus.call(getPropMsg);
if (!propReply.isValid()) {
continue;
}
QVariantMap properties = propReply.value();
// Check if DefaultRoute is true
bool defaultRoute = properties.value("DefaultRoute").toBool();
// Get RouteMetric (default to maximum if not available)
quint32 routeMetric = std::numeric_limits<quint32>::max();
if (properties.contains("RouteMetric")) {
routeMetric = properties.value("RouteMetric").toUInt();
}
// Get DNS servers - use Get method directly for DNS property
QDBusMessage getDnsMsg = QDBusMessage::createMethodCall(
DBUS_RESOLVE_SERVICE, linkPath, DBUS_PROPERTY_INTERFACE, "Get");
getDnsMsg << QString("org.freedesktop.resolve1.Link");
getDnsMsg << QString("DNS");
QDBusReply<QVariant> dnsReply = bus.call(getDnsMsg);
if (!dnsReply.isValid()) {
continue;
}
QVariant dnsVariant = dnsReply.value();
QStringList dnsServers;
if (dnsVariant.canConvert<QDBusArgument>()) {
QDBusArgument dnsArg = qvariant_cast<QDBusArgument>(dnsVariant);
dnsServers = extractDnsFromDbusArgument(dnsArg);
}
if (!dnsServers.isEmpty()) {
LinkInfo info;
info.ifindex = ifindex;
info.defaultRoute = defaultRoute;
info.routeMetric = routeMetric;
info.dnsServers = dnsServers;
links.append(info);
}
}
return links;
}
}
#endif
QPair<QString, QString> NetworkUtilities::getSystemDnsAddress()
{
QPair<QString, QString> result;
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
// Try systemd-resolved via D-Bus first
QDBusConnection bus = QDBusConnection::systemBus();
if (bus.isConnected()) {
// Try to get DNS from Resolve DNS property using org.freedesktop.DBus.Properties
QDBusMessage message = QDBusMessage::createMethodCall(
DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, DBUS_PROPERTY_INTERFACE, "Get");
message << QString(DBUS_RESOLVE_MANAGER);
message << QString("DNS");
QDBusReply<QVariant> dnsReply = bus.call(message);
if (dnsReply.isValid()) {
QDBusArgument dnsArg = qvariant_cast<QDBusArgument>(dnsReply.value());
QList<DnsResolver> resolverList = qdbus_cast<QList<DnsResolver>>(dnsArg);
QStringList dnsServers;
for (const auto& resolver : resolverList) {
if (resolver.protocol() == QAbstractSocket::IPv4Protocol) {
QString dnsStr = resolver.toString();
if (checkIPv4Format(dnsStr) && !dnsServers.contains(dnsStr)) {
dnsServers.append(dnsStr);
}
}
}
if (!dnsServers.isEmpty()) {
result.first = dnsServers.first();
if (dnsServers.size() > 1) {
result.second = dnsServers.at(1);
}
return result;
}
}
// If no global DNS, try to get DNS from interfaces
QList<LinkInfo> links = getDnsFromInterfaces(bus);
if (!links.isEmpty()) {
// Sort by priority: DefaultRoute first, then by RouteMetric
std::sort(links.begin(), links.end());
// Get DNS from the highest priority interface
const LinkInfo& bestLink = links.first();
result.first = bestLink.dnsServers.first();
if (bestLink.dnsServers.size() > 1) {
result.second = bestLink.dnsServers.at(1);
}
qDebug() << "Got DNS from interface" << bestLink.ifindex
<< "DefaultRoute:" << bestLink.defaultRoute
<< "RouteMetric:" << bestLink.routeMetric;
return result;
}
}
// Fallback to /etc/resolv.conf
QFile resolvConf("/etc/resolv.conf");
if (resolvConf.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&resolvConf);
QStringList dnsServers;
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.startsWith("nameserver")) {
QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() >= 2) {
QString dns = parts.at(1);
if (checkIPv4Format(dns)) {
dnsServers.append(dns);
}
}
}
}
if (!dnsServers.isEmpty()) {
result.first = dnsServers.first();
if (dnsServers.size() > 1) {
result.second = dnsServers.at(1);
}
return result;
}
}
qWarning() << "Failed to get system DNS on Linux";
return result; // Return empty pair
#elif defined(Q_OS_WIN)
// Use GetAdaptersAddresses to get DNS servers
ULONG bufferSize = 0;
DWORD ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr, nullptr, &bufferSize);
if (ret == ERROR_BUFFER_OVERFLOW) {
PIP_ADAPTER_ADDRESSES adapterAddresses = (PIP_ADAPTER_ADDRESSES)malloc(bufferSize);
if (adapterAddresses) {
ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr, adapterAddresses, &bufferSize);
if (ret == NO_ERROR) {
PIP_ADAPTER_ADDRESSES currentAdapter = adapterAddresses;
QStringList dnsServers;
while (currentAdapter) {
// Check if adapter is active and has IP addresses
if (currentAdapter->OperStatus == IfOperStatusUp &&
currentAdapter->FirstUnicastAddress != nullptr) {
PIP_ADAPTER_DNS_SERVER_ADDRESS dnsServer = currentAdapter->FirstDnsServerAddress;
while (dnsServer) {
if (dnsServer->Address.lpSockaddr->sa_family == AF_INET) {
struct sockaddr_in* sa_in = (struct sockaddr_in*)dnsServer->Address.lpSockaddr;
char ipstr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sa_in->sin_addr, ipstr, INET_ADDRSTRLEN);
QString dns = QString::fromLatin1(ipstr);
if (checkIPv4Format(dns) && !dnsServers.contains(dns)) {
dnsServers.append(dns);
}
}
dnsServer = dnsServer->Next;
}
}
currentAdapter = currentAdapter->Next;
}
if (!dnsServers.isEmpty()) {
result.first = dnsServers.first();
if (dnsServers.size() > 1) {
result.second = dnsServers.at(1);
}
qDebug() << "Got system DNS from Windows:" << result.first << result.second;
free(adapterAddresses);
return result;
}
}
free(adapterAddresses);
}
}
qWarning() << "Failed to get system DNS on Windows";
return result; // Return empty pair
#elif defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
// Use SCDynamicStore to get DNS from system configuration
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("amneziavpn"), nullptr, nullptr);
if (store) {
CFDictionaryRef dnsDict = (CFDictionaryRef)SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/DNS"));
if (dnsDict) {
CFArrayRef dnsServersArray = (CFArrayRef)CFDictionaryGetValue(dnsDict, CFSTR("ServerAddresses"));
if (dnsServersArray && CFArrayGetCount(dnsServersArray) > 0) {
QStringList dnsServers;
for (CFIndex i = 0; i < CFArrayGetCount(dnsServersArray); i++) {
CFStringRef dnsString = (CFStringRef)CFArrayGetValueAtIndex(dnsServersArray, i);
if (dnsString) {
char buffer[256];
if (CFStringGetCString(dnsString, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
QString dns = QString::fromLatin1(buffer);
if (checkIPv4Format(dns)) {
dnsServers.append(dns);
}
}
}
}
if (!dnsServers.isEmpty()) {
result.first = dnsServers.first();
if (dnsServers.size() > 1) {
result.second = dnsServers.at(1);
}
qDebug() << "Got system DNS from macOS:" << result.first << result.second;
CFRelease(dnsDict);
CFRelease(store);
return result;
}
}
CFRelease(dnsDict);
}
CFRelease(store);
}
qWarning() << "Failed to get system DNS on macOS";
return result; // Return empty pair
#else
qWarning() << "System DNS reading not implemented for this platform";
return result; // Return empty pair
#endif
}

View File

@@ -6,6 +6,7 @@
#include <QString>
#include <QHostAddress>
#include <QNetworkReply>
#include <QPair>
class NetworkUtilities : public QObject
@@ -31,6 +32,9 @@ public:
static QString netMaskFromIpWithSubnet(const QString ip);
static QString ipAddressFromIpWithSubnet(const QString ip);
static QStringList summarizeRoutes(const QStringList &ips, const QString cidr);
// Returns pair of (primary DNS, secondary DNS) or empty strings on error
static QPair<QString, QString> getSystemDnsAddress();
};
#endif // NETWORKUTILITIES_H

View File

@@ -11,6 +11,7 @@
#include <QDBusArgument>
#include <QHostAddress>
#include <QtDBus/QtDBus>
#include <QtEndian>
/* D-Bus metatype for marshalling arguments to the SetLinkDNS method */
class DnsResolver : public QHostAddress {
@@ -25,12 +26,8 @@ class DnsResolver : public QHostAddress {
args << AF_INET6;
args << QByteArray::fromRawData((const char*)&addrv6, sizeof(addrv6));
} else {
quint32 addrv4 = ip.toIPv4Address();
QByteArray data(4, 0);
data[0] = (addrv4 >> 24) & 0xff;
data[1] = (addrv4 >> 16) & 0xff;
data[2] = (addrv4 >> 8) & 0xff;
data[3] = (addrv4 >> 0) & 0xff;
quint32 addrv4 = qToBigEndian(ip.toIPv4Address());
QByteArray data = QByteArray::fromRawData((const char*)&addrv4, sizeof(addrv4));
args << AF_INET;
args << data;
}
@@ -46,12 +43,8 @@ class DnsResolver : public QHostAddress {
args.endStructure();
if (family == AF_INET6) {
ip.setAddress(data.constData());
} else if (data.count() >= 4) {
quint32 addrv4 = 0;
addrv4 |= (data[0] << 24);
addrv4 |= (data[1] << 16);
addrv4 |= (data[2] << 8);
addrv4 |= (data[3] << 0);
} else if (data.size() >= 4) {
const quint32 addrv4 = qFromBigEndian<quint32>(data.constData());
ip.setAddress(addrv4);
}
return args;

View File

@@ -145,6 +145,15 @@ public:
setValue("Conf/useAmneziaDns", enabled);
}
bool useSystemDnsAddress() const
{
return value("Conf/useSystemDnsAddress", false).toBool();
}
void setUseSystemDnsAddress(bool enabled)
{
setValue("Conf/useSystemDnsAddress", enabled);
}
QString primaryDns() const;
QString secondaryDns() const;

View File

@@ -57,6 +57,12 @@ void ConnectionController::openConnection()
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
auto dns = m_serversModel->getDnsPair(serverIndex);
// Check if DNS retrieval failed (empty pair means system DNS retrieval failed)
if (dns.first.isEmpty() && dns.second.isEmpty()) {
emit connectionErrorOccurred(ErrorCode::InternalError);
return;
}
auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container);
emit connectToVpn(serverIndex, credentials, container, vpnConfiguration);

View File

@@ -79,6 +79,17 @@ bool SettingsController::isAmneziaDnsEnabled()
return m_settings->useAmneziaDns();
}
bool SettingsController::isUseSystemDnsAddressEnabled()
{
return m_settings->useSystemDnsAddress();
}
void SettingsController::setUseSystemDnsAddress(bool enable)
{
m_settings->setUseSystemDnsAddress(enable);
emit useSystemDnsAddressChanged(enable);
}
QString SettingsController::getPrimaryDns()
{
return m_settings->primaryDns();

View File

@@ -26,6 +26,7 @@ public:
Q_PROPERTY(bool isNotificationPermissionGranted READ isNotificationPermissionGranted NOTIFY onNotificationStateChanged)
Q_PROPERTY(bool isKillSwitchEnabled READ isKillSwitchEnabled WRITE toggleKillSwitch NOTIFY killSwitchEnabledChanged)
Q_PROPERTY(bool strictKillSwitchEnabled READ isStrictKillSwitchEnabled WRITE toggleStrictKillSwitch NOTIFY strictKillSwitchEnabledChanged)
Q_PROPERTY(bool useSystemDnsAddressEnabled READ isUseSystemDnsAddressEnabled WRITE setUseSystemDnsAddress NOTIFY useSystemDnsAddressChanged)
Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled)
Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged)
@@ -41,6 +42,9 @@ public slots:
void toggleAmneziaDns(bool enable);
bool isAmneziaDnsEnabled();
bool isUseSystemDnsAddressEnabled();
void setUseSystemDnsAddress(bool enable);
QString getPrimaryDns();
void setPrimaryDns(const QString &dns);
@@ -126,6 +130,8 @@ signals:
void amneziaDnsToggled(bool enable);
void useSystemDnsAddressChanged(bool enabled);
void loggingDisableByWatcher();
void onNotificationStateChanged();

View File

@@ -621,6 +621,26 @@ QPair<QString, QString> ServersModel::getDnsPair(int serverIndex)
}
}
// Check if system DNS should be used
if (m_settings->useSystemDnsAddress() && apiUtils::isPremiumServer(server)) {
auto systemDns = NetworkUtilities::getSystemDnsAddress();
bool hasPrimary = !systemDns.first.isEmpty() && NetworkUtilities::checkIPv4Format(systemDns.first);
bool hasSecondary = !systemDns.second.isEmpty() && NetworkUtilities::checkIPv4Format(systemDns.second);
if (hasPrimary || hasSecondary) {
if (hasPrimary) {
dns.first = systemDns.first;
}
if (hasSecondary) {
dns.second = systemDns.second;
}
qDebug() << "VpnConfigurator::getDnsForConfig using system DNS:" << dns.first << dns.second;
} else {
qWarning() << "Failed to get system DNS for premium config, connection will fail";
}
return dns;
}
dns.first = server.value(config_key::dns1).toString();
dns.second = server.value(config_key::dns2).toString();

View File

@@ -75,6 +75,29 @@ PageType {
DividerType {}
SwitcherType {
id: useSystemDnsSwitch
visible: ServersModel.processedServerIsPremium
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("Use system DNS")
descriptionText: qsTr("Use system DNS servers")
checked: SettingsController.useSystemDnsAddressEnabled
onToggled: function() {
if (checked !== SettingsController.useSystemDnsAddressEnabled) {
SettingsController.setUseSystemDnsAddress(checked)
}
}
}
DividerType {
visible: ServersModel.processedServerIsPremium
}
LabelWithButtonType {
id: dnsServersButton