DNS update

This commit is contained in:
svamnezia
2026-02-05 06:46:18 +03:00
parent 6123268846
commit 3a7d285e55
10 changed files with 811 additions and 283 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,9 @@
# User settings
*.user
# Gateway configs (contains sensitive endpoints)
gateway.json
client/gateway.json
macOSPackage/
AmneziaVPN.dmg
AmneziaVPN.exe

View File

@@ -227,5 +227,16 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
)
endif()
# Copy gateway.json to build directory if exists
if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/gateway.json")
add_custom_command(
TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_LIST_DIR}/gateway.json
$<TARGET_FILE_DIR:${PROJECT}>
COMMENT "Copying gateway.json to build directory"
)
endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
qt_finalize_target(${PROJECT})

View File

@@ -1,18 +1,23 @@
#include "gatewayController.h"
#include <algorithm>
#include <atomic>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMutex>
#include <QNetworkReply>
#include <QProcessEnvironment>
#include <QPromise>
#include <QUrl>
#include <QHostAddress>
#include <QDebug>
#include <QtConcurrent>
#include "QBlockCipher.h"
#include "QRsa.h"
@@ -50,6 +55,90 @@ namespace
constexpr int httpStatusCodeNotImplemented = 501;
}
// Parse TransportsConfig from JSON
TransportsConfig TransportsConfig::fromJson(const QJsonObject &json)
{
TransportsConfig config;
// Parse primary transport
QString primaryStr = json.value("primary").toString("http").toLower();
if (primaryStr == "http") {
config.primary = PrimaryTransport::Http;
} else if (primaryStr == "dns_udp" || primaryStr == "udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "dns_tcp" || primaryStr == "tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dns_dot" || primaryStr == "dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "dns_doh" || primaryStr == "doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "dns_doq" || primaryStr == "doq") {
config.primary = PrimaryTransport::DnsDoq;
}
// Parse retry settings
config.retryCount = json.value("retry_count").toInt(3);
config.timeoutMs = json.value("timeout_ms").toInt(10000);
// Parse HTTP config
if (json.contains("http")) {
QJsonObject httpObj = json["http"].toObject();
config.httpEnabled = httpObj.value("enabled").toBool(true);
config.httpEndpoint = httpObj.value("endpoint").toString();
}
// Parse DNS transports (each with its own server/domain)
if (json.contains("dns_transports")) {
QJsonArray transportsArray = json["dns_transports"].toArray();
for (const auto &transportVal : transportsArray) {
QJsonObject transportObj = transportVal.toObject();
DnsTransportEntry entry;
// Each transport has its own server and domain
entry.server = transportObj.value("server").toString();
entry.domain = transportObj.value("domain").toString();
entry.port = static_cast<quint16>(transportObj.value("port").toInt(15353));
entry.dohPath = transportObj.value("path").toString("/dns-query");
QString typeStr = transportObj.value("type").toString().toLower();
if (typeStr == "udp") {
entry.type = NetworkUtilities::DnsTransport::Udp;
if (entry.port == 15353 && !transportObj.contains("port")) {
entry.port = 15353;
}
} else if (typeStr == "tcp") {
entry.type = NetworkUtilities::DnsTransport::Tcp;
if (entry.port == 15353 && !transportObj.contains("port")) {
entry.port = 15353;
}
} else if (typeStr == "dot" || typeStr == "tls") {
entry.type = NetworkUtilities::DnsTransport::Tls;
if (!transportObj.contains("port")) {
entry.port = 8853;
}
} else if (typeStr == "doh" || typeStr == "https") {
entry.type = NetworkUtilities::DnsTransport::Https;
if (!transportObj.contains("port")) {
entry.port = 443;
}
} else if (typeStr == "doq" || typeStr == "quic") {
entry.type = NetworkUtilities::DnsTransport::Quic;
if (!transportObj.contains("port")) {
entry.port = 8853;
}
} else {
continue; // Skip unknown transport
}
if (entry.isValid()) {
config.dnsTransports.append(entry);
}
}
}
return config;
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent)
: QObject(parent),
@@ -81,6 +170,296 @@ void GatewayController::setDnsServer(const QString &dnsServer, const QString &ba
<< "Transport:" << transportName << "Port:" << port;
}
void GatewayController::setTransportsConfig(const TransportsConfig &config)
{
m_transportsConfig = config;
// Update legacy fields for backward compatibility
if (!config.httpEndpoint.isEmpty()) {
m_gatewayEndpoint = config.httpEndpoint;
}
// Update timeout from config
if (config.timeoutMs > 0) {
m_requestTimeoutMsecs = config.timeoutMs;
}
qDebug() << "[Transport] Config set: HTTP enabled=" << config.httpEnabled
<< "endpoint=" << config.httpEndpoint
<< "DNS transports=" << config.dnsTransports.size()
<< "primary=" << static_cast<int>(config.primary)
<< "retry=" << config.retryCount
<< "timeout=" << config.timeoutMs;
}
bool GatewayController::loadTransportsConfig(const QString &filePath, const QString &envVarName)
{
// Try environment variable first
QString envValue = QProcessEnvironment::systemEnvironment().value(envVarName);
if (!envValue.isEmpty()) {
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(envValue.toUtf8(), &parseError);
if (parseError.error == QJsonParseError::NoError) {
setTransportsConfig(TransportsConfig::fromJson(doc.object()));
qDebug() << "[Transport] Loaded config from env:" << envVarName;
return true;
}
qWarning() << "[Transport] Failed to parse env" << envVarName << ":" << parseError.errorString();
}
// Try file
QFile file(filePath);
if (file.open(QIODevice::ReadOnly)) {
QByteArray data = file.readAll();
file.close();
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
if (parseError.error == QJsonParseError::NoError) {
setTransportsConfig(TransportsConfig::fromJson(doc.object()));
qDebug() << "[Transport] Loaded config from file:" << filePath;
return true;
}
qWarning() << "[Transport] Failed to parse file" << filePath << ":" << parseError.errorString();
}
qDebug() << "[Transport] No config found, using defaults";
return false;
}
ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
if (!m_transportsConfig.isValid()) {
qWarning() << "[Transport] Invalid config, falling back to HTTP only";
return post(endpoint, apiPayload, responseBody);
}
// Prepare encrypted request once (skip DNS resolve for DNS tunneling)
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload, true);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
// Extract endpoint name for DNS tunneling
QString endpointName = endpoint;
endpointName.remove("%1");
if (endpointName.startsWith("v1/")) {
endpointName = endpointName.mid(3);
}
if (endpointName.endsWith("/")) {
endpointName.chop(1);
}
// Helper: find DNS transport by type
auto findDnsTransport = [&](NetworkUtilities::DnsTransport type) -> const DnsTransportEntry* {
for (const auto &t : m_transportsConfig.dnsTransports) {
if (t.type == type && t.isValid()) return &t;
}
return nullptr;
};
// Helper: try HTTP transport
auto tryHttp = [&]() -> ErrorCode {
if (!m_transportsConfig.httpEnabled) return ErrorCode::AmneziaServiceConnectionFailed;
qDebug() << "[Transport] PRIMARY: Trying HTTP";
EncryptedRequestData httpRequestData = prepareRequest(endpoint, apiPayload, false);
if (httpRequestData.errorCode != ErrorCode::NoError) return httpRequestData.errorCode;
QNetworkAccessManager nam;
QNetworkReply *reply = nam.post(httpRequestData.request, httpRequestData.requestBody);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec();
QByteArray encryptedBody = reply->readAll();
auto replyError = reply->error();
reply->deleteLater();
if (replyError != QNetworkReply::NoError || encryptedBody.isEmpty()) {
qDebug() << "[Transport] PRIMARY HTTP failed";
return ErrorCode::AmneziaServiceConnectionFailed;
}
try {
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(encryptedBody, httpRequestData.key, httpRequestData.iv, "", httpRequestData.salt);
qDebug() << "[Transport] PRIMARY HTTP succeeded";
return ErrorCode::NoError;
} catch (...) {
return ErrorCode::ApiConfigDecryptionError;
}
};
// Helper: try DNS transport
auto tryDns = [&](const DnsTransportEntry &transport) -> ErrorCode {
QString transportName;
switch (transport.type) {
case NetworkUtilities::DnsTransport::Udp: transportName = "UDP"; break;
case NetworkUtilities::DnsTransport::Tcp: transportName = "TCP"; break;
case NetworkUtilities::DnsTransport::Tls: transportName = "DoT"; break;
case NetworkUtilities::DnsTransport::Https: transportName = "DoH"; break;
case NetworkUtilities::DnsTransport::Quic: transportName = "DoQ"; break;
}
qDebug() << "[Transport] PRIMARY: Trying DNS" << transportName;
QByteArray dnsResponse = NetworkUtilities::sendViaDnsTunnel(
encRequestData.requestBody, endpointName, transport.domain,
transport.server, transport.type, transport.port, m_requestTimeoutMsecs, transport.dohPath);
if (dnsResponse.isEmpty()) {
qDebug() << "[Transport] PRIMARY DNS" << transportName << "failed";
return ErrorCode::AmneziaServiceConnectionFailed;
}
try {
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(dnsResponse, encRequestData.key, encRequestData.iv, "", encRequestData.salt);
qDebug() << "[Transport] PRIMARY DNS" << transportName << "succeeded";
return ErrorCode::NoError;
} catch (...) {
return ErrorCode::ApiConfigDecryptionError;
}
};
// === STEP 1: Try PRIMARY transport first ===
qDebug() << "[Transport] Trying primary transport:" << static_cast<int>(m_transportsConfig.primary);
ErrorCode primaryResult = ErrorCode::AmneziaServiceConnectionFailed;
switch (m_transportsConfig.primary) {
case PrimaryTransport::Http:
primaryResult = tryHttp();
break;
case PrimaryTransport::DnsUdp:
if (auto t = findDnsTransport(NetworkUtilities::DnsTransport::Udp)) primaryResult = tryDns(*t);
break;
case PrimaryTransport::DnsTcp:
if (auto t = findDnsTransport(NetworkUtilities::DnsTransport::Tcp)) primaryResult = tryDns(*t);
break;
case PrimaryTransport::DnsDot:
if (auto t = findDnsTransport(NetworkUtilities::DnsTransport::Tls)) primaryResult = tryDns(*t);
break;
case PrimaryTransport::DnsDoh:
if (auto t = findDnsTransport(NetworkUtilities::DnsTransport::Https)) primaryResult = tryDns(*t);
break;
case PrimaryTransport::DnsDoq:
if (auto t = findDnsTransport(NetworkUtilities::DnsTransport::Quic)) primaryResult = tryDns(*t);
break;
}
if (primaryResult == ErrorCode::NoError) {
return ErrorCode::NoError;
}
// === STEP 2: Primary failed — launch ALL other transports in parallel ===
qDebug() << "[Transport] Primary failed, launching parallel fallback";
std::atomic<bool> gotSuccess{false};
QByteArray successResult;
QString successTransport;
QMutex resultMutex;
QList<QFuture<void>> futures;
// HTTP (if not primary and enabled)
if (m_transportsConfig.primary != PrimaryTransport::Http && m_transportsConfig.httpEnabled) {
auto httpFuture = QtConcurrent::run([&]() {
if (gotSuccess.load()) return;
qDebug() << "[Transport] FALLBACK: Trying HTTP";
EncryptedRequestData httpRequestData = prepareRequest(endpoint, apiPayload, false);
if (httpRequestData.errorCode != ErrorCode::NoError) return;
QNetworkAccessManager nam;
QNetworkReply *reply = nam.post(httpRequestData.request, httpRequestData.requestBody);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec();
if (gotSuccess.load()) { reply->deleteLater(); return; }
QByteArray encryptedBody = reply->readAll();
auto replyError = reply->error();
reply->deleteLater();
if (replyError != QNetworkReply::NoError || encryptedBody.isEmpty()) return;
try {
QSimpleCrypto::QBlockCipher blockCipher;
QByteArray decrypted = blockCipher.decryptAesBlockCipher(encryptedBody, httpRequestData.key, httpRequestData.iv, "", httpRequestData.salt);
if (!gotSuccess.exchange(true)) {
QMutexLocker lock(&resultMutex);
successResult = decrypted;
successTransport = "HTTP";
}
} catch (...) {}
});
futures.append(httpFuture);
}
// DNS transports (skip the one that was primary)
for (const auto &transport : m_transportsConfig.dnsTransports) {
if (!transport.isValid()) continue;
// Skip if this was primary
bool wasPrimary = false;
switch (m_transportsConfig.primary) {
case PrimaryTransport::DnsUdp: wasPrimary = (transport.type == NetworkUtilities::DnsTransport::Udp); break;
case PrimaryTransport::DnsTcp: wasPrimary = (transport.type == NetworkUtilities::DnsTransport::Tcp); break;
case PrimaryTransport::DnsDot: wasPrimary = (transport.type == NetworkUtilities::DnsTransport::Tls); break;
case PrimaryTransport::DnsDoh: wasPrimary = (transport.type == NetworkUtilities::DnsTransport::Https); break;
case PrimaryTransport::DnsDoq: wasPrimary = (transport.type == NetworkUtilities::DnsTransport::Quic); break;
default: break;
}
if (wasPrimary) continue;
auto dnsFuture = QtConcurrent::run([&, transport]() {
if (gotSuccess.load()) return;
QString transportName;
switch (transport.type) {
case NetworkUtilities::DnsTransport::Udp: transportName = "UDP"; break;
case NetworkUtilities::DnsTransport::Tcp: transportName = "TCP"; break;
case NetworkUtilities::DnsTransport::Tls: transportName = "DoT"; break;
case NetworkUtilities::DnsTransport::Https: transportName = "DoH"; break;
case NetworkUtilities::DnsTransport::Quic: transportName = "DoQ"; break;
}
qDebug() << "[Transport] FALLBACK: Trying DNS" << transportName;
QByteArray dnsResponse = NetworkUtilities::sendViaDnsTunnel(
encRequestData.requestBody, endpointName, transport.domain,
transport.server, transport.type, transport.port, m_requestTimeoutMsecs, transport.dohPath);
if (dnsResponse.isEmpty()) return;
try {
QSimpleCrypto::QBlockCipher blockCipher;
QByteArray decrypted = blockCipher.decryptAesBlockCipher(dnsResponse, encRequestData.key, encRequestData.iv, "", encRequestData.salt);
if (!gotSuccess.exchange(true)) {
QMutexLocker lock(&resultMutex);
successResult = decrypted;
successTransport = "DNS-" + transportName;
}
} catch (...) {}
});
futures.append(dnsFuture);
}
// CRITICAL: Wait for ALL futures to complete to prevent use-after-free
// (lambdas capture references to local variables like resultMutex, successResult)
for (auto &future : futures) {
future.waitForFinished();
}
if (gotSuccess.load()) {
responseBody = successResult;
qDebug() << "[Transport] FALLBACK success via" << successTransport;
return ErrorCode::NoError;
}
qDebug() << "[Transport] All transports failed";
return ErrorCode::AmneziaServiceConnectionFailed;
}
QString GatewayController::resolveGatewayHostname(const QString &hostname)
{
if (m_dnsServer.isEmpty()) {
@@ -116,8 +495,8 @@ ErrorCode GatewayController::postViaDns(const QString &endpoint, const QJsonObje
return ErrorCode::AmneziaServiceConnectionFailed;
}
// Prepare encrypted request
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
// Prepare encrypted request (skip DNS resolve - we send directly to m_dnsServer)
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload, true);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
@@ -169,7 +548,7 @@ ErrorCode GatewayController::postViaDns(const QString &endpoint, const QJsonObje
}
}
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload, bool skipDnsResolve)
{
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
@@ -183,13 +562,14 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
// DNS резолв через TCP/UDP
// DNS резолв через TCP/UDP (пропускаем для DNS tunneling — там запрос идёт напрямую к DNS серверу)
QString finalGatewayEndpoint = m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl;
QUrl gatewayUrl(finalGatewayEndpoint);
QString hostname = gatewayUrl.host();
// Проверяем, нужно ли резолвить (если это не IP адрес и не localhost)
if (!hostname.isEmpty() &&
if (!skipDnsResolve &&
!hostname.isEmpty() &&
hostname != "localhost" &&
!NetworkUtilities::checkIPv4Format(hostname) &&
QHostAddress(hostname).isNull()) {

View File

@@ -2,6 +2,8 @@
#define GATEWAYCONTROLLER_H
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include <QNetworkReply>
#include <QObject>
#include <QPair>
@@ -15,6 +17,33 @@
#include "platforms/ios/ios_controller.h"
#endif
// NEW: Configuration for a single DNS transport (each can have its own server/domain)
struct DnsTransportEntry {
NetworkUtilities::DnsTransport type = NetworkUtilities::DnsTransport::Udp;
QString server; // DNS server IP
QString domain; // Base domain for tunneling
quint16 port = 15353;
QString dohPath = "/dns-query";
bool isValid() const { return !server.isEmpty() && !domain.isEmpty(); }
};
// NEW: Primary transport type
enum class PrimaryTransport { Http, DnsUdp, DnsTcp, DnsDot, DnsDoh, DnsDoq };
// NEW: Full transports configuration
struct TransportsConfig {
PrimaryTransport primary = PrimaryTransport::Http;
bool httpEnabled = true;
QString httpEndpoint;
QList<DnsTransportEntry> dnsTransports;
int retryCount = 3;
int timeoutMs = 10000;
bool isValid() const { return httpEnabled || !dnsTransports.isEmpty(); }
static TransportsConfig fromJson(const QJsonObject &json);
};
class GatewayController : public QObject
{
Q_OBJECT
@@ -32,6 +61,13 @@ public:
// DNS tunneling - send request via DNS transport
amnezia::ErrorCode postViaDns(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
// NEW: Load config from file or environment variable
bool loadTransportsConfig(const QString &filePath, const QString &envVarName = "AMNEZIA_GATEWAY");
void setTransportsConfig(const TransportsConfig &config);
// NEW: Parallel request via all configured transports (primary first, then others)
amnezia::ErrorCode postParallel(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
private:
struct EncryptedRequestData
@@ -50,7 +86,7 @@ private:
bool isDecryptionSuccessful;
};
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload, bool skipDnsResolve = false);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
QString resolveGatewayHostname(const QString &hostname);
@@ -73,12 +109,15 @@ private:
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
// DNS transport settings
// DNS transport settings (for individual transport)
QString m_dnsServer;
QString m_dnsBaseDomain;
NetworkUtilities::DnsTransport m_dnsTransport = NetworkUtilities::DnsTransport::Udp;
quint16 m_dnsPort = 15353;
QString m_dohEndpoint = "/dns-query";
// NEW: Full transports configuration
TransportsConfig m_transportsConfig;
inline static QString m_proxyUrl;
};

View File

@@ -58,6 +58,8 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QUrl>
#include <QThread>
#include <QElapsedTimer>
namespace
{
@@ -1147,37 +1149,44 @@ namespace {
int pos = 12;
// Skip questions
for (int i = 0; i < qdCount && pos < response.size(); i++) {
while (pos < response.size() && data[pos] != 0) {
if ((data[pos] & 0xC0) == 0xC0) { pos += 2; break; }
pos += data[pos] + 1;
// Helper lambda to safely skip DNS name with bounds checking
auto skipDnsName = [&]() -> bool {
int maxLabels = 128; // Prevent infinite loops
while (pos < response.size() && data[pos] != 0 && maxLabels-- > 0) {
if ((data[pos] & 0xC0) == 0xC0) {
pos += 2;
return pos <= response.size();
}
int labelLen = data[pos];
if (pos + 1 + labelLen > response.size()) return false; // Bounds check
pos += labelLen + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
return pos <= response.size();
};
// Skip questions
for (int i = 0; i < qdCount && pos < response.size(); i++) {
if (!skipDnsName()) return meta;
if (pos + 4 > response.size()) return meta;
pos += 4; // QTYPE + QCLASS
}
// Skip answers
for (int i = 0; i < anCount && pos < response.size(); i++) {
while (pos < response.size() && data[pos] != 0) {
if ((data[pos] & 0xC0) == 0xC0) { pos += 2; break; }
pos += data[pos] + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
if (pos + 10 > response.size()) break;
if (!skipDnsName()) return meta;
if (pos + 10 > response.size()) return meta;
quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9];
if (pos + 10 + rdlen > response.size()) return meta;
pos += 10 + rdlen;
}
// Skip authority
for (int i = 0; i < nsCount && pos < response.size(); i++) {
while (pos < response.size() && data[pos] != 0) {
if ((data[pos] & 0xC0) == 0xC0) { pos += 2; break; }
pos += data[pos] + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
if (pos + 10 > response.size()) break;
if (!skipDnsName()) return meta;
if (pos + 10 > response.size()) return meta;
quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9];
if (pos + 10 + rdlen > response.size()) return meta;
pos += 10 + rdlen;
}
@@ -1187,17 +1196,14 @@ namespace {
if (pos < response.size() && data[pos] == 0) {
pos++; // Root label for OPT
} else {
while (pos < response.size() && data[pos] != 0) {
if ((data[pos] & 0xC0) == 0xC0) { pos += 2; break; }
pos += data[pos] + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
if (!skipDnsName()) return meta;
}
if (pos + 10 > response.size()) break;
if (pos + 10 > response.size()) return meta;
quint16 rtype = (data[pos] << 8) | data[pos + 1];
quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9];
if (pos + 10 + rdlen > response.size()) return meta;
pos += 10;
if (rtype == 41 && rdlen > 0) { // OPT record
@@ -1295,24 +1301,47 @@ namespace {
qDebug() << "[DNS Tunnel] Response header: id=" << transactionId << "flags=" << Qt::hex << flags
<< "questions=" << qdCount << "answers=" << anCount << "authority=" << nsCount << "additional=" << arCount;
// Validate QR bit - must be 1 for response (bit 15 of flags)
if ((flags & 0x8000) == 0) {
qDebug() << "[DNS Tunnel] Invalid response: QR bit not set (not a response)";
return QByteArray();
}
// Check for errors (RCODE in lower 4 bits of flags)
quint8 rcode = flags & 0x0F;
if (rcode != 0) {
qDebug() << "[DNS Tunnel] Response error, RCODE:" << rcode;
}
// Sanity check on counts to prevent excessive iteration
if (anCount > 100 || qdCount > 10) {
qDebug() << "[DNS Tunnel] Suspicious counts, likely garbage data";
return QByteArray();
}
// Helper lambda to safely skip DNS name with bounds checking
auto skipDnsName = [&]() -> bool {
int maxLabels = 128; // Prevent infinite loops
while (pos < response.size() && data[pos] != 0 && maxLabels-- > 0) {
if ((data[pos] & 0xC0) == 0xC0) {
pos += 2;
return pos <= response.size();
}
int labelLen = data[pos];
if (pos + 1 + labelLen > response.size()) return false;
pos += labelLen + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
return pos <= response.size();
};
// Skip question section
for (int i = 0; i < qdCount && pos < response.size(); i++) {
// Skip name
while (pos < response.size()) {
quint8 len = data[pos++];
if (len == 0) break;
if ((len & 0xC0) == 0xC0) {
pos++; // Skip pointer byte
break;
}
pos += len;
if (!skipDnsName()) {
qDebug() << "[DNS Tunnel] Failed to skip question name";
return QByteArray();
}
if (pos + 4 > response.size()) return QByteArray();
pos += 4; // Skip QTYPE and QCLASS
}
@@ -1321,15 +1350,9 @@ namespace {
// Read answer section - looking for TXT records
QByteArray combinedTxt;
for (int i = 0; i < anCount && pos < response.size(); i++) {
// Skip name (handle compression)
while (pos < response.size()) {
quint8 len = data[pos++];
if (len == 0) break;
if ((len & 0xC0) == 0xC0) {
pos++; // Skip pointer byte
break;
}
pos += len;
if (!skipDnsName()) {
qDebug() << "[DNS Tunnel] Failed to skip answer name at pos=" << pos;
break;
}
if (pos + 10 > response.size()) {
@@ -1344,13 +1367,21 @@ namespace {
qDebug() << "[DNS Tunnel] Answer" << i << ": type=" << rtype << "class=" << rclass << "ttl=" << ttl << "rdlen=" << rdlength;
// Bounds check for rdlength
if (pos + rdlength > response.size()) {
qDebug() << "[DNS Tunnel] rdlength exceeds buffer, truncating";
break;
}
if (rtype == 16) { // TXT record
int rdEnd = pos + rdlength;
while (pos < rdEnd && pos < response.size()) {
quint8 txtLen = data[pos++];
if (txtLen > 0 && pos + txtLen <= rdEnd) {
if (txtLen > 0 && pos + txtLen <= rdEnd && pos + txtLen <= response.size()) {
combinedTxt.append(reinterpret_cast<const char*>(data + pos), txtLen);
pos += txtLen;
} else {
break; // Invalid TXT length
}
}
} else {
@@ -1396,9 +1427,9 @@ QByteArray NetworkUtilities::sendViaDnsTunnel(const QByteArray &payload, const Q
case DnsTransport::Https:
return sendViaDnsTunnelHttps(payload, queryName, dnsServer, port, dohEndpoint, timeoutMsecs);
case DnsTransport::Quic:
// DoQ uses QUIC - not yet implemented, fallback to chunked UDP
qDebug() << "[DNS Tunnel] DoQ not yet implemented, falling back to UDP";
return sendViaDnsTunnelUdpChunked(payload, queryName, dnsServer, port, timeoutMsecs);
// DoQ uses QUIC - not yet implemented
qDebug() << "[DNS Tunnel] DoQ not yet implemented";
return QByteArray(); // Return empty to trigger fallback to other transports
}
return QByteArray();
}
@@ -1593,45 +1624,49 @@ QByteArray NetworkUtilities::sendViaDnsTunnelTls(const QByteArray &payload, cons
qDebug() << "[DNS Tunnel DoT] Sent" << bytesWritten << "bytes";
// Wait for response
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(&socket, &QSslSocket::readyRead, [&]() {
if (socket.bytesAvailable() >= 2 && response.isEmpty()) {
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() == 2) {
quint16 responseLength = qFromBigEndian<quint16>(*reinterpret_cast<const quint16*>(lengthBytes.constData()));
while (socket.bytesAvailable() < responseLength) {
if (!socket.waitForReadyRead(timeoutMsecs / 2)) {
break;
}
}
if (socket.bytesAvailable() >= responseLength) {
response = socket.read(responseLength);
responseReceived = true;
loop.quit();
}
}
}
});
// Synchronous read: first get 2-byte length prefix
QElapsedTimer timer;
timer.start();
loop.exec();
timer.stop();
socket.close();
if (!responseReceived || response.isEmpty()) {
qDebug() << "[DNS Tunnel DoT] No response received (timeout)";
while (socket.bytesAvailable() < 2) {
int remaining = timeoutMsecs - timer.elapsed();
if (remaining <= 0 || !socket.waitForReadyRead(remaining)) {
qDebug() << "[DNS Tunnel DoT] Timeout waiting for response length";
socket.close();
return QByteArray();
}
}
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() != 2) {
qDebug() << "[DNS Tunnel DoT] Failed to read response length";
socket.close();
return QByteArray();
}
quint16 responseLength = qFromBigEndian<quint16>(*reinterpret_cast<const quint16*>(lengthBytes.constData()));
// Now read the full response body
QByteArray response;
while (response.size() < responseLength) {
int remaining = timeoutMsecs - timer.elapsed();
if (remaining <= 0) {
qDebug() << "[DNS Tunnel DoT] Timeout waiting for response body";
socket.close();
return QByteArray();
}
if (socket.bytesAvailable() > 0) {
response.append(socket.read(responseLength - response.size()));
} else if (!socket.waitForReadyRead(remaining)) {
qDebug() << "[DNS Tunnel DoT] Timeout waiting for more data";
socket.close();
return QByteArray();
}
}
socket.close();
qDebug() << "[DNS Tunnel DoT] Received response:" << response.size() << "bytes";
return parseDnsTxtResponse(response);
}
@@ -1649,7 +1684,9 @@ QByteArray NetworkUtilities::sendViaDnsTunnelHttps(const QByteArray &payload, co
}
// DoH uses HTTP POST with application/dns-message
QString url = QString("http://%1:%2%3").arg(dnsServer).arg(port).arg(endpoint);
// Use HTTPS for port 443, HTTP for other ports (like 80 for local testing)
QString scheme = (port == 443) ? "https" : "http";
QString url = QString("%1://%2:%3%4").arg(scheme).arg(dnsServer).arg(port).arg(endpoint);
qDebug() << "[DNS Tunnel DoH] Sending to" << url;
@@ -1702,15 +1739,24 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
return QByteArray();
}
// Helper lambda to send UDP request and get raw response
auto sendUdpRequest = [&](const QByteArray &query) -> QByteArray {
// Constants for retry logic
constexpr int MAX_INITIAL_RETRIES = 3;
constexpr int MAX_CHUNK_RETRIES = 2;
constexpr int MAX_CONCURRENT_REQUESTS = 5;
constexpr int BASE_TIMEOUT_MS = 2000;
// Helper lambda to send UDP request with configurable timeout
auto sendUdpRequestWithTimeout = [&](const QByteArray &query, int requestTimeoutMs) -> QByteArray {
QUdpSocket socket;
socket.writeDatagram(query, dnsAddress, port);
qint64 written = socket.writeDatagram(query, dnsAddress, port);
if (written != query.size()) {
return QByteArray();
}
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs / 5); // Shorter timeout per request
timer.setInterval(requestTimeoutMs);
QByteArray response;
bool responseReceived = false;
@@ -1734,7 +1780,27 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
return responseReceived ? response : QByteArray();
};
// Send initial request with payload
// Helper lambda with retry and exponential backoff
auto sendWithRetry = [&](const QByteArray &query, int maxRetries) -> QByteArray {
for (int attempt = 0; attempt < maxRetries; attempt++) {
int timeout = BASE_TIMEOUT_MS * (attempt + 1); // 2s, 4s, 6s
qDebug() << "[DNS Tunnel UDP Chunked] Attempt" << (attempt + 1) << "/" << maxRetries
<< "timeout:" << timeout << "ms";
QByteArray response = sendUdpRequestWithTimeout(query, timeout);
if (!response.isEmpty()) {
return response;
}
if (attempt < maxRetries - 1) {
qDebug() << "[DNS Tunnel UDP Chunked] Retry after" << (timeout / 2) << "ms";
QThread::msleep(timeout / 2);
}
}
return QByteArray();
};
// === Step 1: Send initial request with retry ===
quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
QByteArray initialQuery = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
@@ -1744,10 +1810,10 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
}
qDebug() << "[DNS Tunnel UDP Chunked] Sending initial request, payload:" << payload.size() << "bytes";
QByteArray firstResponse = sendUdpRequest(initialQuery);
QByteArray firstResponse = sendWithRetry(initialQuery, MAX_INITIAL_RETRIES);
if (firstResponse.isEmpty()) {
qDebug() << "[DNS Tunnel UDP Chunked] No response for initial request";
qDebug() << "[DNS Tunnel UDP Chunked] No response for initial request after" << MAX_INITIAL_RETRIES << "attempts";
return QByteArray();
}
@@ -1762,7 +1828,6 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
// Check if response is chunked
if (meta.totalChunks <= 1) {
// Not chunked or single chunk - return as-is
qDebug() << "[DNS Tunnel UDP Chunked] Single chunk response:" << firstTxtData.size() << "bytes";
return firstTxtData;
}
@@ -1770,51 +1835,137 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
qDebug() << "[DNS Tunnel UDP Chunked] Chunked response: total=" << meta.totalChunks
<< "size=" << meta.totalSize << "chunkId=" << meta.chunkId.toHex();
// Collect all chunks
// === Step 2: Collect all chunks ===
QMap<int, QByteArray> chunks;
chunks[0] = firstTxtData;
// Request remaining chunks
// Build list of chunks to request
QList<int> chunksToRequest;
for (int i = 1; i < meta.totalChunks; i++) {
qDebug() << "[DNS Tunnel UDP Chunked] Requesting chunk" << i;
chunksToRequest.append(i);
}
// === Step 3: Request chunks in parallel batches with retry ===
auto requestChunksBatch = [&](const QList<int> &chunkIndices, int batchTimeout) {
if (chunkIndices.isEmpty()) return;
quint16 chunkTxId = static_cast<quint16>((QDateTime::currentMSecsSinceEpoch() + i) & 0xFFFF);
QByteArray chunkQuery = buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, i);
// Create sockets and send requests for this batch
QList<QSharedPointer<QUdpSocket>> sockets;
QMap<QUdpSocket*, int> socketToIndex;
if (chunkQuery.isEmpty()) {
qDebug() << "[DNS Tunnel UDP Chunked] Failed to build chunk request" << i;
continue;
for (int idx : chunkIndices) {
if (chunks.contains(idx)) continue; // Already have this chunk
quint16 chunkTxId = static_cast<quint16>((QDateTime::currentMSecsSinceEpoch() + idx) & 0xFFFF);
QByteArray chunkQuery = buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, idx);
if (chunkQuery.isEmpty()) {
qDebug() << "[DNS Tunnel UDP Chunked] Failed to build chunk request" << idx;
continue;
}
auto socket = QSharedPointer<QUdpSocket>::create();
socket->writeDatagram(chunkQuery, dnsAddress, port);
socketToIndex[socket.data()] = idx;
sockets.append(socket);
}
QByteArray chunkResponse = sendUdpRequest(chunkQuery);
if (chunkResponse.isEmpty()) {
qDebug() << "[DNS Tunnel UDP Chunked] No response for chunk" << i;
continue;
if (sockets.isEmpty()) return;
qDebug() << "[DNS Tunnel UDP Chunked] Sent" << sockets.size() << "parallel requests";
// Wait for responses with deadline
QEventLoop loop;
QTimer deadline;
deadline.setSingleShot(true);
deadline.setInterval(batchTimeout);
int receivedCount = 0;
int expectedCount = sockets.size();
QObject::connect(&deadline, &QTimer::timeout, &loop, &QEventLoop::quit);
for (auto &socket : sockets) {
QObject::connect(socket.data(), &QUdpSocket::readyRead, [&, sock = socket.data()]() {
while (sock->hasPendingDatagrams()) {
QNetworkDatagram datagram = sock->receiveDatagram();
if (datagram.isValid()) {
QByteArray chunkTxtData = parseDnsTxtResponse(datagram.data());
if (!chunkTxtData.isEmpty()) {
ChunkMeta chunkMeta = parseChunkMeta(datagram.data());
int idx = (chunkMeta.totalChunks > 0) ? chunkMeta.chunkIndex : socketToIndex.value(sock, -1);
if (idx >= 0 && !chunks.contains(idx)) {
chunks[idx] = chunkTxtData;
qDebug() << "[DNS Tunnel UDP Chunked] Received chunk" << idx << ":" << chunkTxtData.size() << "bytes";
receivedCount++;
}
}
}
}
// Exit early if we have all chunks
if (receivedCount >= expectedCount || chunks.size() >= meta.totalChunks) {
loop.quit();
}
});
}
QByteArray chunkTxtData = parseDnsTxtResponse(chunkResponse);
if (!chunkTxtData.isEmpty()) {
ChunkMeta chunkMeta = parseChunkMeta(chunkResponse);
int idx = (chunkMeta.totalChunks > 0) ? chunkMeta.chunkIndex : i;
chunks[idx] = chunkTxtData;
qDebug() << "[DNS Tunnel UDP Chunked] Received chunk" << idx << ":" << chunkTxtData.size() << "bytes";
deadline.start();
loop.exec();
deadline.stop();
};
// Process chunks in batches
int totalTimeout = qMax(timeoutMsecs / 2, 5000); // At least 5 seconds for chunks
int batchTimeout = totalTimeout / (MAX_CHUNK_RETRIES + 1);
for (int retryRound = 0; retryRound <= MAX_CHUNK_RETRIES; retryRound++) {
// Find missing chunks
QList<int> missing;
for (int i = 1; i < meta.totalChunks; i++) {
if (!chunks.contains(i)) {
missing.append(i);
}
}
if (missing.isEmpty()) {
qDebug() << "[DNS Tunnel UDP Chunked] All chunks received";
break;
}
if (retryRound > 0) {
qDebug() << "[DNS Tunnel UDP Chunked] Retry round" << retryRound << "for" << missing.size() << "missing chunks";
}
// Process in batches of MAX_CONCURRENT_REQUESTS
for (int batchStart = 0; batchStart < missing.size(); batchStart += MAX_CONCURRENT_REQUESTS) {
QList<int> batch = missing.mid(batchStart, MAX_CONCURRENT_REQUESTS);
requestChunksBatch(batch, batchTimeout);
}
}
// Check if we have all chunks
if (chunks.size() != meta.totalChunks) {
qDebug() << "[DNS Tunnel UDP Chunked] Missing chunks:" << chunks.size() << "/" << meta.totalChunks;
// Try to return what we have anyway
}
// Combine all chunks in order
QByteArray combined;
// === Step 4: Verify all chunks received ===
QList<int> finalMissing;
for (int i = 0; i < meta.totalChunks; i++) {
if (chunks.contains(i)) {
combined.append(chunks[i]);
if (!chunks.contains(i)) {
finalMissing.append(i);
}
}
qDebug() << "[DNS Tunnel UDP Chunked] Combined" << chunks.size() << "chunks," << combined.size() << "bytes";
if (!finalMissing.isEmpty()) {
qDebug() << "[DNS Tunnel UDP Chunked] FAILED: Missing chunks after all retries:" << finalMissing;
qDebug() << "[DNS Tunnel UDP Chunked] Received" << chunks.size() << "/" << meta.totalChunks << "chunks";
return QByteArray(); // Return empty - don't return partial/corrupted data
}
// === Step 5: Combine all chunks in order ===
QByteArray combined;
combined.reserve(meta.totalSize > 0 ? meta.totalSize : meta.totalChunks * 500);
for (int i = 0; i < meta.totalChunks; i++) {
combined.append(chunks[i]);
}
qDebug() << "[DNS Tunnel UDP Chunked] SUCCESS: Combined" << meta.totalChunks << "chunks," << combined.size() << "bytes";
return combined;
}

View File

@@ -0,0 +1,44 @@
{
"primary": "http",
"retry_count": 3,
"timeout_ms": 10000,
"http": {
"enabled": true,
"endpoint": "https://your-gateway.example.com/"
},
"dns_transports": [
{
"type": "udp",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 5453
},
{
"type": "tcp",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 5453
},
{
"type": "dot",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 8853
},
{
"type": "doh",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 443,
"path": "/dns-query"
},
{
"type": "doq",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 8854
}
]
}

View File

@@ -14,7 +14,8 @@ namespace
const char cloudFlareNs1[] = "1.1.1.1";
const char cloudFlareNs2[] = "1.0.0.1";
constexpr char gatewayEndpoint[] = "http://localhost:80/";
//constexpr char gatewayEndpoint[] = "http://localhost:80/";
constexpr char gatewayEndpoint[] = "http://127.0.0.1:80/";
}
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)

View File

@@ -724,9 +724,6 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
QThread::msleep(10);
#endif
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
@@ -742,32 +739,18 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
apiPayload[configKey::apiEndpoint] = serverConfig.value(configKey::apiEndpoint).toString();
QByteArray responseBody;
QString baseDomain = "gateway.example.com";
QString endpoint = QString("%1v1/proxy_config");
// Try DNS transports first
// 1. UDP
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Udp, 15353);
ErrorCode errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
// 2. TCP
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tcp, 15353);
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
}
if (errorCode != ErrorCode::NoError) {
// 3. DoT
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tls, 8853);
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
}
if (errorCode != ErrorCode::NoError) {
// 4. DoH
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Https, 80, "/dns-query");
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
}
if (errorCode != ErrorCode::NoError) {
// 5. Fallback to HTTP
errorCode = gatewayController.post(endpoint, apiPayload, responseBody);
}
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
// Load transports config from file or env
gatewayController.loadTransportsConfig("gateway.json", "AMNEZIA_GATEWAY");
ErrorCode errorCode = gatewayController.postParallel(endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) {
errorCode = fillServerConfig(serviceProtocol, protocolData, responseBody, serverConfig);
@@ -974,42 +957,14 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
// 1. HTTP (primary transport)
ErrorCode result = gatewayController.post(endpoint, apiPayload, responseBody);
if (result == ErrorCode::NoError) return result;
// Load transports config from file or env
gatewayController.loadTransportsConfig("gateway.json", "AMNEZIA_GATEWAY");
qDebug() << "[Transport] HTTP failed, trying DNS transports as fallback";
// DNS tunneling fallback - base_domain: gateway.example.com
// Порты бэкенда: UDP/TCP=15353, DoT=8853, DoQ=8854
const QString baseDomain = "gateway.example.com";
// 2. DNS UDP (порт 15353)
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Udp, 15353);
result = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (result == ErrorCode::NoError) return result;
// 3. DNS TCP (порт 15353)
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tcp, 15353);
result = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (result == ErrorCode::NoError) return result;
// 4. DoT (порт 8853)
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tls, 8853);
result = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (result == ErrorCode::NoError) return result;
// 5. DoH (порт 80, endpoint /dns-query)
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Https, 80, "/dns-query");
result = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (result == ErrorCode::NoError) return result;
// 6. DoQ (порт 8854)
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Quic, 8854);
result = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
return result;
// Parallel request via all configured transports (HTTP + DNS)
return gatewayController.postParallel(endpoint, apiPayload, responseBody);
}

View File

@@ -1,6 +1,7 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/networkUtilities.h"
#include <QJsonDocument>
#include <QJsonObject>
@@ -32,8 +33,6 @@ void ApiNewsController::fetchNews(bool showError)
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
@@ -45,74 +44,36 @@ void ApiNewsController::fetchNews(bool showError)
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
QString baseDomain = "gateway.example.com";
QString endpoint = QString("%1v1/news");
// 1. HTTP (primary transport - async)
auto future = gatewayController->postAsync(endpoint, payload);
future.then(this, [this, showError, gatewayController, baseDomain, endpoint, payload](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
// HTTP succeeded
if (errorCode == ErrorCode::NoError) {
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
return;
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
// Load transports config from file or env
gatewayController.loadTransportsConfig("gateway.json", "AMNEZIA_GATEWAY");
QByteArray responseBody;
ErrorCode errorCode = gatewayController.postParallel(endpoint, payload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
}
// Parse response
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
// HTTP failed, try DNS transports as fallback (synchronous)
qDebug() << "[Transport] HTTP failed, trying DNS transports as fallback";
GatewayController dnsGateway(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QByteArray dnsResponseBody;
ErrorCode dnsError = ErrorCode::UnknownError;
// 2. DNS UDP
dnsGateway.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Udp, 15353);
dnsError = dnsGateway.postViaDns(endpoint, payload, dnsResponseBody);
if (dnsError != ErrorCode::NoError) {
// 3. DNS TCP
dnsGateway.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tcp, 15353);
dnsError = dnsGateway.postViaDns(endpoint, payload, dnsResponseBody);
}
if (dnsError != ErrorCode::NoError) {
// 4. DoT
dnsGateway.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tls, 8853);
dnsError = dnsGateway.postViaDns(endpoint, payload, dnsResponseBody);
}
if (dnsError != ErrorCode::NoError) {
// 5. DoH
dnsGateway.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Https, 80, "/dns-query");
dnsError = dnsGateway.postViaDns(endpoint, payload, dnsResponseBody);
}
if (dnsError != ErrorCode::NoError) {
emit errorOccurred(dnsError, showError);
return;
}
// DNS succeeded
QJsonDocument doc = QJsonDocument::fromJson(dnsResponseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
});
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
}

View File

@@ -60,8 +60,6 @@ bool ApiSettingsController::getAccountInfo(bool reload)
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
@@ -71,35 +69,19 @@ bool ApiSettingsController::getAccountInfo(bool reload)
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
QString baseDomain = "gateway.example.com";
QString endpoint = QString("%1v1/account_info");
// 1. HTTP (primary transport)
ErrorCode errorCode = gatewayController.post(endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) goto success;
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
qDebug() << "[Transport] HTTP failed, trying DNS transports as fallback";
// Load transports config from file or env
gatewayController.loadTransportsConfig("gateway.json", "AMNEZIA_GATEWAY");
// 2. DNS UDP
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Udp, 15353);
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) goto success;
ErrorCode errorCode = gatewayController.postParallel(endpoint, apiPayload, responseBody);
// 3. DNS TCP
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tcp, 15353);
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) goto success;
// 4. DoT
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Tls, 8853);
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) goto success;
// 5. DoH
gatewayController.setDnsServer("127.0.0.1", baseDomain, NetworkUtilities::DnsTransport::Https, 80, "/dns-query");
errorCode = gatewayController.postViaDns(endpoint, apiPayload, responseBody);
success:
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;