diff --git a/.gitignore b/.gitignore index e05974b0c..f41ec95f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # User settings *.user + +# Gateway configs (contains sensitive endpoints) +gateway.json +client/gateway.json macOSPackage/ AmneziaVPN.dmg AmneziaVPN.exe diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 3a9ffb27a..3d84f0a86 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -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 + $ + COMMENT "Copying gateway.json to build directory" + ) +endif() + target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC}) qt_finalize_target(${PROJECT}) diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index c8a6a05da..99e9544fc 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -1,18 +1,23 @@ #include "gatewayController.h" #include +#include #include #include #include +#include #include #include #include +#include #include +#include #include #include #include #include +#include #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(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(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(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 gotSuccess{false}; + QByteArray successResult; + QString successTransport; + QMutex resultMutex; + QList> 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()) { diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index 5b9bcc3d0..30825dcd8 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -2,6 +2,8 @@ #define GATEWAYCONTROLLER_H #include +#include +#include #include #include #include @@ -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 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; }; diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index 95ab6199c..c4622e4cf 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -58,6 +58,8 @@ #include #include #include +#include +#include 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(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(*reinterpret_cast(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(*reinterpret_cast(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(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 chunks; chunks[0] = firstTxtData; - // Request remaining chunks + // Build list of chunks to request + QList 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 &chunkIndices, int batchTimeout) { + if (chunkIndices.isEmpty()) return; - quint16 chunkTxId = static_cast((QDateTime::currentMSecsSinceEpoch() + i) & 0xFFFF); - QByteArray chunkQuery = buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, i); + // Create sockets and send requests for this batch + QList> sockets; + QMap 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((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::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 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 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 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; } diff --git a/client/gateway.example.json b/client/gateway.example.json new file mode 100644 index 000000000..eae10d7e5 --- /dev/null +++ b/client/gateway.example.json @@ -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 + } + ] +} diff --git a/client/settings.cpp b/client/settings.cpp index 5996a5de4..3a0fa9fce 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -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) diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 4d9d9267f..3bc220417 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -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); } diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp index 04e33f6f7..04b96dd66 100644 --- a/client/ui/controllers/api/apiNewsController.cpp +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -1,6 +1,7 @@ #include "apiNewsController.h" #include "core/api/apiUtils.h" +#include "core/controllers/gatewayController.h" #include "core/networkUtilities.h" #include #include @@ -32,8 +33,6 @@ void ApiNewsController::fetchNews(bool showError) return; } - auto gatewayController = QSharedPointer::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 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(); } diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 8318c5145..49b6f1cf2 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -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;