diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ed828841a..a7fe48e6c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -44,6 +44,10 @@ add_definitions(-DAGW_DNS_DOH_PATH="$ENV{AGW_DNS_DOH_PATH}") add_definitions(-DAGW_DNS_RETRY_COUNT="$ENV{AGW_DNS_RETRY_COUNT}") add_definitions(-DAGW_DNS_TIMEOUT_MS="$ENV{AGW_DNS_TIMEOUT_MS}") +if(DEFINED ENV{AGW_INSECURE_SSL} AND NOT "$ENV{AGW_INSECURE_SSL}" STREQUAL "" AND NOT "$ENV{AGW_INSECURE_SSL}" STREQUAL "0") + add_definitions(-DAGW_INSECURE_SSL=1) +endif() + if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) set(PACKAGES ${PACKAGES} Widgets) endif() diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index d592b646b..1f1830020 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -23,6 +23,12 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/version.h ${CLIENT_ROOT_DIR}/core/sshclient.h ${CLIENT_ROOT_DIR}/core/networkUtilities.h + ${CLIENT_ROOT_DIR}/core/transport/igatewaytransport.h + ${CLIENT_ROOT_DIR}/core/transport/httpGatewayTransport.h + ${CLIENT_ROOT_DIR}/core/transport/dnsGatewayTransport.h + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsResolver.h + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsTunnel.h + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsPacket_p.h ${CLIENT_ROOT_DIR}/core/serialization/serialization.h ${CLIENT_ROOT_DIR}/core/serialization/transfer.h ${CLIENT_ROOT_DIR}/../common/logger/logger.h @@ -68,6 +74,11 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/protocols/vpnprotocol.cpp ${CLIENT_ROOT_DIR}/core/sshclient.cpp ${CLIENT_ROOT_DIR}/core/networkUtilities.cpp + ${CLIENT_ROOT_DIR}/core/transport/httpGatewayTransport.cpp + ${CLIENT_ROOT_DIR}/core/transport/dnsGatewayTransport.cpp + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsResolver.cpp + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsTunnel.cpp + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsPacket.cpp ${CLIENT_ROOT_DIR}/core/serialization/outbound.cpp ${CLIENT_ROOT_DIR}/core/serialization/inbound.cpp ${CLIENT_ROOT_DIR}/core/serialization/ss.cpp diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 57d7c33ee..b0fcba108 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -1,21 +1,11 @@ #include "gatewayController.h" -#include -#include -#include -#include - -#include +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include +#include +#include #include #include @@ -23,14 +13,10 @@ #include "QRsa.h" #include "amnezia_application.h" -#include "core/api/apiUtils.h" -#include "core/networkUtilities.h" +#include "core/transport/dnsGatewayTransport.h" +#include "core/transport/httpGatewayTransport.h" #include "utilities.h" -#ifdef AMNEZIA_DESKTOP - #include "core/ipcclient.h" -#endif - namespace { namespace configKey @@ -43,24 +29,25 @@ namespace constexpr char keyPayload[] = "key_payload"; } - constexpr QLatin1String errorResponsePattern1("No active configuration found for"); - constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for"); - constexpr QLatin1String errorResponsePattern3("Account not found."); + amnezia::transport::dns::DnsProtocol dnsProtocolFromPrimary(PrimaryTransport p) + { + switch (p) { + case PrimaryTransport::DnsUdp: return amnezia::transport::dns::DnsProtocol::Udp; + case PrimaryTransport::DnsTcp: return amnezia::transport::dns::DnsProtocol::Tcp; + case PrimaryTransport::DnsDot: return amnezia::transport::dns::DnsProtocol::Tls; + case PrimaryTransport::DnsDoh: return amnezia::transport::dns::DnsProtocol::Https; + case PrimaryTransport::DnsDoq: return amnezia::transport::dns::DnsProtocol::Quic; + default: return amnezia::transport::dns::DnsProtocol::Udp; + } + } +} // namespace - constexpr QLatin1String updateRequestResponsePattern("client version update is required"); - - constexpr int httpStatusCodeNotFound = 404; - constexpr int httpStatusCodeConflict = 409; - - constexpr int httpStatusCodeNotImplemented = 501; -} - -// Parse TransportsConfig from JSON TransportsConfig TransportsConfig::fromJson(const QJsonObject &json) { + using amnezia::transport::dns::DnsProtocol; + TransportsConfig config; - - // Parse primary transport + QString primaryStr = json.value("primary").toString("http").toLower(); if (primaryStr == "http") { config.primary = PrimaryTransport::Http; @@ -75,136 +62,139 @@ TransportsConfig TransportsConfig::fromJson(const QJsonObject &json) } 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; - } + entry.type = DnsProtocol::Udp; } else if (typeStr == "tcp") { - entry.type = NetworkUtilities::DnsTransport::Tcp; - if (entry.port == 15353 && !transportObj.contains("port")) { - entry.port = 15353; - } + entry.type = DnsProtocol::Tcp; } else if (typeStr == "dot" || typeStr == "tls") { - entry.type = NetworkUtilities::DnsTransport::Tls; - if (!transportObj.contains("port")) { - entry.port = 8853; - } + entry.type = DnsProtocol::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; - } + entry.type = DnsProtocol::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; - } + entry.type = DnsProtocol::Quic; + if (!transportObj.contains("port")) entry.port = 8853; } else { - continue; // Skip unknown transport + continue; } - + 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) +GatewayController::GatewayController(const QString &gatewayEndpoint, + const bool isDevEnvironment, + const int requestTimeoutMsecs, + const bool isStrictKillSwitchEnabled, + QObject *parent) : QObject(parent), + m_requestTimeoutMsecs(requestTimeoutMsecs), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment), - m_requestTimeoutMsecs(requestTimeoutMsecs), m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) { + auto httpTransport = std::make_shared( + m_gatewayEndpoint, m_isDevEnvironment, m_requestTimeoutMsecs, m_isStrictKillSwitchEnabled); + { + QMutexLocker lock(&m_transportMutex); + m_transport = std::move(httpTransport); + } } -void GatewayController::setDnsServer(const QString &dnsServer, const QString &baseDomain, - NetworkUtilities::DnsTransport transport, quint16 port, const QString &dohEndpoint) +std::shared_ptr GatewayController::buildTransport( + const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled) { - m_dnsServer = dnsServer; - m_dnsBaseDomain = baseDomain; - m_dnsTransport = transport; - m_dnsPort = port; - m_dohEndpoint = dohEndpoint; - - QString transportName; - switch (transport) { - 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; + using namespace amnezia::transport; + + auto makeHttp = [&](const QString &httpEndpoint) { + return std::make_shared( + httpEndpoint, isDevEnvironment, requestTimeoutMsecs, isStrictKillSwitchEnabled); + }; + + if (config.primary == PrimaryTransport::Http) { + return makeHttp(config.httpEndpoint); } - qDebug() << "[DNS Tunnel] Server:" << dnsServer << "BaseDomain:" << baseDomain - << "Transport:" << transportName << "Port:" << port; + + const auto wantedProtocol = dnsProtocolFromPrimary(config.primary); + for (const auto &entry : config.dnsTransports) { + if (entry.type == wantedProtocol && entry.isValid()) { + return std::make_shared( + entry.type, entry.server, entry.domain, entry.port, + requestTimeoutMsecs, isStrictKillSwitchEnabled, entry.dohPath); + } + } + + return makeHttp(config.httpEndpoint); } 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; + if (!config.httpEndpoint.isEmpty()) { + m_gatewayEndpoint = config.httpEndpoint; + } + + TransportsConfig effective = config; + if (effective.httpEndpoint.isEmpty()) { + effective.httpEndpoint = m_gatewayEndpoint; + } + + auto newTransport = buildTransport(effective, m_requestTimeoutMsecs, m_isDevEnvironment, m_isStrictKillSwitchEnabled); + QString activeName; + { + QMutexLocker lock(&m_transportMutex); + m_transport = std::move(newTransport); + activeName = m_transport ? m_transport->name() : QStringLiteral("none"); + } + + qDebug() << "[Transport] Active transport set to" << activeName; } TransportsConfig GatewayController::buildTransportsConfig() { + using amnezia::transport::dns::DnsProtocol; + TransportsConfig config; - QString server(AGW_DNS_SERVER); - QString domain(AGW_DNS_DOMAIN); + QString server = QString(AGW_DNS_SERVER).trimmed(); + QString domain = QString(AGW_DNS_DOMAIN).trimmed(); if (server.isEmpty() || domain.isEmpty()) { qDebug() << "[Transport] DNS server/domain not configured, HTTP only"; return config; } - QString primaryStr = QString(AGW_DNS_PRIMARY).toLower(); + QString primaryStr = QString(AGW_DNS_PRIMARY).trimmed().toLower(); if (primaryStr == "udp" || primaryStr == "dns_udp") { config.primary = PrimaryTransport::DnsUdp; } else if (primaryStr == "tcp" || primaryStr == "dns_tcp") { @@ -219,35 +209,35 @@ TransportsConfig GatewayController::buildTransportsConfig() config.primary = PrimaryTransport::Http; } - int retryCount = QString(AGW_DNS_RETRY_COUNT).toInt(); + int retryCount = QString(AGW_DNS_RETRY_COUNT).trimmed().toInt(); config.retryCount = (retryCount > 0) ? retryCount : 3; - int timeoutMs = QString(AGW_DNS_TIMEOUT_MS).toInt(); + int timeoutMs = QString(AGW_DNS_TIMEOUT_MS).trimmed().toInt(); config.timeoutMs = (timeoutMs > 0) ? timeoutMs : 10000; config.httpEnabled = true; - auto addTransport = [&](NetworkUtilities::DnsTransport type, const char *portDefine, quint16 defaultPort, + auto addTransport = [&](DnsProtocol type, const char *portDefine, quint16 defaultPort, const QString &dohPath = QString()) { DnsTransportEntry entry; entry.type = type; entry.server = server; entry.domain = domain; - quint16 port = QString(portDefine).toUShort(); + quint16 port = QString(portDefine).trimmed().toUShort(); entry.port = (port > 0) ? port : defaultPort; if (!dohPath.isEmpty()) entry.dohPath = dohPath; config.dnsTransports.append(entry); }; - addTransport(NetworkUtilities::DnsTransport::Udp, AGW_DNS_PORT_UDP, 5353); - addTransport(NetworkUtilities::DnsTransport::Tcp, AGW_DNS_PORT_UDP, 5353); - addTransport(NetworkUtilities::DnsTransport::Tls, AGW_DNS_PORT_DOT, 853); + addTransport(DnsProtocol::Udp, AGW_DNS_PORT_UDP, 5353); + addTransport(DnsProtocol::Tcp, AGW_DNS_PORT_UDP, 5353); + addTransport(DnsProtocol::Tls, AGW_DNS_PORT_DOT, 853); - QString dohPath = QString(AGW_DNS_DOH_PATH); + QString dohPath = QString(AGW_DNS_DOH_PATH).trimmed(); if (dohPath.isEmpty()) dohPath = "/dns-query"; - addTransport(NetworkUtilities::DnsTransport::Https, AGW_DNS_PORT_DOH, 443, dohPath); + addTransport(DnsProtocol::Https, AGW_DNS_PORT_DOH, 443, dohPath); - addTransport(NetworkUtilities::DnsTransport::Quic, AGW_DNS_PORT_DOQ, 8853); + addTransport(DnsProtocol::Quic, AGW_DNS_PORT_DOQ, 8853); qDebug() << "[Transport] Built config from env: server=" << server << "domain=" << domain << "transports=" << config.dnsTransports.size() << "primary=" << static_cast(config.primary); @@ -255,988 +245,114 @@ TransportsConfig GatewayController::buildTransportsConfig() return config; } -ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +GatewayController::EncryptedRequest GatewayController::encryptRequest(const QJsonObject &apiPayload) { - 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); - } - - // Pre-resolve all DNS server hostnames to IPs (thread-safe: done before launching parallel threads) - QMap resolvedHosts; - for (const auto &t : m_transportsConfig.dnsTransports) { - if (!t.server.isEmpty() && !resolvedHosts.contains(t.server)) { - QHostAddress addr(t.server); - if (addr.isNull()) { - QHostInfo info = QHostInfo::fromName(t.server); - if (!info.addresses().isEmpty()) { - resolvedHosts[t.server] = info.addresses().first().toString(); - qDebug() << "[Transport] Resolved" << t.server << "->" << resolvedHosts[t.server]; - } else { - resolvedHosts[t.server] = t.server; - qDebug() << "[Transport] Failed to resolve" << t.server; - } - } else { - resolvedHosts[t.server] = t.server; - } - } - } - - // 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(); - int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - QString errorStr = reply->errorString(); - reply->deleteLater(); - - if (replyError != QNetworkReply::NoError || encryptedBody.isEmpty()) { - qDebug() << "[Transport] PRIMARY HTTP failed:" << replyError << errorStr - << "status:" << httpStatus << "body size:" << encryptedBody.size(); - 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; - } - - bool needsHostname = (transport.type == NetworkUtilities::DnsTransport::Https || - transport.type == NetworkUtilities::DnsTransport::Tls); - QString serverAddr = needsHostname ? transport.server : resolvedHosts.value(transport.server, transport.server); - qDebug() << "[Transport] PRIMARY: Trying DNS" << transportName; - QByteArray dnsResponse = NetworkUtilities::sendViaDnsTunnel( - encRequestData.requestBody, endpointName, transport.domain, - serverAddr, 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 threads; - - auto launchThread = [&](std::function func) { - QThread *thread = QThread::create(std::move(func)); - threads.append(thread); - thread->start(); - QThread::msleep(10); - }; - - // HTTP (if not primary and enabled) - if (m_transportsConfig.primary != PrimaryTransport::Http && m_transportsConfig.httpEnabled) { - launchThread([&]() { - if (gotSuccess.load()) return; - - qDebug() << "[Transport] FALLBACK: Trying HTTP"; - EncryptedRequestData httpRequestData = prepareRequest(endpoint, apiPayload, true); - 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 (...) {} - }); - } - - // 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; - - launchThread([&, 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; - } - - // TLS-based transports need original hostname for certificate validation - bool needsHostname = (transport.type == NetworkUtilities::DnsTransport::Https || - transport.type == NetworkUtilities::DnsTransport::Tls); - QString serverAddr = needsHostname ? transport.server : resolvedHosts.value(transport.server, transport.server); - qDebug() << "[Transport] FALLBACK: Trying DNS" << transportName; - QByteArray dnsResponse = NetworkUtilities::sendViaDnsTunnel( - encRequestData.requestBody, endpointName, transport.domain, - serverAddr, 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 (...) {} - }); - } - - for (auto *t : threads) { - t->wait(); - delete t; - } - - 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()) { - return QString(); - } - - QString transportName; - switch (m_dnsTransport) { - 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() << "[DNS] Resolving" << hostname << "via" << transportName << "server:" << m_dnsServer << "port:" << m_dnsPort; - - QString ip = NetworkUtilities::resolveDns(hostname, m_dnsServer, m_dnsTransport, m_dnsPort, 3000, m_dohEndpoint); - - if (!ip.isEmpty()) { - qDebug() << "[DNS] Resolved:" << hostname << "->" << ip << "via" << transportName; - } else { - qDebug() << "[DNS] Resolution failed for:" << hostname << "via" << transportName; - } - - return ip; -} - -ErrorCode GatewayController::postViaDns(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) -{ - if (m_dnsServer.isEmpty() || m_dnsBaseDomain.isEmpty()) { - qDebug() << "[DNS Tunnel] DNS server or base domain not set"; - return ErrorCode::AmneziaServiceConnectionFailed; - } - - // 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; - } - - // Extract endpoint name from full path (e.g., "/v1/config" -> "config") - QString endpointName = endpoint; - endpointName.remove("%1"); // Remove placeholder - if (endpointName.startsWith("v1/")) { - endpointName = endpointName.mid(3); // Remove "v1/" - } - if (endpointName.endsWith("/")) { - endpointName.chop(1); - } - - QString transportName; - switch (m_dnsTransport) { - 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() << "[DNS Tunnel] Sending request to endpoint:" << endpointName - << "via" << transportName << "payload size:" << encRequestData.requestBody.size(); - - // Send via DNS tunnel - QByteArray dnsResponse = NetworkUtilities::sendViaDnsTunnel( - encRequestData.requestBody, endpointName, m_dnsBaseDomain, - m_dnsServer, m_dnsTransport, m_dnsPort, m_requestTimeoutMsecs, m_dohEndpoint); - - if (dnsResponse.isEmpty()) { - qDebug() << "[DNS Tunnel] Empty response"; - return ErrorCode::AmneziaServiceConnectionFailed; - } - - qDebug() << "[DNS Tunnel] Received response:" << dnsResponse.size() << "bytes"; - - // Decrypt response - try { - QSimpleCrypto::QBlockCipher blockCipher; - responseBody = blockCipher.decryptAesBlockCipher(dnsResponse, encRequestData.key, encRequestData.iv, "", encRequestData.salt); - qDebug() << "[DNS Tunnel] Decrypted response:" << responseBody.left(200); - return ErrorCode::NoError; - } catch (...) { - qDebug() << "[DNS Tunnel] Failed to decrypt response, returning raw data"; - responseBody = dnsResponse; - return ErrorCode::NoError; - } -} - -GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload, bool skipDnsResolve) -{ - EncryptedRequestData encRequestData; - encRequestData.errorCode = ErrorCode::NoError; - -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs); - 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 tunneling — там запрос идёт напрямую к DNS серверу) - QString finalGatewayEndpoint = m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl; - QUrl gatewayUrl(finalGatewayEndpoint); - QString hostname = gatewayUrl.host(); - - // Проверяем, нужно ли резолвить (если это не IP адрес и не localhost) - if (!skipDnsResolve && - !hostname.isEmpty() && - hostname != "localhost" && - !NetworkUtilities::checkIPv4Format(hostname) && - QHostAddress(hostname).isNull()) { - - QString resolvedIp = resolveGatewayHostname(hostname); - if (!resolvedIp.isEmpty()) { - gatewayUrl.setHost(resolvedIp); - finalGatewayEndpoint = gatewayUrl.toString(); - qDebug() << "DNS resolved:" << hostname << "->" << resolvedIp; - } else { - // Fallback: используем оригинальный hostname - qWarning() << "DNS resolution failed for:" << hostname << ", using original hostname"; - } - } - - encRequestData.request.setUrl(endpoint.arg(finalGatewayEndpoint)); - - // bypass killSwitch exceptions for API-gateway -#ifdef AMNEZIA_DESKTOP - if (m_isStrictKillSwitchEnabled) { - QString host = QUrl(encRequestData.request.url()).host(); - QString ip = NetworkUtilities::getIPAddress(host); - if (!ip.isEmpty()) { - IpcClient::withInterface([&](QSharedPointer iface) { - QRemoteObjectPendingReply reply = iface->addKillSwitchAllowedRange(QStringList { ip }); - if (!reply.waitForFinished(1000) || !reply.returnValue()) - qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call"; - }); - } - } -#endif + EncryptedRequest result; + result.errorCode = amnezia::ErrorCode::NoError; QSimpleCrypto::QBlockCipher blockCipher; - encRequestData.key = blockCipher.generatePrivateSalt(32); - encRequestData.iv = blockCipher.generatePrivateSalt(32); - encRequestData.salt = blockCipher.generatePrivateSalt(8); + result.key = blockCipher.generatePrivateSalt(32); + result.iv = blockCipher.generatePrivateSalt(16); + result.salt = blockCipher.generatePrivateSalt(8); QJsonObject keyPayload; - keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64()); - keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64()); - keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64()); + keyPayload[configKey::aesKey] = QString(result.key.toBase64()); + keyPayload[configKey::aesIv] = QString(result.iv.toBase64()); + keyPayload[configKey::aesSalt] = QString(result.salt.toBase64()); QByteArray encryptedKeyPayload; QByteArray encryptedApiPayload; try { QSimpleCrypto::QRsa rsa; - EVP_PKEY *publicKey = nullptr; try { QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + rsaKey = rsaKey.trimmed(); rsaKey.replace("\\n", "\n"); - QSimpleCrypto::QRsa rsa; publicKey = rsa.getPublicKeyFromByteArray(rsaKey); } catch (...) { Utils::logException(); qCritical() << "error loading public key from environment variables"; - encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey; - return encRequestData; + result.errorCode = amnezia::ErrorCode::ApiMissingAgwPublicKey; + return result; } - encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); + encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(QJsonDocument::Compact), + publicKey, RSA_PKCS1_PADDING); EVP_PKEY_free(publicKey); - encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, - "", encRequestData.salt); + encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(QJsonDocument::Compact), + result.key, result.iv, "", result.salt); } catch (...) { Utils::logException(); qCritical() << "error when encrypting the request body"; - encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError; - return encRequestData; + result.errorCode = amnezia::ErrorCode::ApiConfigDecryptionError; + return result; } QJsonObject requestBody; requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - encRequestData.requestBody = QJsonDocument(requestBody).toJson(); - return encRequestData; + result.body = QJsonDocument(requestBody).toJson(QJsonDocument::Compact); + return result; } -GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody, - QNetworkReply::NetworkError replyError, const QByteArray &key, - const QByteArray &iv, const QByteArray &salt) +amnezia::transport::DecryptionResult GatewayController::decryptResponse(const QByteArray &encryptedResponseBody, + const QByteArray &key, + const QByteArray &iv, + const QByteArray &salt) const { - DecryptionResult result; - result.decryptedBody = encryptedResponseBody; - result.isDecryptionSuccessful = false; + amnezia::transport::DecryptionResult result; + result.decrypted = encryptedResponseBody; + result.isOk = false; + + if (encryptedResponseBody.isEmpty()) { + return result; + } try { QSimpleCrypto::QBlockCipher blockCipher; - result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); - result.isDecryptionSuccessful = true; + result.decrypted = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); + result.isOk = true; } catch (...) { - result.decryptedBody = encryptedResponseBody; - result.isDecryptionSuccessful = false; + result.decrypted = encryptedResponseBody; + result.isOk = false; } return result; } -ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +std::shared_ptr GatewayController::currentTransport() const { - EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); - if (encRequestData.errorCode != ErrorCode::NoError) { - return encRequestData.errorCode; - } - - QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); - - QEventLoop wait; - connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - - QList sslErrors; - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); - - QByteArray encryptedResponseBody = reply->readAll(); - QString replyErrorString = reply->errorString(); - auto replyError = reply->error(); - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - reply->deleteLater(); - - auto decryptionResult = - tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); - - if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) { - auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) { - encRequestData.request.setUrl(url); - return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); - }; - - auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData, - &decryptionResult, this](QNetworkReply *reply, const QList &nestedSslErrors) { - encryptedResponseBody = reply->readAll(); - replyErrorString = reply->errorString(); - replyError = reply->error(); - httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - decryptionResult = - tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); - - if (!sslErrors.isEmpty() - || shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) { - sslErrors = nestedSslErrors; - return false; - } - return true; - }; - - auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); - auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); - bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction); - } - - auto errorCode = - apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody); - if (errorCode) { - return errorCode; - } - - if (!decryptionResult.isDecryptionSuccessful) { - qCritical() << "error when decrypting the request body"; - return ErrorCode::ApiConfigDecryptionError; - } - - responseBody = decryptionResult.decryptedBody; - return ErrorCode::NoError; + QMutexLocker lock(&m_transportMutex); + return m_transport; } -QFuture> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) +amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) { - auto promise = QSharedPointer>>::create(); - promise->start(); - - EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); - if (encRequestData.errorCode != ErrorCode::NoError) { - promise->addResult(qMakePair(encRequestData.errorCode, QByteArray())); - promise->finish(); - return promise->future(); + EncryptedRequest enc = encryptRequest(apiPayload); + if (enc.errorCode != amnezia::ErrorCode::NoError) { + return enc.errorCode; } - QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); - - auto sslErrors = QSharedPointer>::create(); - - connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { *sslErrors = errors; }); - - connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable { - QByteArray encryptedResponseBody = reply->readAll(); - QString replyErrorString = reply->errorString(); - auto replyError = reply->error(); - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - reply->deleteLater(); - - auto decryptionResult = - tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); - - auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult, - const QList &sslErrors, QNetworkReply::NetworkError replyError, - const QString &replyErrorString, int httpStatusCode) { - auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, - decryptionResult.decryptedBody); - if (errorCode) { - promise->addResult(qMakePair(errorCode, QByteArray())); - promise->finish(); - return; - } - - if (!decryptionResult.isDecryptionSuccessful) { - Utils::logException(); - qCritical() << "error when decrypting the request body"; - promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); - promise->finish(); - return; - } - - promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody)); - promise->finish(); - }; - - if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) { - auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); - auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); - - QStringList baseUrls; - if (m_isDevEnvironment) { - baseUrls = QString(DEV_S3_ENDPOINT).split(", "); - } else { - baseUrls = QString(PROD_S3_ENDPOINT).split(", "); - } - - QStringList proxyStorageUrls; - if (!serviceType.isEmpty()) { - for (const auto &baseUrl : baseUrls) { - QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); - proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) - + ".json"); - } - } - for (const auto &baseUrl : baseUrls) - proxyStorageUrls.push_back(baseUrl + "endpoints.json"); - - getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { - getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) { - bypassProxyAsync(endpoint, proxyUrl, encRequestData, - [processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful, - const QList &sslErrors, QNetworkReply::NetworkError replyError, - const QString &replyErrorString, int httpStatusCode) { - GatewayController::DecryptionResult result; - result.decryptedBody = decryptedBody; - result.isDecryptionSuccessful = isDecryptionSuccessful; - processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode); - }); - }); - }); - - } else { - processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode); - } - }); - - return promise->future(); -} - -QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) -{ - QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QEventLoop wait; - QList sslErrors; - QNetworkReply *reply; - - QStringList baseUrls; - if (m_isDevEnvironment) { - baseUrls = QString(DEV_S3_ENDPOINT).split(", "); - } else { - baseUrls = QString(PROD_S3_ENDPOINT).split(", "); + auto transport = currentTransport(); + if (!transport) { + return amnezia::ErrorCode::AmneziaServiceConnectionFailed; } - QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; - - QStringList proxyStorageUrls; - if (!serviceType.isEmpty()) { - for (const auto &baseUrl : baseUrls) { - QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); - proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); - } - } - for (const auto &baseUrl : baseUrls) { - proxyStorageUrls.push_back(baseUrl + "endpoints.json"); - } - - for (const auto &proxyStorageUrl : proxyStorageUrls) { - request.setUrl(proxyStorageUrl); - reply = amnApp->networkManager()->get(request); - - connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); - - if (reply->error() == QNetworkReply::NetworkError::NoError) { - auto encryptedResponseBody = reply->readAll(); - reply->deleteLater(); - - EVP_PKEY *privateKey = nullptr; - QByteArray responseBody; - try { - if (!m_isDevEnvironment) { - QCryptographicHash hash(QCryptographicHash::Sha512); - hash.addData(key); - QByteArray hashResult = hash.result().toHex(); - - QByteArray key = QByteArray::fromHex(hashResult.left(64)); - QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); - - QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); - - QSimpleCrypto::QBlockCipher blockCipher; - responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); - } else { - responseBody = encryptedResponseBody; - } - } catch (...) { - Utils::logException(); - qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; - continue; - } - - auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); - - QStringList endpoints; - for (const auto &endpoint : endpointsArray) { - endpoints.push_back(endpoint.toString()); - } - return endpoints; - } else { - auto replyError = reply->error(); - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qDebug() << replyError; - qDebug() << httpStatusCode; - qDebug() << "go to the next storage endpoint"; - - reply->deleteLater(); - } - } - return {}; -} - -bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, - bool isDecryptionSuccessful) -{ - const QByteArray &responseBody = decryptedResponseBody; - - int httpStatus = -1; - if (isDecryptionSuccessful) { - QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody); - if (jsonDoc.isObject()) { - QJsonObject jsonObj = jsonDoc.object(); - httpStatus = jsonObj.value("http_status").toInt(-1); - } - } else { - qDebug() << "failed to decrypt the data"; - return true; - } - - if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) { - qDebug() << "timeout occurred"; - qDebug() << replyError; - return true; - } else if (responseBody.contains("html")) { - qDebug() << "the response contains an html tag"; - return true; - } else if (httpStatus == httpStatusCodeNotFound) { - if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) - || responseBody.contains(errorResponsePattern3)) { - return false; - } else { - qDebug() << replyError; - return true; - } - } else if (httpStatus == httpStatusCodeNotImplemented) { - if (responseBody.contains(updateRequestResponsePattern)) { - return false; - } else { - qDebug() << replyError; - return true; - } - } else if (httpStatus == httpStatusCodeConflict) { - return false; - } else if (replyError != QNetworkReply::NetworkError::NoError) { - qDebug() << replyError; - return true; - } - return false; -} - -void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode, - std::function requestFunction, - std::function &sslErrors)> replyProcessingFunction) -{ - QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode); - std::random_device randomDevice; - std::mt19937 generator(randomDevice()); - std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); - - QByteArray responseBody; - - auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, - std::function requestFunction, - std::function &sslErrors)> replyProcessingFunction) { - QEventLoop wait; - QList sslErrors; - - qDebug() << "go to the next proxy endpoint"; - QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl)); - - QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); - - auto result = replyProcessingFunction(reply, sslErrors); - reply->deleteLater(); - return result; + auto decryptionHook = [this, key = enc.key, iv = enc.iv, salt = enc.salt](const QByteArray &encrypted) { + return decryptResponse(encrypted, key, iv, salt); }; - if (m_proxyUrl.isEmpty()) { - QNetworkRequest request; - request.setTransferTimeout(1000); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QEventLoop wait; - QList sslErrors; - QNetworkReply *reply; - - for (const QString &proxyUrl : proxyUrls) { - request.setUrl(proxyUrl + "lmbd-health"); - reply = amnApp->networkManager()->get(request); - - connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); - - if (reply->error() == QNetworkReply::NetworkError::NoError) { - reply->deleteLater(); - - m_proxyUrl = proxyUrl; - if (!m_proxyUrl.isEmpty()) { - break; - } - } else { - reply->deleteLater(); - } - } - } - - if (!m_proxyUrl.isEmpty()) { - if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) { - return; - } - } - - for (const QString &proxyUrl : proxyUrls) { - if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) { - m_proxyUrl = proxyUrl; - break; - } - } + return transport->send(endpoint, enc.body, responseBody, decryptionHook); } -void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, - std::function onComplete) +QFuture> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) { - if (currentProxyStorageIndex >= proxyStorageUrls.size()) { - onComplete({}); - return; - } - - QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setUrl(proxyStorageUrls[currentProxyStorageIndex]); - - QNetworkReply *reply = amnApp->networkManager()->get(request); - - // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList &e) { *(state->sslErrors) = e; }); - - connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() { - if (reply->error() == QNetworkReply::NoError) { - QByteArray encrypted = reply->readAll(); - reply->deleteLater(); - - QByteArray responseBody; - try { - QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; - if (!m_isDevEnvironment) { - QCryptographicHash hash(QCryptographicHash::Sha512); - hash.addData(key); - QByteArray h = hash.result().toHex(); - - QByteArray decKey = QByteArray::fromHex(h.left(64)); - QByteArray iv = QByteArray::fromHex(h.mid(64, 32)); - QByteArray ba = QByteArray::fromBase64(encrypted); - - QSimpleCrypto::QBlockCipher cipher; - responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv); - } else { - responseBody = encrypted; - } - } catch (...) { - Utils::logException(); - qCritical() << "error decrypting payload"; - QMetaObject::invokeMethod( - this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); - return; - } - - QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array(); - QStringList endpoints; - for (const QJsonValue &endpoint : endpointsArray) - endpoints.push_back(endpoint.toString()); - - QStringList shuffled = endpoints; - std::random_device randomDevice; - std::mt19937 generator(randomDevice()); - std::shuffle(shuffled.begin(), shuffled.end(), generator); - - onComplete(shuffled); - return; - } - - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qDebug() << httpStatusCode; - qDebug() << "go to the next storage endpoint"; - reply->deleteLater(); - QMetaObject::invokeMethod( - this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); - }); -} - -void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, - std::function onComplete) -{ - if (currentProxyIndex >= proxyUrls.size()) { - onComplete(""); - return; - } - - QNetworkRequest request; - request.setTransferTimeout(1000); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health"); - - QNetworkReply *reply = amnApp->networkManager()->get(request); - - // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList &e) { - // *(state->sslErrors) = e; - // }); - - connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() { - reply->deleteLater(); - - if (reply->error() == QNetworkReply::NoError) { - m_proxyUrl = proxyUrls[currentProxyIndex]; - onComplete(m_proxyUrl); - return; - } - - qDebug() << "go to the next proxy endpoint"; - QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection); - }); -} - -void GatewayController::bypassProxyAsync( - const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, - std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete) -{ - auto sslErrors = QSharedPointer>::create(); - if (proxyUrl.isEmpty()) { - onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0); - return; - } - - QNetworkRequest request = encRequestData.request; - request.setUrl(endpoint.arg(proxyUrl)); - - QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody); - - connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList &errors) { *sslErrors = errors; }); - - connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() { - QByteArray encryptedResponseBody = reply->readAll(); - QString replyErrorString = reply->errorString(); - auto replyError = reply->error(); - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - reply->deleteLater(); - - auto decryptionResult = - tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); - - onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString, - httpStatusCode); + return QtConcurrent::run([this, endpoint, apiPayload]() { + QByteArray responseBody; + amnezia::ErrorCode errorCode = post(endpoint, apiPayload, responseBody); + return qMakePair(errorCode, responseBody); }); } diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index 528c4811c..fc8b6bc77 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -4,42 +4,37 @@ #include #include #include -#include +#include #include #include -#include -#include +#include #include "core/defs.h" -#include "core/networkUtilities.h" +#include "core/transport/dns/dnsResolver.h" +#include "core/transport/igatewaytransport.h" -#ifdef Q_OS_IOS - #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 +struct DnsTransportEntry +{ + amnezia::transport::dns::DnsProtocol type = amnezia::transport::dns::DnsProtocol::Udp; + QString server; + QString domain; 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 { +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); }; @@ -49,76 +44,45 @@ class GatewayController : public QObject Q_OBJECT public: - explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, - const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); + explicit GatewayController(const QString &gatewayEndpoint, + const bool isDevEnvironment, + const int requestTimeoutMsecs, + const bool isStrictKillSwitchEnabled, + QObject *parent = nullptr); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); QFuture> postAsync(const QString &endpoint, const QJsonObject apiPayload); - - // DNS transport settings - void setDnsServer(const QString &dnsServer, const QString &baseDomain, NetworkUtilities::DnsTransport transport, - quint16 port, const QString &dohEndpoint = "/dns-query"); - - // DNS tunneling - send request via DNS transport - amnezia::ErrorCode postViaDns(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); - + static TransportsConfig buildTransportsConfig(); 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 + struct EncryptedRequest { - QNetworkRequest request; - QByteArray requestBody; + QByteArray body; QByteArray key; QByteArray iv; QByteArray salt; - amnezia::ErrorCode errorCode; + amnezia::ErrorCode errorCode = amnezia::ErrorCode::NoError; }; - struct DecryptionResult - { - QByteArray decryptedBody; - bool isDecryptionSuccessful; - }; + EncryptedRequest encryptRequest(const QJsonObject &apiPayload); + amnezia::transport::DecryptionResult decryptResponse(const QByteArray &encryptedResponseBody, + const QByteArray &key, + const QByteArray &iv, + const QByteArray &salt) const; - 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); - - QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode); - bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful); - void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode, - std::function requestFunction, - std::function &sslErrors)> replyProcessingFunction); - - void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, - std::function onComplete); - void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function onComplete); - void bypassProxyAsync( - const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, - std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete); + std::shared_ptr currentTransport() const; + static std::shared_ptr buildTransport( + const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled); int m_requestTimeoutMsecs; QString m_gatewayEndpoint; bool m_isDevEnvironment = false; bool m_isStrictKillSwitchEnabled = false; - - // 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; + mutable QMutex m_transportMutex; + std::shared_ptr m_transport; }; #endif // GATEWAYCONTROLLER_H diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index 75ea096d9..b054ed032 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -1,9 +1,6 @@ #include "networkUtilities.h" #include #include -#include -#include -#include #ifdef Q_OS_WIN #include @@ -47,197 +44,7 @@ #include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include - -namespace -{ - QHostAddress resolveHostAddress(const QString &host) { - QHostAddress addr(host); - if (!addr.isNull()) return addr; - QHostInfo info = QHostInfo::fromName(host); - if (!info.addresses().isEmpty()) return info.addresses().first(); - return QHostAddress(); - } - - constexpr quint16 DNS_PORT = 53; - constexpr quint16 DNS_TYPE_A = 1; // A record - constexpr quint16 DNS_CLASS_IN = 1; // Internet class - - // DNS Header structure (RFC 1035) - struct DnsHeader { - quint16 id; // Transaction ID - quint16 flags; // Flags - quint16 qdcount; // Question count - quint16 ancount; // Answer count - quint16 nscount; // Authority count - quint16 arcount; // Additional count - }; - - // Helper function to encode hostname to QNAME format - QByteArray encodeDnsName(const QString &hostname) - { - QByteArray result; - QStringList parts = hostname.split('.'); - - for (const QString &part : parts) { - if (part.length() > 63) { - return QByteArray(); // Invalid - } - result.append(static_cast(part.length())); - result.append(part.toUtf8()); - } - result.append(static_cast(0)); // Null terminator - - return result; - } - - // Helper function to build DNS query packet - QByteArray buildDnsQuery(const QString &hostname, quint16 transactionId) - { - QByteArray packet; - - // DNS Header - DnsHeader header; - header.id = qToBigEndian(transactionId); - header.flags = qToBigEndian(0x0100); // Standard query, recursion desired - header.qdcount = qToBigEndian(1); // One question - header.ancount = 0; - header.nscount = 0; - header.arcount = 0; - - packet.append(reinterpret_cast(&header), sizeof(DnsHeader)); - - // Question section - QByteArray qname = encodeDnsName(hostname); - if (qname.isEmpty()) { - return QByteArray(); - } - packet.append(qname); - - // QTYPE (A record) - quint16 qtype = qToBigEndian(DNS_TYPE_A); - packet.append(reinterpret_cast(&qtype), sizeof(quint16)); - - // QCLASS (IN) - quint16 qclass = qToBigEndian(DNS_CLASS_IN); - packet.append(reinterpret_cast(&qclass), sizeof(quint16)); - - return packet; - } - - // Helper function to parse DNS response and extract IP address - QString parseDnsResponse(const QByteArray &response, bool isTcp) - { - if (response.size() < static_cast(sizeof(DnsHeader))) { - return QString(); - } - - // Skip length prefix for TCP - int offset = isTcp ? 2 : 0; - if (response.size() < offset + static_cast(sizeof(DnsHeader))) { - return QString(); - } - - // Parse header - DnsHeader header; - memcpy(&header, response.constData() + offset, sizeof(DnsHeader)); - offset += sizeof(DnsHeader); - - quint16 flags = qFromBigEndian(header.flags); - quint16 ancount = qFromBigEndian(header.ancount); - - // Check if response is valid (QR bit set, no error) - if ((flags & 0x8000) == 0 || (flags & 0x000F) != 0) { - return QString(); // Not a response or has error - } - - if (ancount == 0) { - return QString(); // No answers - } - - // Skip question section - // Find end of QNAME (null terminator) - while (offset < response.size() && response.at(offset) != 0) { - quint8 length = static_cast(response.at(offset)); - if (length > 63) { - return QString(); // Invalid - } - offset += length + 1; - } - if (offset >= response.size()) { - return QString(); - } - offset++; // Skip null terminator - - // Skip QTYPE and QCLASS (4 bytes) - offset += 4; - - // Parse answer section - for (int i = 0; i < ancount && offset < response.size(); i++) { - // Skip NAME (can be pointer or label) - if (offset >= response.size()) { - break; - } - - quint8 nameByte = static_cast(response.at(offset)); - if ((nameByte & 0xC0) == 0xC0) { - // Pointer (compressed name) - offset += 2; - } else { - // Label sequence - while (offset < response.size() && response.at(offset) != 0) { - quint8 length = static_cast(response.at(offset)); - if (length > 63) { - return QString(); - } - offset += length + 1; - } - offset++; // Skip null terminator - } - - // Read TYPE, CLASS, TTL - if (offset + 10 > response.size()) { - break; - } - - quint16 type = qFromBigEndian(*reinterpret_cast(response.constData() + offset)); - offset += 2; - offset += 2; // Skip CLASS - offset += 4; // Skip TTL - - // Read RDLENGTH and RDATA - quint16 rdlength = qFromBigEndian(*reinterpret_cast(response.constData() + offset)); - offset += 2; - - if (type == DNS_TYPE_A && rdlength == 4) { - // A record with IPv4 address - if (offset + 4 > response.size()) { - break; - } - - QHostAddress ip; - ip.setAddress(qFromBigEndian(*reinterpret_cast(response.constData() + offset))); - return ip.toString(); - } - - offset += rdlength; // Skip RDATA - } - - return QString(); - } -} QRegularExpression NetworkUtilities::ipAddressRegExp() { @@ -691,1255 +498,3 @@ QPair NetworkUtilities::getGatewayAndIface() return { gateway, QNetworkInterface::interfaceFromIndex(index) }; #endif } - -QString NetworkUtilities::resolveDns(const QString &hostname, const QString &dnsServer, DnsTransport transport, - quint16 port, int timeoutMsecs, const QString &dohEndpoint) -{ - switch (transport) { - case DnsTransport::Udp: - return resolveDnsOverUdp(hostname, dnsServer, port, timeoutMsecs); - case DnsTransport::Tcp: - return resolveDnsOverTcp(hostname, dnsServer, port, timeoutMsecs); - case DnsTransport::Tls: - return resolveDnsOverTls(hostname, dnsServer, port, timeoutMsecs); - case DnsTransport::Https: - return resolveDnsOverHttps(hostname, dnsServer, dohEndpoint, timeoutMsecs); - case DnsTransport::Quic: - return resolveDnsOverQuic(hostname, dnsServer, port, timeoutMsecs); - } - return QString(); -} - -QString NetworkUtilities::resolveDnsOverUdp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - QUdpSocket socket; - - // Генерируем случайный transaction ID - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - - // Формируем DNS запрос - QByteArray query = buildDnsQuery(hostname, transactionId); - if (query.isEmpty()) { - return QString(); - } - - // Отправляем запрос - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - return QString(); - } - - qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port); - if (bytesWritten != query.size()) { - return QString(); - } - - // Ждем ответ с таймаутом - 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, &QUdpSocket::readyRead, [&]() { - while (socket.hasPendingDatagrams()) { - QNetworkDatagram datagram = socket.receiveDatagram(); - if (datagram.isValid()) { - response = datagram.data(); - responseReceived = true; - loop.quit(); - } - } - }); - - timer.start(); - loop.exec(); - timer.stop(); - - if (!responseReceived || response.isEmpty()) { - return QString(); - } - - // Парсим ответ - return parseDnsResponse(response, false); -} - -QString NetworkUtilities::resolveDnsOverTcp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - QTcpSocket socket; - - // Подключаемся к DNS серверу - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - return QString(); - } - - socket.connectToHost(dnsAddress, port); - - if (!socket.waitForConnected(timeoutMsecs)) { - return QString(); - } - - // Генерируем случайный transaction ID - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - - // Формируем DNS запрос - QByteArray query = buildDnsQuery(hostname, transactionId); - if (query.isEmpty()) { - socket.close(); - return QString(); - } - - // Для TCP добавляем 2-байтовое поле длины перед пакетом - quint16 length = qToBigEndian(static_cast(query.size())); - QByteArray tcpQuery; - tcpQuery.append(reinterpret_cast(&length), sizeof(quint16)); - tcpQuery.append(query); - - // Отправляем запрос - qint64 bytesWritten = socket.write(tcpQuery); - if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { - socket.close(); - return QString(); - } - - // Ждем ответ с таймаутом - 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, &QTcpSocket::readyRead, [&]() { - // Читаем длину ответа (первые 2 байта) - 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(); - } - } - } - }); - - timer.start(); - loop.exec(); - timer.stop(); - - socket.close(); - - if (!responseReceived || response.isEmpty()) { - return QString(); - } - - // Парсим ответ (для TCP уже без префикса длины) - return parseDnsResponse(response, true); -} - -QString NetworkUtilities::resolveDnsOverTls(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - QSslSocket socket; - - // Подключаемся к DNS серверу через TLS - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - return QString(); - } - - socket.setPeerVerifyMode(QSslSocket::QueryPeer); - - socket.connectToHostEncrypted(dnsAddress.toString(), port); - - if (!socket.waitForConnected(timeoutMsecs)) { - return QString(); - } - - // Ждем завершения TLS handshake - if (!socket.waitForEncrypted(timeoutMsecs)) { - socket.close(); - return QString(); - } - - // Генерируем случайный transaction ID - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - - // Формируем DNS запрос - QByteArray query = buildDnsQuery(hostname, transactionId); - if (query.isEmpty()) { - socket.close(); - return QString(); - } - - // Для TLS (как и для TCP) добавляем 2-байтовое поле длины перед пакетом - quint16 length = qToBigEndian(static_cast(query.size())); - QByteArray tlsQuery; - tlsQuery.append(reinterpret_cast(&length), sizeof(quint16)); - tlsQuery.append(query); - - // Отправляем запрос - qint64 bytesWritten = socket.write(tlsQuery); - if (bytesWritten != tlsQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { - socket.close(); - return QString(); - } - - // Ждем ответ с таймаутом - 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, [&]() { - // Читаем длину ответа (первые 2 байта) - 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(); - } - } - } - }); - - timer.start(); - loop.exec(); - timer.stop(); - - socket.close(); - - if (!responseReceived || response.isEmpty()) { - return QString(); - } - - // Парсим ответ (для TLS тот же формат что и для TCP) - return parseDnsResponse(response, true); -} - -QString NetworkUtilities::resolveDnsOverHttps(const QString &hostname, const QString &dnsServer, const QString &endpoint, int timeoutMsecs) -{ - // DNS over HTTPS использует указанный endpoint - QString dohUrl = QString("https://%1%2").arg(dnsServer, endpoint); - - // Формируем DNS запрос - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - QByteArray query = buildDnsQuery(hostname, transactionId); - if (query.isEmpty()) { - return QString(); - } - - // Создаем HTTP запрос - QNetworkRequest request; - request.setUrl(QUrl(dohUrl)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/dns-message"); - request.setRawHeader("Accept", "application/dns-message"); - request.setTransferTimeout(timeoutMsecs); - - // Отправляем POST запрос - QNetworkAccessManager nam; - QNetworkReply *reply = nam.post(request, query); - - // Ждем ответ с таймаутом - 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(reply, &QNetworkReply::finished, [&]() { - if (reply->error() == QNetworkReply::NoError) { - response = reply->readAll(); - responseReceived = true; - } - loop.quit(); - }); - - timer.start(); - loop.exec(); - timer.stop(); - - reply->deleteLater(); - - if (!responseReceived || response.isEmpty()) { - return QString(); - } - - // Парсим DNS ответ (DoH возвращает чистый DNS пакет без префикса) - return parseDnsResponse(response, false); -} - -QString NetworkUtilities::resolveDnsOverQuic(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - // DNS over QUIC использует QUIC протокол (UDP-based с TLS) - // QUIC требует специальной библиотеки (quiche, msquic и т.д.) - // Qt не имеет встроенной поддержки QUIC - // Для упрощения используем QUdpSocket с TLS поверх UDP - // В реальной реализации нужна библиотека QUIC - - QUdpSocket socket; - - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - return QString(); - } - - // Формируем DNS запрос - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - QByteArray query = buildDnsQuery(hostname, transactionId); - if (query.isEmpty()) { - return QString(); - } - - // Для QUIC нужен специальный формат с QUIC заголовками - // Упрощенная версия: отправляем DNS пакет через UDP - // В реальной реализации нужно: - // 1. Установить QUIC соединение (Initial packet, Handshake) - // 2. Отправить DNS запрос в QUIC stream - // 3. Получить ответ из QUIC stream - - // Временная реализация: используем UDP как fallback - // TODO: Реализовать полноценный QUIC протокол с использованием библиотеки - - qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port); - if (bytesWritten != query.size()) { - return QString(); - } - - // Ждем ответ с таймаутом - 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, &QUdpSocket::readyRead, [&]() { - while (socket.hasPendingDatagrams()) { - QNetworkDatagram datagram = socket.receiveDatagram(); - if (datagram.isValid()) { - response = datagram.data(); - responseReceived = true; - loop.quit(); - } - } - }); - - timer.start(); - loop.exec(); - timer.stop(); - - if (!responseReceived || response.isEmpty()) { - return QString(); - } - - // Парсим DNS ответ - return parseDnsResponse(response, false); -} - -// ============== DNS Tunneling ============== - -namespace { - // EDNS0 option codes (same as backend) - constexpr quint16 EDNS0_PAYLOAD_OPTION_CODE = 65001; // Payload in request - constexpr quint16 EDNS0_CHUNK_REQUEST_CODE = 65002; // Client requests chunk - constexpr quint16 EDNS0_CHUNK_RESPONSE_CODE = 65003; // Server responds with chunk meta - - // Chunk metadata from server response (24 bytes) - struct ChunkMeta { - QByteArray chunkId; // 16 bytes - quint16 totalChunks; // big endian - quint16 chunkIndex; // big endian - quint32 totalSize; // big endian - }; - - // Helper to append big-endian uint16 - void appendUint16BE(QByteArray &data, quint16 value) { - data.append(static_cast((value >> 8) & 0xFF)); - data.append(static_cast(value & 0xFF)); - } - - // Build DNS TXT query for requesting a specific chunk (no payload, just chunkId + index) - QByteArray buildDnsChunkRequest(const QString &queryName, quint16 transactionId, - const QByteArray &chunkId, quint16 chunkIndex) - { - QByteArray query; - - // DNS Header (12 bytes) - appendUint16BE(query, transactionId); - appendUint16BE(query, 0x0100); // Flags: standard query, RD=1 - appendUint16BE(query, 1); // Questions: 1 - appendUint16BE(query, 0); // Answers: 0 - appendUint16BE(query, 0); // Authority: 0 - appendUint16BE(query, 1); // Additional: 1 (EDNS0 OPT) - - // Question section - QNAME - QStringList labels = queryName.split('.'); - for (const QString &label : labels) { - QByteArray labelBytes = label.toUtf8(); - query.append(static_cast(labelBytes.size())); - query.append(labelBytes); - } - query.append(static_cast(0)); // Root label - appendUint16BE(query, 16); // QTYPE: TXT - appendUint16BE(query, 1); // QCLASS: IN - - // Additional section - EDNS0 OPT record with chunk request - // Format: chunkId(16) + chunkIndex(2) = 18 bytes - quint16 optionDataLen = 4 + 18; // code(2) + length(2) + data(18) - - query.append(static_cast(0)); // Name: root - appendUint16BE(query, 41); // TYPE: OPT - appendUint16BE(query, 4096); // CLASS: UDP payload size - query.append(static_cast(0)); // Extended RCODE - query.append(static_cast(0)); // EDNS version - appendUint16BE(query, 0); // Flags - appendUint16BE(query, optionDataLen); // RDLENGTH - - // RDATA: EDNS0 chunk request option - appendUint16BE(query, EDNS0_CHUNK_REQUEST_CODE); - appendUint16BE(query, 18); // Option length - query.append(chunkId.left(16).leftJustified(16, '\0')); // chunkId (16 bytes) - appendUint16BE(query, chunkIndex); // chunkIndex - - return query; - } - - // Parse EDNS0 chunk metadata from DNS response - ChunkMeta parseChunkMeta(const QByteArray &response) - { - ChunkMeta meta; - meta.totalChunks = 0; - meta.chunkIndex = 0; - meta.totalSize = 0; - - if (response.size() < 12) return meta; - - const quint8 *data = reinterpret_cast(response.constData()); - - // Parse header - quint16 qdCount = (data[4] << 8) | data[5]; - quint16 anCount = (data[6] << 8) | data[7]; - quint16 nsCount = (data[8] << 8) | data[9]; - quint16 arCount = (data[10] << 8) | data[11]; - - int pos = 12; - - // 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++) { - 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++) { - 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; - } - - // Parse additional (looking for OPT record) - for (int i = 0; i < arCount && pos < response.size(); i++) { - // Skip name - if (pos < response.size() && data[pos] == 0) { - pos++; // Root label for OPT - } else { - if (!skipDnsName()) return meta; - } - - 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 - int optEnd = pos + rdlen; - while (pos + 4 <= optEnd) { - quint16 optCode = (data[pos] << 8) | data[pos + 1]; - quint16 optLen = (data[pos + 2] << 8) | data[pos + 3]; - pos += 4; - - if (optCode == EDNS0_CHUNK_RESPONSE_CODE && optLen >= 24) { - // Parse chunk metadata: chunkId(16) + total(2) + index(2) + size(4) - meta.chunkId = QByteArray(reinterpret_cast(data + pos), 16); - meta.totalChunks = (data[pos + 16] << 8) | data[pos + 17]; - meta.chunkIndex = (data[pos + 18] << 8) | data[pos + 19]; - meta.totalSize = (data[pos + 20] << 24) | (data[pos + 21] << 16) | - (data[pos + 22] << 8) | data[pos + 23]; - qDebug() << "[DNS Tunnel] Chunk meta: id=" << meta.chunkId.toHex() - << "total=" << meta.totalChunks << "index=" << meta.chunkIndex - << "size=" << meta.totalSize; - return meta; - } - pos += optLen; - } - } else { - pos += rdlen; - } - } - - return meta; - } - - // Build DNS TXT query with EDNS0 payload (initial request) - QByteArray buildDnsTxtQueryWithPayload(const QString &queryName, quint16 transactionId, const QByteArray &payload) - { - QByteArray query; - - // DNS Header (12 bytes) - appendUint16BE(query, transactionId); // Transaction ID - appendUint16BE(query, 0x0100); // Flags: standard query, RD=1 - appendUint16BE(query, 1); // Questions: 1 - appendUint16BE(query, 0); // Answers: 0 - appendUint16BE(query, 0); // Authority: 0 - appendUint16BE(query, 1); // Additional: 1 (EDNS0 OPT) - - // Question section - QNAME - QStringList labels = queryName.split('.'); - for (const QString &label : labels) { - QByteArray labelBytes = label.toUtf8(); - query.append(static_cast(labelBytes.size())); - query.append(labelBytes); - } - query.append(static_cast(0)); // Root label (end of QNAME) - appendUint16BE(query, 16); // QTYPE: TXT (16) - appendUint16BE(query, 1); // QCLASS: IN (1) - - // Additional section - EDNS0 OPT record - QByteArray payloadBase64 = payload.toBase64(); - quint16 optionDataLen = 4 + payloadBase64.size(); // code(2) + length(2) + data - - query.append(static_cast(0)); // Name: root (0) - appendUint16BE(query, 41); // TYPE: OPT (41) - appendUint16BE(query, 4096); // CLASS: UDP payload size - query.append(static_cast(0)); // Extended RCODE - query.append(static_cast(0)); // EDNS version - appendUint16BE(query, 0); // Flags (Z) - appendUint16BE(query, optionDataLen); // RDLENGTH - - // RDATA: EDNS0 payload option - appendUint16BE(query, EDNS0_PAYLOAD_OPTION_CODE); // Option code - appendUint16BE(query, payloadBase64.size()); // Option length - query.append(payloadBase64); // Option data - - return query; - } - - // Parse DNS TXT response and extract payload - QByteArray parseDnsTxtResponse(const QByteArray &response) - { - if (response.size() < 12) { - qDebug() << "[DNS Tunnel] Response too short:" << response.size(); - return QByteArray(); - } - - // Parse header manually to avoid QDataStream issues - const uchar *data = reinterpret_cast(response.constData()); - int pos = 0; - - quint16 transactionId = (data[pos] << 8) | data[pos+1]; pos += 2; - quint16 flags = (data[pos] << 8) | data[pos+1]; pos += 2; - quint16 qdCount = (data[pos] << 8) | data[pos+1]; pos += 2; - quint16 anCount = (data[pos] << 8) | data[pos+1]; pos += 2; - quint16 nsCount = (data[pos] << 8) | data[pos+1]; pos += 2; - quint16 arCount = (data[pos] << 8) | data[pos+1]; pos += 2; - - 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++) { - 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 - } - - qDebug() << "[DNS Tunnel] After questions, pos=" << pos << "size=" << response.size(); - - // Read answer section - looking for TXT records - QByteArray combinedTxt; - for (int i = 0; i < anCount && pos < response.size(); i++) { - if (!skipDnsName()) { - qDebug() << "[DNS Tunnel] Failed to skip answer name at pos=" << pos; - break; - } - - if (pos + 10 > response.size()) { - qDebug() << "[DNS Tunnel] Not enough data for RR header at pos=" << pos; - break; - } - - quint16 rtype = (data[pos] << 8) | data[pos+1]; pos += 2; - quint16 rclass = (data[pos] << 8) | data[pos+1]; pos += 2; - quint32 ttl = (data[pos] << 24) | (data[pos+1] << 16) | (data[pos+2] << 8) | data[pos+3]; pos += 4; - quint16 rdlength = (data[pos] << 8) | data[pos+1]; pos += 2; - - 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 && pos + txtLen <= response.size()) { - combinedTxt.append(reinterpret_cast(data + pos), txtLen); - pos += txtLen; - } else { - break; // Invalid TXT length - } - } - } else { - pos += rdlength; // Skip non-TXT record data - } - } - - if (combinedTxt.isEmpty()) { - qDebug() << "[DNS Tunnel] No TXT records in response"; - return QByteArray(); - } - - qDebug() << "[DNS Tunnel] Received TXT data:" << combinedTxt.size() << "bytes"; - - // Decode base64 - QByteArray decoded = QByteArray::fromBase64(combinedTxt); - qDebug() << "[DNS Tunnel] Decoded data:" << decoded.size() << "bytes, preview:" << decoded.left(100); - return decoded; - } -} - -QByteArray NetworkUtilities::sendViaDnsTunnel(const QByteArray &payload, const QString &endpoint, const QString &baseDomain, - const QString &dnsServer, DnsTransport transport, quint16 port, - int timeoutMsecs, const QString &dohEndpoint) -{ - // Build query name: endpoint.baseDomain (e.g., services.gateway.example.com) - QString queryName = QString("%1.%2").arg(endpoint, baseDomain); - - qDebug() << "[DNS Tunnel] Sending to" << queryName << "via" << (transport == DnsTransport::Udp ? "UDP" : - transport == DnsTransport::Tcp ? "TCP" : - transport == DnsTransport::Tls ? "DoT" : - transport == DnsTransport::Https ? "DoH" : "DoQ") - << "server:" << dnsServer << "port:" << port << "payload:" << payload.size() << "bytes"; - - switch (transport) { - case DnsTransport::Udp: - // Try chunked UDP first (handles large responses) - return sendViaDnsTunnelUdpChunked(payload, queryName, dnsServer, port, timeoutMsecs); - case DnsTransport::Tcp: - return sendViaDnsTunnelTcp(payload, queryName, dnsServer, port, timeoutMsecs); - case DnsTransport::Tls: - return sendViaDnsTunnelTls(payload, queryName, dnsServer, port, timeoutMsecs); - case DnsTransport::Https: - return sendViaDnsTunnelHttps(payload, queryName, dnsServer, port, dohEndpoint, timeoutMsecs); - case DnsTransport::Quic: - // 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(); -} - -QByteArray NetworkUtilities::sendViaDnsTunnelUdp(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - QUdpSocket socket; - - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); - - if (query.isEmpty()) { - qDebug() << "[DNS Tunnel UDP] Failed to build query"; - return QByteArray(); - } - - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - qDebug() << "[DNS Tunnel UDP] Invalid DNS server address:" << dnsServer; - return QByteArray(); - } - - qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port); - if (bytesWritten != query.size()) { - qDebug() << "[DNS Tunnel UDP] Failed to send query"; - return QByteArray(); - } - - qDebug() << "[DNS Tunnel UDP] Sent" << bytesWritten << "bytes to" << dnsServer << ":" << port; - - QElapsedTimer timer; - timer.start(); - - while (timer.elapsed() < timeoutMsecs) { - if (socket.waitForReadyRead(qMax(1, timeoutMsecs - static_cast(timer.elapsed())))) { - while (socket.hasPendingDatagrams()) { - QNetworkDatagram datagram = socket.receiveDatagram(); - if (datagram.isValid()) { - qDebug() << "[DNS Tunnel UDP] Received response:" << datagram.data().size() << "bytes"; - return parseDnsTxtResponse(datagram.data()); - } - } - } - } - - qDebug() << "[DNS Tunnel UDP] No response received (timeout)"; - return QByteArray(); -} - -QByteArray NetworkUtilities::sendViaDnsTunnelTcp(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - QTcpSocket socket; - - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - qDebug() << "[DNS Tunnel TCP] Invalid DNS server address:" << dnsServer; - return QByteArray(); - } - - socket.connectToHost(dnsAddress, port); - if (!socket.waitForConnected(timeoutMsecs)) { - qDebug() << "[DNS Tunnel TCP] Connection failed:" << socket.errorString(); - return QByteArray(); - } - - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); - - if (query.isEmpty()) { - qDebug() << "[DNS Tunnel TCP] Failed to build query"; - socket.close(); - return QByteArray(); - } - - // TCP DNS: 2-byte length prefix - quint16 length = qToBigEndian(static_cast(query.size())); - QByteArray tcpQuery; - tcpQuery.append(reinterpret_cast(&length), sizeof(quint16)); - tcpQuery.append(query); - - qint64 bytesWritten = socket.write(tcpQuery); - if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { - qDebug() << "[DNS Tunnel TCP] Failed to send query"; - socket.close(); - return QByteArray(); - } - - qDebug() << "[DNS Tunnel TCP] Sent" << bytesWritten << "bytes to" << dnsServer << ":" << port; - - // Synchronous read: first get 2-byte length prefix - QElapsedTimer timer; - timer.start(); - - while (socket.bytesAvailable() < 2) { - int remaining = timeoutMsecs - timer.elapsed(); - if (remaining <= 0 || !socket.waitForReadyRead(remaining)) { - qDebug() << "[DNS Tunnel TCP] Timeout waiting for response length"; - socket.close(); - return QByteArray(); - } - } - - QByteArray lengthBytes = socket.read(2); - if (lengthBytes.size() != 2) { - qDebug() << "[DNS Tunnel TCP] Failed to read response length"; - socket.close(); - return QByteArray(); - } - - quint16 responseLength = qFromBigEndian(*reinterpret_cast(lengthBytes.constData())); - - QByteArray response; - while (response.size() < responseLength) { - int remaining = timeoutMsecs - timer.elapsed(); - if (remaining <= 0) { - qDebug() << "[DNS Tunnel TCP] 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 TCP] Timeout waiting for more data"; - socket.close(); - return QByteArray(); - } - } - - socket.close(); - - qDebug() << "[DNS Tunnel TCP] Received response:" << response.size() << "bytes"; - return parseDnsTxtResponse(response); -} - -QByteArray NetworkUtilities::sendViaDnsTunnelTls(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - QSslSocket socket; - socket.setPeerVerifyMode(QSslSocket::VerifyPeer); - - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - qDebug() << "[DNS Tunnel DoT] Invalid DNS server address:" << dnsServer; - return QByteArray(); - } - - socket.connectToHostEncrypted(dnsServer, port); - if (!socket.waitForEncrypted(timeoutMsecs)) { - qDebug() << "[DNS Tunnel DoT] TLS handshake failed:" << socket.errorString(); - return QByteArray(); - } - - qDebug() << "[DNS Tunnel DoT] TLS connected to" << dnsServer << ":" << port; - - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); - - if (query.isEmpty()) { - qDebug() << "[DNS Tunnel DoT] Failed to build query"; - socket.close(); - return QByteArray(); - } - - // TCP DNS format: 2-byte length prefix - quint16 length = qToBigEndian(static_cast(query.size())); - QByteArray tcpQuery; - tcpQuery.append(reinterpret_cast(&length), sizeof(quint16)); - tcpQuery.append(query); - - qint64 bytesWritten = socket.write(tcpQuery); - if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { - qDebug() << "[DNS Tunnel DoT] Failed to send query"; - socket.close(); - return QByteArray(); - } - - qDebug() << "[DNS Tunnel DoT] Sent" << bytesWritten << "bytes"; - - // Synchronous read: first get 2-byte length prefix - QElapsedTimer timer; - timer.start(); - - 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); -} - -QByteArray NetworkUtilities::sendViaDnsTunnelHttps(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, const QString &endpoint, int timeoutMsecs) -{ - // Build DNS query packet - quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); - QByteArray dnsQuery = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); - - if (dnsQuery.isEmpty()) { - qDebug() << "[DNS Tunnel DoH] Failed to build query"; - return QByteArray(); - } - - // DoH uses HTTP POST with application/dns-message - // 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; - - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/dns-message"); - request.setRawHeader("Accept", "application/dns-message"); - request.setTransferTimeout(timeoutMsecs); - - QNetworkAccessManager manager; - QNetworkReply *reply = manager.post(request, dnsQuery); - - // Synchronous wait using QEventLoop (safe since each thread gets its own) - QEventLoop loop; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - - QTimer::singleShot(timeoutMsecs, &loop, &QEventLoop::quit); - loop.exec(); - - if (!reply->isFinished()) { - qDebug() << "[DNS Tunnel DoH] Timeout"; - reply->abort(); - reply->deleteLater(); - return QByteArray(); - } - - if (reply->error() != QNetworkReply::NoError) { - qDebug() << "[DNS Tunnel DoH] HTTP error:" << reply->errorString(); - reply->deleteLater(); - return QByteArray(); - } - - QByteArray response = reply->readAll(); - reply->deleteLater(); - - if (response.isEmpty()) { - qDebug() << "[DNS Tunnel DoH] Empty response"; - return QByteArray(); - } - - qDebug() << "[DNS Tunnel DoH] Received response:" << response.size() << "bytes"; - return parseDnsTxtResponse(response); -} - -QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs) -{ - qDebug() << "[DNS Tunnel UDP Chunked] Starting request to" << queryName; - - QHostAddress dnsAddress = resolveHostAddress(dnsServer); - if (dnsAddress.isNull()) { - qDebug() << "[DNS Tunnel UDP Chunked] Invalid DNS server:" << dnsServer; - return 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; - qint64 written = socket.writeDatagram(query, dnsAddress, port); - if (written != query.size()) { - return QByteArray(); - } - - QElapsedTimer timer; - timer.start(); - - while (timer.elapsed() < requestTimeoutMs) { - if (socket.waitForReadyRead(qMax(1, requestTimeoutMs - static_cast(timer.elapsed())))) { - while (socket.hasPendingDatagrams()) { - QNetworkDatagram datagram = socket.receiveDatagram(); - if (datagram.isValid()) { - return datagram.data(); - } - } - } - } - - return QByteArray(); - }; - - // 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); - - if (initialQuery.isEmpty()) { - qDebug() << "[DNS Tunnel UDP Chunked] Failed to build initial query"; - return QByteArray(); - } - - qDebug() << "[DNS Tunnel UDP Chunked] Sending initial request, payload:" << payload.size() << "bytes"; - QByteArray firstResponse = sendWithRetry(initialQuery, MAX_INITIAL_RETRIES); - - if (firstResponse.isEmpty()) { - qDebug() << "[DNS Tunnel UDP Chunked] No response for initial request after" << MAX_INITIAL_RETRIES << "attempts"; - return QByteArray(); - } - - // Parse chunk metadata from EDNS0 option 65003 - ChunkMeta meta = parseChunkMeta(firstResponse); - QByteArray firstTxtData = parseDnsTxtResponse(firstResponse); - - if (firstTxtData.isEmpty()) { - qDebug() << "[DNS Tunnel UDP Chunked] Failed to parse TXT response"; - return QByteArray(); - } - - // Check if response is chunked - if (meta.totalChunks <= 1) { - qDebug() << "[DNS Tunnel UDP Chunked] Single chunk response:" << firstTxtData.size() << "bytes"; - return firstTxtData; - } - - qDebug() << "[DNS Tunnel UDP Chunked] Chunked response: total=" << meta.totalChunks - << "size=" << meta.totalSize << "chunkId=" << meta.chunkId.toHex(); - - // === Step 2: Collect all chunks === - QMap chunks; - chunks[0] = firstTxtData; - - // Build list of chunks to request - QList chunksToRequest; - for (int i = 1; i < meta.totalChunks; i++) { - chunksToRequest.append(i); - } - - // === Step 3: Request chunks in parallel batches with retry === - auto requestChunksBatch = [&](const QList &chunkIndices, int batchTimeout) { - if (chunkIndices.isEmpty()) return; - - // Create sockets and send requests for this batch - QList> sockets; - QMap socketToIndex; - - for (int idx : chunkIndices) { - if (chunks.contains(idx)) continue; - - 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); - } - - if (sockets.isEmpty()) return; - - qDebug() << "[DNS Tunnel UDP Chunked] Sent" << sockets.size() << "parallel requests"; - - // Poll all sockets synchronously until timeout - QElapsedTimer deadline; - deadline.start(); - int receivedCount = 0; - int expectedCount = sockets.size(); - - while (deadline.elapsed() < batchTimeout && receivedCount < expectedCount && chunks.size() < meta.totalChunks) { - for (auto &socket : sockets) { - if (socket->waitForReadyRead(50)) { - while (socket->hasPendingDatagrams()) { - QNetworkDatagram datagram = socket->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(socket.data(), -1); - if (idx >= 0 && !chunks.contains(idx)) { - chunks[idx] = chunkTxtData; - qDebug() << "[DNS Tunnel UDP Chunked] Received chunk" << idx << ":" << chunkTxtData.size() << "bytes"; - receivedCount++; - } - } - } - } - } - } - } - }; - - // 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); - } - } - - // === Step 4: Verify all chunks received === - QList finalMissing; - for (int i = 0; i < meta.totalChunks; i++) { - if (!chunks.contains(i)) { - finalMissing.append(i); - } - } - - 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/core/networkUtilities.h b/client/core/networkUtilities.h index a074732e2..102097568 100644 --- a/client/core/networkUtilities.h +++ b/client/core/networkUtilities.h @@ -31,34 +31,6 @@ public: static QString netMaskFromIpWithSubnet(const QString ip); static QString ipAddressFromIpWithSubnet(const QString ip); static QStringList summarizeRoutes(const QStringList &ips, const QString cidr); - - // DNS resolution methods - enum class DnsTransport { Udp, Tcp, Tls, Https, Quic }; - - static QString resolveDns(const QString &hostname, const QString &dnsServer, DnsTransport transport, - quint16 port, int timeoutMsecs = 3000, const QString &dohEndpoint = "/dns-query"); - static QString resolveDnsOverUdp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); - static QString resolveDnsOverTcp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); - static QString resolveDnsOverTls(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); - static QString resolveDnsOverHttps(const QString &hostname, const QString &dnsServer, const QString &endpoint, int timeoutMsecs = 3000); - static QString resolveDnsOverQuic(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); - - // DNS tunneling - send/receive data via DNS TXT records - static QByteArray sendViaDnsTunnel(const QByteArray &payload, const QString &endpoint, const QString &baseDomain, - const QString &dnsServer, DnsTransport transport, quint16 port, - int timeoutMsecs = 30000, const QString &dohEndpoint = "/dns-query"); - static QByteArray sendViaDnsTunnelUdp(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs); - static QByteArray sendViaDnsTunnelTcp(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs); - static QByteArray sendViaDnsTunnelTls(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs); - static QByteArray sendViaDnsTunnelHttps(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, const QString &endpoint, int timeoutMsecs); - - // Chunked UDP - for large responses - static QByteArray sendViaDnsTunnelUdpChunked(const QByteArray &payload, const QString &queryName, - const QString &dnsServer, quint16 port, int timeoutMsecs); }; #endif // NETWORKUTILITIES_H diff --git a/client/core/transport/dns/dnsPacket.cpp b/client/core/transport/dns/dnsPacket.cpp new file mode 100644 index 000000000..5a452884d --- /dev/null +++ b/client/core/transport/dns/dnsPacket.cpp @@ -0,0 +1,153 @@ +#include "dnsPacket_p.h" + +#include +#include + +namespace amnezia::transport::dns::detail +{ + +QHostAddress resolveHostAddress(const QString &host) +{ + QHostAddress addr(host); + if (!addr.isNull()) return addr; + QHostInfo info = QHostInfo::fromName(host); + if (!info.addresses().isEmpty()) return info.addresses().first(); + return QHostAddress(); +} + +QByteArray encodeDnsName(const QString &hostname) +{ + QByteArray result; + const QStringList parts = hostname.split('.'); + + for (const QString &part : parts) { + if (part.length() > 63) { + return QByteArray(); + } + result.append(static_cast(part.length())); + result.append(part.toUtf8()); + } + result.append(static_cast(0)); + return result; +} + +QByteArray buildDnsQuery(const QString &hostname, quint16 transactionId) +{ + QByteArray packet; + + DnsHeader header; + header.id = qToBigEndian(transactionId); + header.flags = qToBigEndian(0x0100); + header.qdcount = qToBigEndian(1); + header.ancount = 0; + header.nscount = 0; + header.arcount = 0; + + packet.append(reinterpret_cast(&header), sizeof(DnsHeader)); + + const QByteArray qname = encodeDnsName(hostname); + if (qname.isEmpty()) { + return QByteArray(); + } + packet.append(qname); + + quint16 qtype = qToBigEndian(DNS_TYPE_A); + packet.append(reinterpret_cast(&qtype), sizeof(quint16)); + + quint16 qclass = qToBigEndian(DNS_CLASS_IN); + packet.append(reinterpret_cast(&qclass), sizeof(quint16)); + + return packet; +} + +QString parseDnsResponse(const QByteArray &response, bool isTcp) +{ + if (response.size() < static_cast(sizeof(DnsHeader))) { + return QString(); + } + + int offset = isTcp ? 2 : 0; + if (response.size() < offset + static_cast(sizeof(DnsHeader))) { + return QString(); + } + + DnsHeader header; + std::memcpy(&header, response.constData() + offset, sizeof(DnsHeader)); + offset += sizeof(DnsHeader); + + const quint16 flags = qFromBigEndian(header.flags); + const quint16 ancount = qFromBigEndian(header.ancount); + + if ((flags & 0x8000) == 0 || (flags & 0x000F) != 0) { + return QString(); + } + + if (ancount == 0) { + return QString(); + } + + while (offset < response.size() && response.at(offset) != 0) { + const quint8 length = static_cast(response.at(offset)); + if (length > 63) { + return QString(); + } + offset += length + 1; + } + if (offset >= response.size()) { + return QString(); + } + offset++; + + offset += 4; + + for (int i = 0; i < ancount && offset < response.size(); ++i) { + if (offset >= response.size()) { + break; + } + + const quint8 nameByte = static_cast(response.at(offset)); + if ((nameByte & 0xC0) == 0xC0) { + offset += 2; + } else { + while (offset < response.size() && response.at(offset) != 0) { + const quint8 length = static_cast(response.at(offset)); + if (length > 63) { + return QString(); + } + offset += length + 1; + } + offset++; + } + + if (offset + 10 > response.size()) { + break; + } + + const quint16 type = + qFromBigEndian(*reinterpret_cast(response.constData() + offset)); + offset += 2; + offset += 2; + offset += 4; + + const quint16 rdlength = + qFromBigEndian(*reinterpret_cast(response.constData() + offset)); + offset += 2; + + if (type == DNS_TYPE_A && rdlength == 4) { + if (offset + 4 > response.size()) { + break; + } + + QHostAddress ip; + ip.setAddress( + qFromBigEndian(*reinterpret_cast(response.constData() + offset))); + return ip.toString(); + } + + offset += rdlength; + } + + return QString(); +} + +} // namespace amnezia::transport::dns::detail diff --git a/client/core/transport/dns/dnsPacket_p.h b/client/core/transport/dns/dnsPacket_p.h new file mode 100644 index 000000000..da181e80e --- /dev/null +++ b/client/core/transport/dns/dnsPacket_p.h @@ -0,0 +1,38 @@ +#ifndef DNSPACKET_P_H +#define DNSPACKET_P_H + +#include +#include +#include +#include + +namespace amnezia::transport::dns::detail +{ + +constexpr quint16 DNS_PORT = 53; +constexpr quint16 DNS_TYPE_A = 1; +constexpr quint16 DNS_CLASS_IN = 1; + +#pragma pack(push, 1) +struct DnsHeader +{ + quint16 id; + quint16 flags; + quint16 qdcount; + quint16 ancount; + quint16 nscount; + quint16 arcount; +}; +#pragma pack(pop) + +QHostAddress resolveHostAddress(const QString &host); + +QByteArray encodeDnsName(const QString &hostname); + +QByteArray buildDnsQuery(const QString &hostname, quint16 transactionId); + +QString parseDnsResponse(const QByteArray &response, bool isTcp); + +} // namespace amnezia::transport::dns::detail + +#endif // DNSPACKET_P_H diff --git a/client/core/transport/dns/dnsResolver.cpp b/client/core/transport/dns/dnsResolver.cpp new file mode 100644 index 000000000..46a4850b0 --- /dev/null +++ b/client/core/transport/dns/dnsResolver.cpp @@ -0,0 +1,354 @@ +#include "dnsResolver.h" + +#include "dnsPacket_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace amnezia::transport::dns::DnsResolver +{ + +using detail::buildDnsQuery; +using detail::parseDnsResponse; +using detail::resolveHostAddress; + +QString resolve(const QString &hostname, + const QString &dnsServer, + DnsProtocol protocol, + quint16 port, + int timeoutMsecs, + const QString &dohEndpoint) +{ + switch (protocol) { + case DnsProtocol::Udp: + return resolveOverUdp(hostname, dnsServer, port, timeoutMsecs); + case DnsProtocol::Tcp: + return resolveOverTcp(hostname, dnsServer, port, timeoutMsecs); + case DnsProtocol::Tls: + return resolveOverTls(hostname, dnsServer, port, timeoutMsecs); + case DnsProtocol::Https: + return resolveOverHttps(hostname, dnsServer, dohEndpoint, timeoutMsecs); + case DnsProtocol::Quic: + return resolveOverQuic(hostname, dnsServer, port, timeoutMsecs); + } + return QString(); +} + +QString resolveOverUdp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + QUdpSocket socket; + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsQuery(hostname, transactionId); + if (query.isEmpty()) { + return QString(); + } + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + return QString(); + } + + const qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port); + if (bytesWritten != query.size()) { + return QString(); + } + + 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, &QUdpSocket::readyRead, [&]() { + while (socket.hasPendingDatagrams()) { + QNetworkDatagram datagram = socket.receiveDatagram(); + if (datagram.isValid()) { + response = datagram.data(); + responseReceived = true; + loop.quit(); + } + } + }); + + timer.start(); + loop.exec(); + timer.stop(); + + if (!responseReceived || response.isEmpty()) { + return QString(); + } + + return parseDnsResponse(response, false); +} + +QString resolveOverTcp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + QTcpSocket socket; + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + return QString(); + } + + socket.connectToHost(dnsAddress, port); + if (!socket.waitForConnected(timeoutMsecs)) { + return QString(); + } + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsQuery(hostname, transactionId); + if (query.isEmpty()) { + socket.close(); + return QString(); + } + + quint16 length = qToBigEndian(static_cast(query.size())); + QByteArray tcpQuery; + tcpQuery.append(reinterpret_cast(&length), sizeof(quint16)); + tcpQuery.append(query); + + const qint64 bytesWritten = socket.write(tcpQuery); + if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { + socket.close(); + return QString(); + } + + 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, &QTcpSocket::readyRead, [&]() { + if (socket.bytesAvailable() >= 2 && response.isEmpty()) { + QByteArray lengthBytes = socket.read(2); + if (lengthBytes.size() == 2) { + const 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(); + } + } + } + }); + + timer.start(); + loop.exec(); + timer.stop(); + + socket.close(); + + if (!responseReceived || response.isEmpty()) { + return QString(); + } + + return parseDnsResponse(response, true); +} + +QString resolveOverTls(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + QSslSocket socket; + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + return QString(); + } + + socket.setPeerVerifyMode(QSslSocket::QueryPeer); + socket.connectToHostEncrypted(dnsAddress.toString(), port); + + if (!socket.waitForConnected(timeoutMsecs)) { + return QString(); + } + + if (!socket.waitForEncrypted(timeoutMsecs)) { + socket.close(); + return QString(); + } + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsQuery(hostname, transactionId); + if (query.isEmpty()) { + socket.close(); + return QString(); + } + + quint16 length = qToBigEndian(static_cast(query.size())); + QByteArray tlsQuery; + tlsQuery.append(reinterpret_cast(&length), sizeof(quint16)); + tlsQuery.append(query); + + const qint64 bytesWritten = socket.write(tlsQuery); + if (bytesWritten != tlsQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { + socket.close(); + return QString(); + } + + 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) { + const 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(); + } + } + } + }); + + timer.start(); + loop.exec(); + timer.stop(); + + socket.close(); + + if (!responseReceived || response.isEmpty()) { + return QString(); + } + + return parseDnsResponse(response, true); +} + +QString resolveOverHttps(const QString &hostname, const QString &dnsServer, const QString &endpoint, int timeoutMsecs) +{ + const QString dohUrl = QStringLiteral("https://%1%2").arg(dnsServer, endpoint); + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsQuery(hostname, transactionId); + if (query.isEmpty()) { + return QString(); + } + + QNetworkRequest request; + request.setUrl(QUrl(dohUrl)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/dns-message"); + request.setRawHeader("Accept", "application/dns-message"); + request.setTransferTimeout(timeoutMsecs); + + QNetworkAccessManager nam; + QNetworkReply *reply = nam.post(request, query); + + 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(reply, &QNetworkReply::finished, [&]() { + if (reply->error() == QNetworkReply::NoError) { + response = reply->readAll(); + responseReceived = true; + } + loop.quit(); + }); + + timer.start(); + loop.exec(); + timer.stop(); + + reply->deleteLater(); + + if (!responseReceived || response.isEmpty()) { + return QString(); + } + + return parseDnsResponse(response, false); +} + +QString resolveOverQuic(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + // QUIC требует специальной библиотеки — пока используем UDP fallback + QUdpSocket socket; + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + return QString(); + } + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsQuery(hostname, transactionId); + if (query.isEmpty()) { + return QString(); + } + + const qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port); + if (bytesWritten != query.size()) { + return QString(); + } + + 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, &QUdpSocket::readyRead, [&]() { + while (socket.hasPendingDatagrams()) { + QNetworkDatagram datagram = socket.receiveDatagram(); + if (datagram.isValid()) { + response = datagram.data(); + responseReceived = true; + loop.quit(); + } + } + }); + + timer.start(); + loop.exec(); + timer.stop(); + + if (!responseReceived || response.isEmpty()) { + return QString(); + } + + return parseDnsResponse(response, false); +} + +} // namespace amnezia::transport::dns::DnsResolver diff --git a/client/core/transport/dns/dnsResolver.h b/client/core/transport/dns/dnsResolver.h new file mode 100644 index 000000000..121f83513 --- /dev/null +++ b/client/core/transport/dns/dnsResolver.h @@ -0,0 +1,29 @@ +#ifndef DNSRESOLVER_H +#define DNSRESOLVER_H + +#include + +namespace amnezia::transport::dns +{ + +enum class DnsProtocol { Udp, Tcp, Tls, Https, Quic }; + +namespace DnsResolver +{ + QString resolve(const QString &hostname, + const QString &dnsServer, + DnsProtocol protocol, + quint16 port, + int timeoutMsecs = 3000, + const QString &dohEndpoint = QStringLiteral("/dns-query")); + + QString resolveOverUdp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); + QString resolveOverTcp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); + QString resolveOverTls(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); + QString resolveOverHttps(const QString &hostname, const QString &dnsServer, const QString &endpoint, int timeoutMsecs = 3000); + QString resolveOverQuic(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000); +} // namespace DnsResolver + +} // namespace amnezia::transport::dns + +#endif // DNSRESOLVER_H diff --git a/client/core/transport/dns/dnsTunnel.cpp b/client/core/transport/dns/dnsTunnel.cpp new file mode 100644 index 000000000..b7580fa45 --- /dev/null +++ b/client/core/transport/dns/dnsTunnel.cpp @@ -0,0 +1,822 @@ +#include "dnsTunnel.h" + +#include "dnsPacket_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace amnezia::transport::dns::DnsTunnel +{ + +using detail::resolveHostAddress; + +namespace +{ + constexpr quint16 EDNS0_PAYLOAD_OPTION_CODE = 65001; + constexpr quint16 EDNS0_CHUNK_REQUEST_CODE = 65002; + constexpr quint16 EDNS0_CHUNK_RESPONSE_CODE = 65003; + + struct ChunkMeta + { + QByteArray chunkId; + quint16 totalChunks = 0; + quint16 chunkIndex = 0; + quint32 totalSize = 0; + }; + + void appendUint16BE(QByteArray &data, quint16 value) + { + data.append(static_cast((value >> 8) & 0xFF)); + data.append(static_cast(value & 0xFF)); + } + + QByteArray buildDnsChunkRequest(const QString &queryName, quint16 transactionId, + const QByteArray &chunkId, quint16 chunkIndex) + { + QByteArray query; + + appendUint16BE(query, transactionId); + appendUint16BE(query, 0x0100); + appendUint16BE(query, 1); + appendUint16BE(query, 0); + appendUint16BE(query, 0); + appendUint16BE(query, 1); + + const QStringList labels = queryName.split('.'); + for (const QString &label : labels) { + QByteArray labelBytes = label.toUtf8(); + query.append(static_cast(labelBytes.size())); + query.append(labelBytes); + } + query.append(static_cast(0)); + appendUint16BE(query, 16); + appendUint16BE(query, 1); + + const quint16 optionDataLen = 4 + 18; + + query.append(static_cast(0)); + appendUint16BE(query, 41); + appendUint16BE(query, 4096); + query.append(static_cast(0)); + query.append(static_cast(0)); + appendUint16BE(query, 0); + appendUint16BE(query, optionDataLen); + + appendUint16BE(query, EDNS0_CHUNK_REQUEST_CODE); + appendUint16BE(query, 18); + query.append(chunkId.left(16).leftJustified(16, '\0')); + appendUint16BE(query, chunkIndex); + + return query; + } + + ChunkMeta parseChunkMeta(const QByteArray &response) + { + ChunkMeta meta; + + if (response.size() < 12) return meta; + + const quint8 *data = reinterpret_cast(response.constData()); + + const quint16 qdCount = (data[4] << 8) | data[5]; + const quint16 anCount = (data[6] << 8) | data[7]; + const quint16 nsCount = (data[8] << 8) | data[9]; + const quint16 arCount = (data[10] << 8) | data[11]; + + int pos = 12; + + auto skipDnsName = [&]() -> bool { + int maxLabels = 128; + while (pos < response.size() && data[pos] != 0 && maxLabels-- > 0) { + if ((data[pos] & 0xC0) == 0xC0) { + pos += 2; + return pos <= response.size(); + } + const 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(); + }; + + for (int i = 0; i < qdCount && pos < response.size(); ++i) { + if (!skipDnsName()) return meta; + if (pos + 4 > response.size()) return meta; + pos += 4; + } + + for (int i = 0; i < anCount && pos < response.size(); ++i) { + if (!skipDnsName()) return meta; + if (pos + 10 > response.size()) return meta; + const quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9]; + if (pos + 10 + rdlen > response.size()) return meta; + pos += 10 + rdlen; + } + + for (int i = 0; i < nsCount && pos < response.size(); ++i) { + if (!skipDnsName()) return meta; + if (pos + 10 > response.size()) return meta; + const quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9]; + if (pos + 10 + rdlen > response.size()) return meta; + pos += 10 + rdlen; + } + + for (int i = 0; i < arCount && pos < response.size(); ++i) { + if (pos < response.size() && data[pos] == 0) { + pos++; + } else { + if (!skipDnsName()) return meta; + } + + if (pos + 10 > response.size()) return meta; + + const quint16 rtype = (data[pos] << 8) | data[pos + 1]; + const quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9]; + if (pos + 10 + rdlen > response.size()) return meta; + pos += 10; + + if (rtype == 41 && rdlen > 0) { + const int optEnd = pos + rdlen; + while (pos + 4 <= optEnd) { + const quint16 optCode = (data[pos] << 8) | data[pos + 1]; + const quint16 optLen = (data[pos + 2] << 8) | data[pos + 3]; + pos += 4; + + if (optCode == EDNS0_CHUNK_RESPONSE_CODE && optLen >= 24) { + meta.chunkId = QByteArray(reinterpret_cast(data + pos), 16); + meta.totalChunks = (data[pos + 16] << 8) | data[pos + 17]; + meta.chunkIndex = (data[pos + 18] << 8) | data[pos + 19]; + meta.totalSize = (static_cast(data[pos + 20]) << 24) + | (static_cast(data[pos + 21]) << 16) + | (static_cast(data[pos + 22]) << 8) | data[pos + 23]; + return meta; + } + pos += optLen; + } + } else { + pos += rdlen; + } + } + + return meta; + } + + QByteArray buildDnsTxtQueryWithPayload(const QString &queryName, quint16 transactionId, const QByteArray &payload) + { + QByteArray query; + + appendUint16BE(query, transactionId); + appendUint16BE(query, 0x0100); + appendUint16BE(query, 1); + appendUint16BE(query, 0); + appendUint16BE(query, 0); + appendUint16BE(query, 1); + + const QStringList labels = queryName.split('.'); + for (const QString &label : labels) { + QByteArray labelBytes = label.toUtf8(); + query.append(static_cast(labelBytes.size())); + query.append(labelBytes); + } + query.append(static_cast(0)); + appendUint16BE(query, 16); + appendUint16BE(query, 1); + + const QByteArray payloadBase64 = payload.toBase64(); + const quint16 optionDataLen = 4 + payloadBase64.size(); + + query.append(static_cast(0)); + appendUint16BE(query, 41); + appendUint16BE(query, 4096); + query.append(static_cast(0)); + query.append(static_cast(0)); + appendUint16BE(query, 0); + appendUint16BE(query, optionDataLen); + + appendUint16BE(query, EDNS0_PAYLOAD_OPTION_CODE); + appendUint16BE(query, payloadBase64.size()); + query.append(payloadBase64); + + return query; + } + + QByteArray parseDnsTxtResponse(const QByteArray &response) + { + if (response.size() < 12) { + return QByteArray(); + } + + const uchar *data = reinterpret_cast(response.constData()); + int pos = 0; + + pos += 2; + const quint16 flags = (data[pos] << 8) | data[pos + 1]; pos += 2; + const quint16 qdCount = (data[pos] << 8) | data[pos + 1]; pos += 2; + const quint16 anCount = (data[pos] << 8) | data[pos + 1]; pos += 2; + pos += 2; + pos += 2; + + if ((flags & 0x8000) == 0) { + return QByteArray(); + } + + if (anCount > 100 || qdCount > 10) { + return QByteArray(); + } + + auto skipDnsName = [&]() -> bool { + int maxLabels = 128; + while (pos < response.size() && data[pos] != 0 && maxLabels-- > 0) { + if ((data[pos] & 0xC0) == 0xC0) { + pos += 2; + return pos <= response.size(); + } + const 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(); + }; + + for (int i = 0; i < qdCount && pos < response.size(); ++i) { + if (!skipDnsName()) { + return QByteArray(); + } + if (pos + 4 > response.size()) return QByteArray(); + pos += 4; + } + + QByteArray combinedTxt; + for (int i = 0; i < anCount && pos < response.size(); ++i) { + if (!skipDnsName()) { + break; + } + + if (pos + 10 > response.size()) { + break; + } + + const quint16 rtype = (data[pos] << 8) | data[pos + 1]; pos += 2; + pos += 2; // class + pos += 4; // ttl + const quint16 rdlength = (data[pos] << 8) | data[pos + 1]; pos += 2; + + if (pos + rdlength > response.size()) { + break; + } + + if (rtype == 16) { + const int rdEnd = pos + rdlength; + while (pos < rdEnd && pos < response.size()) { + const quint8 txtLen = data[pos++]; + if (txtLen > 0 && pos + txtLen <= rdEnd && pos + txtLen <= response.size()) { + combinedTxt.append(reinterpret_cast(data + pos), txtLen); + pos += txtLen; + } else { + break; + } + } + } else { + pos += rdlength; + } + } + + if (combinedTxt.isEmpty()) { + return QByteArray(); + } + + return QByteArray::fromBase64(combinedTxt); + } +} // namespace + +QByteArray send(const QByteArray &payload, + const QString &endpointName, + const QString &baseDomain, + const QString &dnsServer, + DnsProtocol protocol, + quint16 port, + int timeoutMsecs, + const QString &dohEndpoint) +{ + const QString queryName = QStringLiteral("%1.%2").arg(endpointName, baseDomain); + + switch (protocol) { + case DnsProtocol::Udp: + return sendOverUdpChunked(payload, queryName, dnsServer, port, timeoutMsecs); + case DnsProtocol::Tcp: + return sendOverTcp(payload, queryName, dnsServer, port, timeoutMsecs); + case DnsProtocol::Tls: + return sendOverTls(payload, queryName, dnsServer, port, timeoutMsecs); + case DnsProtocol::Https: + return sendOverHttps(payload, queryName, dnsServer, port, dohEndpoint, timeoutMsecs); + case DnsProtocol::Quic: + return QByteArray(); + } + return QByteArray(); +} + +QByteArray sendOverUdp(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + QUdpSocket socket; + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); + + if (query.isEmpty()) { + return QByteArray(); + } + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + return QByteArray(); + } + + const qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port); + if (bytesWritten != query.size()) { + return QByteArray(); + } + + QElapsedTimer timer; + timer.start(); + + while (timer.elapsed() < timeoutMsecs) { + if (socket.waitForReadyRead(qMax(1, timeoutMsecs - static_cast(timer.elapsed())))) { + while (socket.hasPendingDatagrams()) { + QNetworkDatagram datagram = socket.receiveDatagram(); + if (datagram.isValid()) { + return parseDnsTxtResponse(datagram.data()); + } + } + } + } + + return QByteArray(); +} + +QByteArray sendOverTcp(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + qDebug() << "[DNS-TCP] start: queryName=" << queryName << "server=" << dnsServer + << "port=" << port << "payloadBytes=" << payload.size(); + QTcpSocket socket; + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + qWarning() << "[DNS-TCP] failed to resolve" << dnsServer; + return QByteArray(); + } + + socket.connectToHost(dnsAddress, port); + if (!socket.waitForConnected(timeoutMsecs)) { + qWarning() << "[DNS-TCP] connect failed:" << socket.errorString(); + return QByteArray(); + } + qDebug() << "[DNS-TCP] connected"; + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); + + if (query.isEmpty()) { + qWarning() << "[DNS-TCP] failed to build DNS query"; + socket.close(); + return QByteArray(); + } + qDebug() << "[DNS-TCP] built DNS query bytes=" << query.size() << "txid=" << transactionId; + qDebug() << "[DNS-TCP] query head hex (first 64 bytes):" + << query.left(64).toHex(' '); + qDebug() << "[DNS-TCP] query tail hex (last 32 bytes):" + << query.right(32).toHex(' '); + + quint16 length = qToBigEndian(static_cast(query.size())); + QByteArray tcpQuery; + tcpQuery.append(reinterpret_cast(&length), sizeof(quint16)); + tcpQuery.append(query); + + const qint64 bytesWritten = socket.write(tcpQuery); + qDebug() << "[DNS-TCP] wrote bytes=" << bytesWritten << "/ expected=" << tcpQuery.size(); + if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { + qWarning() << "[DNS-TCP] write failed:" << socket.errorString(); + socket.close(); + return QByteArray(); + } + + QElapsedTimer timer; + timer.start(); + + while (socket.bytesAvailable() < 2) { + const int remaining = timeoutMsecs - timer.elapsed(); + if (remaining <= 0 || !socket.waitForReadyRead(remaining)) { + qWarning() << "[DNS-TCP] timeout waiting for response length, socketState=" + << socket.state() << "err=" << socket.errorString() + << "bytesAvailable=" << socket.bytesAvailable(); + socket.close(); + return QByteArray(); + } + } + + QByteArray lengthBytes = socket.read(2); + if (lengthBytes.size() != 2) { + qWarning() << "[DNS-TCP] could not read length prefix"; + socket.close(); + return QByteArray(); + } + + const quint16 responseLength = + qFromBigEndian(*reinterpret_cast(lengthBytes.constData())); + qDebug() << "[DNS-TCP] response length prefix=" << responseLength; + + QByteArray response; + while (response.size() < responseLength) { + const int remaining = timeoutMsecs - timer.elapsed(); + if (remaining <= 0) { + qWarning() << "[DNS-TCP] timeout reading body, got" << response.size() << "/" << responseLength; + socket.close(); + return QByteArray(); + } + + if (socket.bytesAvailable() > 0) { + response.append(socket.read(responseLength - response.size())); + } else if (!socket.waitForReadyRead(remaining)) { + qWarning() << "[DNS-TCP] timeout in waitForReadyRead, got" << response.size() << "/" << responseLength; + socket.close(); + return QByteArray(); + } + } + + qDebug() << "[DNS-TCP] full response read, bytes=" << response.size(); + socket.close(); + QByteArray parsed = parseDnsTxtResponse(response); + qDebug() << "[DNS-TCP] parsed TXT payload bytes=" << parsed.size(); + return parsed; +} + +QByteArray sendOverTls(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + QSslSocket socket; +#ifdef AGW_INSECURE_SSL + socket.setPeerVerifyMode(QSslSocket::VerifyNone); + QObject::connect(&socket, QOverload &>::of(&QSslSocket::sslErrors), + &socket, [&socket](const QList &errs) { + qWarning() << "[DoT] sslErrors (ignored, AGW_INSECURE_SSL=1):" << errs; + socket.ignoreSslErrors(); + }); +#else + socket.setPeerVerifyMode(QSslSocket::VerifyPeer); +#endif + + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + qWarning() << "[DoT] failed to resolve" << dnsServer; + return QByteArray(); + } + + socket.connectToHostEncrypted(dnsServer, port); + if (!socket.waitForEncrypted(timeoutMsecs)) { + qWarning() << "[DoT] handshake failed:" << socket.errorString(); + return QByteArray(); + } + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); + + if (query.isEmpty()) { + socket.close(); + return QByteArray(); + } + + quint16 length = qToBigEndian(static_cast(query.size())); + QByteArray tcpQuery; + tcpQuery.append(reinterpret_cast(&length), sizeof(quint16)); + tcpQuery.append(query); + + const qint64 bytesWritten = socket.write(tcpQuery); + if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) { + socket.close(); + return QByteArray(); + } + + QElapsedTimer timer; + timer.start(); + + while (socket.bytesAvailable() < 2) { + const int remaining = timeoutMsecs - timer.elapsed(); + if (remaining <= 0 || !socket.waitForReadyRead(remaining)) { + socket.close(); + return QByteArray(); + } + } + + QByteArray lengthBytes = socket.read(2); + if (lengthBytes.size() != 2) { + socket.close(); + return QByteArray(); + } + + const quint16 responseLength = + qFromBigEndian(*reinterpret_cast(lengthBytes.constData())); + + QByteArray response; + while (response.size() < responseLength) { + const int remaining = timeoutMsecs - timer.elapsed(); + if (remaining <= 0) { + socket.close(); + return QByteArray(); + } + + if (socket.bytesAvailable() > 0) { + response.append(socket.read(responseLength - response.size())); + } else if (!socket.waitForReadyRead(remaining)) { + socket.close(); + return QByteArray(); + } + } + + socket.close(); + return parseDnsTxtResponse(response); +} + +QByteArray sendOverHttps(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, const QString &endpoint, int timeoutMsecs) +{ + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray dnsQuery = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); + + qDebug() << "[DoH] queryName=" << queryName << "payloadBytes=" << payload.size() + << "dnsQueryBytes=" << dnsQuery.size() << "txid=" << transactionId; + + if (dnsQuery.isEmpty()) { + qWarning() << "[DoH] failed to build DNS query (payload too big or queryName invalid)"; + return QByteArray(); + } + + const QString scheme = (port == 443) ? QStringLiteral("https") : QStringLiteral("http"); + const QString url = QStringLiteral("%1://%2:%3%4").arg(scheme).arg(dnsServer).arg(port).arg(endpoint); + + qDebug() << "[DoH] POST" << url << "timeoutMs=" << timeoutMsecs; + + QNetworkRequest request((QUrl(url))); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/dns-message"); + request.setRawHeader("Accept", "application/dns-message"); + request.setTransferTimeout(timeoutMsecs); + + QNetworkAccessManager manager; + QNetworkReply *reply = manager.post(request, dnsQuery); + + QObject::connect(reply, &QNetworkReply::sslErrors, reply, + [reply](const QList &errs) { + qWarning() << "[DoH] sslErrors:" << errs; +#ifdef AGW_INSECURE_SSL + qWarning() << "[DoH] AGW_INSECURE_SSL=1, ignoring SSL errors"; + reply->ignoreSslErrors(); +#endif + }); + + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + QTimer::singleShot(timeoutMsecs, &loop, &QEventLoop::quit); + loop.exec(); + + if (!reply->isFinished()) { + qWarning() << "[DoH] timeout after" << timeoutMsecs << "ms, aborting"; + reply->abort(); + reply->deleteLater(); + return QByteArray(); + } + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[DoH] reply error:" << reply->error() << reply->errorString() + << "httpStatus=" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + reply->deleteLater(); + return QByteArray(); + } + + QByteArray response = reply->readAll(); + qDebug() << "[DoH] raw HTTP response bytes=" << response.size() + << "httpStatus=" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + reply->deleteLater(); + + if (response.isEmpty()) { + qWarning() << "[DoH] empty HTTP response body"; + return QByteArray(); + } + + QByteArray parsed = parseDnsTxtResponse(response); + qDebug() << "[DoH] parsed TXT payload bytes=" << parsed.size(); + return parsed; +} + +QByteArray sendOverUdpChunked(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs) +{ + qDebug() << "[DNS-UDP] start: queryName=" << queryName << "server=" << dnsServer + << "port=" << port << "payloadBytes=" << payload.size() << "timeoutMs=" << timeoutMsecs; + const QHostAddress dnsAddress = resolveHostAddress(dnsServer); + if (dnsAddress.isNull()) { + qWarning() << "[DNS-UDP] failed to resolve" << dnsServer; + return QByteArray(); + } + qDebug() << "[DNS-UDP] resolved to" << dnsAddress.toString(); + + constexpr int MAX_INITIAL_RETRIES = 3; + constexpr int MAX_CHUNK_RETRIES = 2; + constexpr int MAX_CONCURRENT_REQUESTS = 5; + constexpr int BASE_TIMEOUT_MS = 2000; + + auto sendUdpRequestWithTimeout = [&](const QByteArray &query, int requestTimeoutMs) -> QByteArray { + QUdpSocket socket; + const qint64 written = socket.writeDatagram(query, dnsAddress, port); + if (written != query.size()) { + return QByteArray(); + } + + QElapsedTimer timer; + timer.start(); + + while (timer.elapsed() < requestTimeoutMs) { + if (socket.waitForReadyRead(qMax(1, requestTimeoutMs - static_cast(timer.elapsed())))) { + while (socket.hasPendingDatagrams()) { + QNetworkDatagram datagram = socket.receiveDatagram(); + if (datagram.isValid()) { + return datagram.data(); + } + } + } + } + + return QByteArray(); + }; + + auto sendWithRetry = [&](const QByteArray &query, int maxRetries) -> QByteArray { + for (int attempt = 0; attempt < maxRetries; ++attempt) { + const int timeout = BASE_TIMEOUT_MS * (attempt + 1); + QByteArray response = sendUdpRequestWithTimeout(query, timeout); + if (!response.isEmpty()) { + return response; + } + + if (attempt < maxRetries - 1) { + QThread::msleep(timeout / 2); + } + } + return QByteArray(); + }; + + const quint16 transactionId = static_cast(QDateTime::currentMSecsSinceEpoch() & 0xFFFF); + const QByteArray initialQuery = buildDnsTxtQueryWithPayload(queryName, transactionId, payload); + + qDebug() << "[DNS-UDP] initialQuery size=" << initialQuery.size() << "txid=" << transactionId; + + if (initialQuery.isEmpty()) { + qWarning() << "[DNS-UDP] failed to build initial query (payload too big or queryName invalid)"; + return QByteArray(); + } + + const QByteArray firstResponse = sendWithRetry(initialQuery, MAX_INITIAL_RETRIES); + qDebug() << "[DNS-UDP] first response size=" << firstResponse.size(); + + if (firstResponse.isEmpty()) { + qWarning() << "[DNS-UDP] no response from server after" << MAX_INITIAL_RETRIES << "retries"; + return QByteArray(); + } + + const ChunkMeta meta = parseChunkMeta(firstResponse); + const QByteArray firstTxtData = parseDnsTxtResponse(firstResponse); + + qDebug() << "[DNS-UDP] meta totalChunks=" << meta.totalChunks + << "chunkId=" << meta.chunkId << "firstTxtData size=" << firstTxtData.size(); + + if (firstTxtData.isEmpty()) { + qWarning() << "[DNS-UDP] failed to parse TXT data from first response"; + return QByteArray(); + } + + if (meta.totalChunks <= 1) { + qDebug() << "[DNS-UDP] single chunk, returning" << firstTxtData.size() << "bytes"; + return firstTxtData; + } + + QMap chunks; + chunks[0] = firstTxtData; + + auto requestChunksBatch = [&](const QList &chunkIndices, int batchTimeout) { + if (chunkIndices.isEmpty()) return; + + QList> sockets; + QMap socketToIndex; + + for (int idx : chunkIndices) { + if (chunks.contains(idx)) continue; + + const quint16 chunkTxId = + static_cast((QDateTime::currentMSecsSinceEpoch() + idx) & 0xFFFF); + const QByteArray chunkQuery = + buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, idx); + + if (chunkQuery.isEmpty()) { + continue; + } + + auto socket = QSharedPointer::create(); + socket->writeDatagram(chunkQuery, dnsAddress, port); + socketToIndex[socket.data()] = idx; + sockets.append(socket); + } + + if (sockets.isEmpty()) return; + + QElapsedTimer deadline; + deadline.start(); + int receivedCount = 0; + const int expectedCount = sockets.size(); + + while (deadline.elapsed() < batchTimeout && receivedCount < expectedCount + && chunks.size() < meta.totalChunks) { + for (auto &socket : sockets) { + if (socket->waitForReadyRead(50)) { + while (socket->hasPendingDatagrams()) { + QNetworkDatagram datagram = socket->receiveDatagram(); + if (datagram.isValid()) { + const QByteArray chunkTxtData = parseDnsTxtResponse(datagram.data()); + if (!chunkTxtData.isEmpty()) { + const ChunkMeta chunkMeta = parseChunkMeta(datagram.data()); + const int idx = (chunkMeta.totalChunks > 0) + ? chunkMeta.chunkIndex + : socketToIndex.value(socket.data(), -1); + if (idx >= 0 && !chunks.contains(idx)) { + chunks[idx] = chunkTxtData; + receivedCount++; + } + } + } + } + } + } + } + }; + + const int totalTimeout = qMax(timeoutMsecs / 2, 5000); + const int batchTimeout = totalTimeout / (MAX_CHUNK_RETRIES + 1); + + for (int retryRound = 0; retryRound <= MAX_CHUNK_RETRIES; ++retryRound) { + QList missing; + for (int i = 1; i < meta.totalChunks; ++i) { + if (!chunks.contains(i)) { + missing.append(i); + } + } + + if (missing.isEmpty()) { + break; + } + + for (int batchStart = 0; batchStart < missing.size(); batchStart += MAX_CONCURRENT_REQUESTS) { + const QList batch = missing.mid(batchStart, MAX_CONCURRENT_REQUESTS); + requestChunksBatch(batch, batchTimeout); + } + } + + QList finalMissing; + for (int i = 0; i < meta.totalChunks; ++i) { + if (!chunks.contains(i)) { + finalMissing.append(i); + } + } + + if (!finalMissing.isEmpty()) { + return QByteArray(); + } + + QByteArray combined; + combined.reserve(meta.totalSize > 0 ? meta.totalSize : meta.totalChunks * 500); + + for (int i = 0; i < meta.totalChunks; ++i) { + combined.append(chunks[i]); + } + + return combined; +} + +} // namespace amnezia::transport::dns::DnsTunnel diff --git a/client/core/transport/dns/dnsTunnel.h b/client/core/transport/dns/dnsTunnel.h new file mode 100644 index 000000000..31c29d355 --- /dev/null +++ b/client/core/transport/dns/dnsTunnel.h @@ -0,0 +1,35 @@ +#ifndef DNSTUNNEL_H +#define DNSTUNNEL_H + +#include +#include + +#include "dnsResolver.h" + +namespace amnezia::transport::dns::DnsTunnel +{ + +QByteArray send(const QByteArray &payload, + const QString &endpointName, + const QString &baseDomain, + const QString &dnsServer, + DnsProtocol protocol, + quint16 port, + int timeoutMsecs = 30000, + const QString &dohEndpoint = QStringLiteral("/dns-query")); + +QByteArray sendOverUdp(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs); +QByteArray sendOverTcp(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs); +QByteArray sendOverTls(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs); +QByteArray sendOverHttps(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, const QString &endpoint, int timeoutMsecs); + +QByteArray sendOverUdpChunked(const QByteArray &payload, const QString &queryName, + const QString &dnsServer, quint16 port, int timeoutMsecs); + +} // namespace amnezia::transport::dns::DnsTunnel + +#endif // DNSTUNNEL_H diff --git a/client/core/transport/dnsGatewayTransport.cpp b/client/core/transport/dnsGatewayTransport.cpp new file mode 100644 index 000000000..1e9cfbe75 --- /dev/null +++ b/client/core/transport/dnsGatewayTransport.cpp @@ -0,0 +1,157 @@ +#include "dnsGatewayTransport.h" + +#include +#include +#include +#include +#include + +#include "dns/dnsTunnel.h" +#include "core/networkUtilities.h" + +#ifdef AMNEZIA_DESKTOP + #include "core/ipcclient.h" +#endif + +namespace amnezia::transport +{ + +DnsGatewayTransport::DnsGatewayTransport(dns::DnsProtocol protocol, + const QString &dnsServer, + const QString &baseDomain, + quint16 port, + int timeoutMsecs, + bool isStrictKillSwitchEnabled, + const QString &dohEndpoint) + : m_protocol(protocol), + m_dnsServer(dnsServer), + m_baseDomain(baseDomain), + m_port(port), + m_timeoutMsecs(timeoutMsecs), + m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled), + m_dohEndpoint(dohEndpoint) +{ +} + +QString DnsGatewayTransport::name() const +{ + switch (m_protocol) { + case dns::DnsProtocol::Udp: return QStringLiteral("DNS-UDP"); + case dns::DnsProtocol::Tcp: return QStringLiteral("DNS-TCP"); + case dns::DnsProtocol::Tls: return QStringLiteral("DNS-DoT"); + case dns::DnsProtocol::Https: return QStringLiteral("DNS-DoH"); + case dns::DnsProtocol::Quic: return QStringLiteral("DNS-DoQ"); + } + return QStringLiteral("DNS"); +} + +QString DnsGatewayTransport::resolveServerOnce() +{ + if (m_resolved.load()) { + return m_resolvedServerIp; + } + + QHostAddress addr(m_dnsServer); + if (!addr.isNull()) { + m_resolvedServerIp = m_dnsServer; + } else { + QHostInfo info = QHostInfo::fromName(m_dnsServer); + if (!info.addresses().isEmpty()) { + m_resolvedServerIp = info.addresses().first().toString(); + } else { + m_resolvedServerIp = m_dnsServer; + } + } + m_resolved.store(true); + return m_resolvedServerIp; +} + +void DnsGatewayTransport::applyKillSwitchAllowlist(const QString &ip) +{ +#ifdef AMNEZIA_DESKTOP + if (!m_isStrictKillSwitchEnabled || ip.isEmpty()) { + return; + } + IpcClient::withInterface([&](QSharedPointer iface) { + QRemoteObjectPendingReply reply = iface->addKillSwitchAllowedRange(QStringList { ip }); + if (!reply.waitForFinished(1000) || !reply.returnValue()) { + qWarning() << "DnsGatewayTransport: addKillSwitchAllowedRange failed for" << ip; + } + }); +#else + Q_UNUSED(ip) +#endif +} + +amnezia::ErrorCode DnsGatewayTransport::send(const QString &endpointTemplate, + const QByteArray &requestBody, + QByteArray &decryptedResponse, + const DecryptionHook &decryptionHook) +{ + QString endpointName = endpointTemplate; + endpointName.remove("%1"); + if (endpointName.startsWith(QLatin1String("v1/"))) { + endpointName = endpointName.mid(3); + } + while (endpointName.endsWith(QLatin1Char('/'))) { + endpointName.chop(1); + } + while (endpointName.startsWith(QLatin1Char('/'))) { + endpointName = endpointName.mid(1); + } + + qDebug() << "[DNS-Transport]" << name() << "send() endpointTemplate=" << endpointTemplate + << "endpointName=" << endpointName << "baseDomain=" << m_baseDomain + << "server=" << m_dnsServer << "port=" << m_port + << "dohPath=" << m_dohEndpoint << "timeoutMs=" << m_timeoutMsecs + << "requestBodyBytes=" << requestBody.size(); + + if (endpointName.isEmpty() || m_baseDomain.isEmpty() || m_dnsServer.isEmpty()) { + qWarning() << "[DNS-Transport] ABORT: empty endpoint/baseDomain/server"; + return amnezia::ErrorCode::AmneziaServiceConnectionFailed; + } + + const bool needsHostname = (m_protocol == dns::DnsProtocol::Tls + || m_protocol == dns::DnsProtocol::Https); + + QString serverIp = resolveServerOnce(); + QString serverForRequest = needsHostname ? m_dnsServer : serverIp; + + qDebug() << "[DNS-Transport] resolved server IP=" << serverIp + << "serverForRequest=" << serverForRequest + << "needsHostname=" << needsHostname; + + applyKillSwitchAllowlist(serverIp); + + const QByteArray encrypted = dns::DnsTunnel::send(requestBody, + endpointName, + m_baseDomain, + serverForRequest, + m_protocol, + m_port, + m_timeoutMsecs, + m_dohEndpoint); + qDebug() << "[DNS-Transport] DnsTunnel::send returned" << encrypted.size() << "bytes"; + if (encrypted.isEmpty()) { + qWarning() << "[DNS-Transport] DnsTunnel returned empty payload, treat as connection failure"; + return amnezia::ErrorCode::AmneziaServiceConnectionFailed; + } + + if (!decryptionHook) { + qCritical() << "[DNS-Transport] decryption hook is null"; + return amnezia::ErrorCode::ApiConfigDecryptionError; + } + + DecryptionResult decrypted = decryptionHook(encrypted); + if (!decrypted.isOk) { + qCritical() << "[DNS-Transport] response decryption failed (encrypted bytes=" + << encrypted.size() << ")"; + return amnezia::ErrorCode::ApiConfigDecryptionError; + } + + qDebug() << "[DNS-Transport] success, decrypted response bytes=" << decrypted.decrypted.size(); + decryptedResponse = decrypted.decrypted; + return amnezia::ErrorCode::NoError; +} + +} // namespace amnezia::transport diff --git a/client/core/transport/dnsGatewayTransport.h b/client/core/transport/dnsGatewayTransport.h new file mode 100644 index 000000000..2dafd11c6 --- /dev/null +++ b/client/core/transport/dnsGatewayTransport.h @@ -0,0 +1,49 @@ +#ifndef DNSGATEWAYTRANSPORT_H +#define DNSGATEWAYTRANSPORT_H + +#include +#include + +#include "dns/dnsResolver.h" +#include "igatewaytransport.h" + +namespace amnezia::transport +{ + +class DnsGatewayTransport : public IGatewayTransport +{ +public: + DnsGatewayTransport(dns::DnsProtocol protocol, + const QString &dnsServer, + const QString &baseDomain, + quint16 port, + int timeoutMsecs, + bool isStrictKillSwitchEnabled, + const QString &dohEndpoint = QStringLiteral("/dns-query")); + + QString name() const override; + + amnezia::ErrorCode send(const QString &endpointTemplate, + const QByteArray &requestBody, + QByteArray &decryptedResponse, + const DecryptionHook &decryptionHook) override; + +private: + QString resolveServerOnce(); + void applyKillSwitchAllowlist(const QString &ip); + + dns::DnsProtocol m_protocol; + QString m_dnsServer; + QString m_baseDomain; + quint16 m_port; + int m_timeoutMsecs; + bool m_isStrictKillSwitchEnabled; + QString m_dohEndpoint; + + std::atomic_bool m_resolved{ false }; + QString m_resolvedServerIp; +}; + +} // namespace amnezia::transport + +#endif // DNSGATEWAYTRANSPORT_H diff --git a/client/core/transport/httpGatewayTransport.cpp b/client/core/transport/httpGatewayTransport.cpp new file mode 100644 index 000000000..5b9221f63 --- /dev/null +++ b/client/core/transport/httpGatewayTransport.cpp @@ -0,0 +1,345 @@ +#include "httpGatewayTransport.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "QBlockCipher.h" + +#include "amnezia_application.h" +#include "core/api/apiUtils.h" +#include "core/networkUtilities.h" +#include "utilities.h" + +#ifdef AMNEZIA_DESKTOP + #include "core/ipcclient.h" +#endif + +#ifdef Q_OS_IOS + #include "platforms/ios/ios_controller.h" +#endif + +namespace amnezia::transport +{ + +QMutex HttpGatewayTransport::s_proxyMutex; +QString HttpGatewayTransport::s_proxyUrl; + +namespace +{ + constexpr int kProxyHealthTimeoutMsecs = 1000; + constexpr int httpStatusCodeNotFound = 404; + constexpr int httpStatusCodeConflict = 409; + constexpr int httpStatusCodeNotImplemented = 501; + + constexpr QLatin1String errorResponsePattern1("No active configuration found for"); + constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for"); + constexpr QLatin1String errorResponsePattern3("Account not found."); + constexpr QLatin1String updateRequestResponsePattern("client version update is required"); +} // namespace + +HttpGatewayTransport::HttpGatewayTransport(const QString &endpoint, + bool isDevEnvironment, + int requestTimeoutMsecs, + bool isStrictKillSwitchEnabled) + : m_endpoint(endpoint), + m_isDevEnvironment(isDevEnvironment), + m_requestTimeoutMsecs(requestTimeoutMsecs), + m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) +{ +} + +void HttpGatewayTransport::applyKillSwitchAllowlist(const QString &host) +{ +#ifdef AMNEZIA_DESKTOP + if (!m_isStrictKillSwitchEnabled || host.isEmpty()) { + return; + } + const QString ip = NetworkUtilities::getIPAddress(host); + if (ip.isEmpty()) { + return; + } + IpcClient::withInterface([&](QSharedPointer iface) { + QRemoteObjectPendingReply reply = iface->addKillSwitchAllowedRange(QStringList { ip }); + if (!reply.waitForFinished(1000) || !reply.returnValue()) { + qWarning() << "HttpGatewayTransport: addKillSwitchAllowedRange failed for" << ip; + } + }); +#else + Q_UNUSED(host) +#endif +} + +HttpGatewayTransport::ReplyOutcome HttpGatewayTransport::doPost(const QString &fullUrl, const QByteArray &requestBody) +{ + ReplyOutcome outcome; + +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("X-Client-Request-ID", + QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); + request.setUrl(fullUrl); + + applyKillSwitchAllowlist(QUrl(fullUrl).host()); + + QNetworkReply *reply = amnApp->networkManager()->post(request, requestBody); + + QEventLoop wait; + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + QObject::connect(reply, &QNetworkReply::sslErrors, [&, reply](const QList &errors) { + outcome.sslErrors = errors; +#ifdef AGW_INSECURE_SSL + qWarning() << "[HTTP] sslErrors (ignored, AGW_INSECURE_SSL=1):" << errors; + reply->ignoreSslErrors(); + outcome.sslErrors.clear(); +#endif + }); + wait.exec(QEventLoop::ExcludeUserInputEvents); + + outcome.encryptedBody = reply->readAll(); + outcome.errorString = reply->errorString(); + outcome.networkError = reply->error(); + outcome.httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + reply->deleteLater(); + return outcome; +} + +bool HttpGatewayTransport::shouldBypass(const ReplyOutcome &outcome, const DecryptionResult &decrypted) const +{ + if (!outcome.sslErrors.isEmpty()) { + return false; + } + + if (!decrypted.isOk) { + return true; + } + + int apiHttpStatus = -1; + QJsonDocument jsonDoc = QJsonDocument::fromJson(decrypted.decrypted); + if (jsonDoc.isObject()) { + apiHttpStatus = jsonDoc.object().value("http_status").toInt(-1); + } + + if (outcome.networkError == QNetworkReply::NetworkError::OperationCanceledError + || outcome.networkError == QNetworkReply::NetworkError::TimeoutError) { + return true; + } + if (decrypted.decrypted.contains("html")) { + return true; + } + if (apiHttpStatus == httpStatusCodeNotFound) { + if (decrypted.decrypted.contains(errorResponsePattern1) + || decrypted.decrypted.contains(errorResponsePattern2) + || decrypted.decrypted.contains(errorResponsePattern3)) { + return false; + } + return true; + } + if (apiHttpStatus == httpStatusCodeNotImplemented) { + if (decrypted.decrypted.contains(updateRequestResponsePattern)) { + return false; + } + return true; + } + if (apiHttpStatus == httpStatusCodeConflict) { + return false; + } + if (outcome.networkError != QNetworkReply::NetworkError::NoError) { + return true; + } + return false; +} + +QStringList HttpGatewayTransport::fetchProxyUrls(const QByteArray &/*serviceHint*/) +{ + QStringList baseUrls = m_isDevEnvironment + ? QString(DEV_S3_ENDPOINT).split(", ") + : QString(PROD_S3_ENDPOINT).split(", "); + + QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + + QStringList proxyStorageUrls; + for (const auto &baseUrl : baseUrls) { + proxyStorageUrls.push_back(baseUrl + "endpoints.json"); + } + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + for (const auto &proxyStorageUrl : proxyStorageUrls) { + request.setUrl(proxyStorageUrl); + QNetworkReply *reply = amnApp->networkManager()->get(request); + QEventLoop wait; + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + wait.exec(QEventLoop::ExcludeUserInputEvents); + + if (reply->error() != QNetworkReply::NoError) { + reply->deleteLater(); + continue; + } + + QByteArray encryptedResponseBody = reply->readAll(); + reply->deleteLater(); + + QByteArray responseBody; + try { + if (!m_isDevEnvironment) { + QCryptographicHash hash(QCryptographicHash::Sha512); + hash.addData(rsaKey); + QByteArray hashResult = hash.result().toHex(); + + QByteArray key = QByteArray::fromHex(hashResult.left(64)); + QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); + + QSimpleCrypto::QBlockCipher blockCipher; + responseBody = blockCipher.decryptAesBlockCipher(QByteArray::fromBase64(encryptedResponseBody), key, iv); + } else { + responseBody = encryptedResponseBody; + } + } catch (...) { + Utils::logException(); + qCritical() << "HttpGatewayTransport: error decrypting proxy storage payload"; + continue; + } + + QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array(); + QStringList endpoints; + endpoints.reserve(endpointsArray.size()); + for (const QJsonValue &endpoint : endpointsArray) { + endpoints.push_back(endpoint.toString()); + } + return endpoints; + } + + return {}; +} + +amnezia::ErrorCode HttpGatewayTransport::send(const QString &endpointTemplate, + const QByteArray &requestBody, + QByteArray &decryptedResponse, + const DecryptionHook &decryptionHook) +{ + auto buildOutcome = [&](const QString &gatewayBase) { + return doPost(endpointTemplate.arg(gatewayBase), requestBody); + }; + + auto tryDecrypt = [&](const QByteArray &encrypted) -> DecryptionResult { + if (!decryptionHook) { + DecryptionResult r; + r.decrypted = encrypted; + r.isOk = false; + return r; + } + return decryptionHook(encrypted); + }; + + QString cachedProxy; + { + QMutexLocker lock(&s_proxyMutex); + cachedProxy = s_proxyUrl; + } + const QString primaryBase = cachedProxy.isEmpty() ? m_endpoint : cachedProxy; + + ReplyOutcome outcome = buildOutcome(primaryBase); + DecryptionResult decrypted = tryDecrypt(outcome.encryptedBody); + + if (outcome.sslErrors.isEmpty() && shouldBypass(outcome, decrypted)) { + QStringList proxyUrls = fetchProxyUrls(QByteArray()); + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); + + bool bypassResolved = false; + + if (cachedProxy.isEmpty()) { + QNetworkRequest healthRequest; + healthRequest.setTransferTimeout(kProxyHealthTimeoutMsecs); + healthRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + for (const QString &proxyUrl : std::as_const(proxyUrls)) { + healthRequest.setUrl(proxyUrl + "lmbd-health"); + QNetworkReply *reply = amnApp->networkManager()->get(healthRequest); + QEventLoop wait; + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + wait.exec(QEventLoop::ExcludeUserInputEvents); + + const auto err = reply->error(); + reply->deleteLater(); + if (err == QNetworkReply::NoError) { + QMutexLocker lock(&s_proxyMutex); + s_proxyUrl = proxyUrl; + cachedProxy = proxyUrl; + break; + } + } + } + + if (!cachedProxy.isEmpty()) { + ReplyOutcome retry = buildOutcome(cachedProxy); + DecryptionResult retryDecrypted = tryDecrypt(retry.encryptedBody); + if (retry.sslErrors.isEmpty() && !shouldBypass(retry, retryDecrypted)) { + outcome = retry; + decrypted = retryDecrypted; + bypassResolved = true; + } + } + + if (!bypassResolved) { + for (const QString &proxyUrl : std::as_const(proxyUrls)) { + ReplyOutcome retry = buildOutcome(proxyUrl); + DecryptionResult retryDecrypted = tryDecrypt(retry.encryptedBody); + if (retry.sslErrors.isEmpty() && !shouldBypass(retry, retryDecrypted)) { + { + QMutexLocker lock(&s_proxyMutex); + s_proxyUrl = proxyUrl; + } + outcome = retry; + decrypted = retryDecrypted; + bypassResolved = true; + break; + } + } + } + } + + auto errorCode = apiUtils::checkNetworkReplyErrors(outcome.sslErrors, + outcome.errorString, + outcome.networkError, + outcome.httpStatusCode, + decrypted.decrypted); + if (errorCode != amnezia::ErrorCode::NoError) { + return errorCode; + } + + if (!decrypted.isOk) { + qCritical() << "HttpGatewayTransport: response decryption failed"; + return amnezia::ErrorCode::ApiConfigDecryptionError; + } + + decryptedResponse = decrypted.decrypted; + return amnezia::ErrorCode::NoError; +} + +} // namespace amnezia::transport diff --git a/client/core/transport/httpGatewayTransport.h b/client/core/transport/httpGatewayTransport.h new file mode 100644 index 000000000..d0c15c077 --- /dev/null +++ b/client/core/transport/httpGatewayTransport.h @@ -0,0 +1,58 @@ +#ifndef HTTPGATEWAYTRANSPORT_H +#define HTTPGATEWAYTRANSPORT_H + +#include +#include +#include +#include +#include +#include +#include + +#include "igatewaytransport.h" + +namespace amnezia::transport +{ + +class HttpGatewayTransport : public IGatewayTransport +{ +public: + HttpGatewayTransport(const QString &endpoint, + bool isDevEnvironment, + int requestTimeoutMsecs, + bool isStrictKillSwitchEnabled); + + QString name() const override { return QStringLiteral("HTTP"); } + + amnezia::ErrorCode send(const QString &endpointTemplate, + const QByteArray &requestBody, + QByteArray &decryptedResponse, + const DecryptionHook &decryptionHook) override; + +private: + struct ReplyOutcome + { + QByteArray encryptedBody; + QList sslErrors; + QNetworkReply::NetworkError networkError = QNetworkReply::NoError; + QString errorString; + int httpStatusCode = 0; + }; + + ReplyOutcome doPost(const QString &fullUrl, const QByteArray &requestBody); + void applyKillSwitchAllowlist(const QString &host); + QStringList fetchProxyUrls(const QByteArray &serviceHint); + bool shouldBypass(const ReplyOutcome &outcome, const DecryptionResult &decrypted) const; + + QString m_endpoint; + bool m_isDevEnvironment; + int m_requestTimeoutMsecs; + bool m_isStrictKillSwitchEnabled; + + static QMutex s_proxyMutex; + static QString s_proxyUrl; +}; + +} // namespace amnezia::transport + +#endif // HTTPGATEWAYTRANSPORT_H diff --git a/client/core/transport/igatewaytransport.h b/client/core/transport/igatewaytransport.h new file mode 100644 index 000000000..ebd50035b --- /dev/null +++ b/client/core/transport/igatewaytransport.h @@ -0,0 +1,36 @@ +#ifndef IGATEWAYTRANSPORT_H +#define IGATEWAYTRANSPORT_H + +#include +#include +#include + +#include "core/defs.h" + +namespace amnezia::transport +{ + +struct DecryptionResult +{ + QByteArray decrypted; + bool isOk = false; +}; + +using DecryptionHook = std::function; + +class IGatewayTransport +{ +public: + virtual ~IGatewayTransport() = default; + + virtual QString name() const = 0; + + virtual amnezia::ErrorCode send(const QString &endpointTemplate, + const QByteArray &requestBody, + QByteArray &decryptedResponse, + const DecryptionHook &decryptionHook) = 0; +}; + +} // namespace amnezia::transport + +#endif // IGATEWAYTRANSPORT_H diff --git a/client/settings.cpp b/client/settings.cpp index 3a0fa9fce..e5fc029ce 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -15,7 +15,7 @@ namespace const char cloudFlareNs2[] = "1.0.0.1"; //constexpr char gatewayEndpoint[] = "http://localhost:80/"; - constexpr char gatewayEndpoint[] = "http://127.0.0.1:80/"; + constexpr char gatewayEndpoint[] = "http://localhost:80/"; } Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this) diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 2be09cc4a..e8cfb7197 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -43,8 +43,9 @@ add_definitions(-DAGW_DNS_TIMEOUT_MS="$ENV{AGW_DNS_TIMEOUT_MS}") qt_add_executable(${PROJECT_NAME} tst_transports.cpp - ${CLIENT_ROOT_DIR}/core/networkUtilities.cpp - ${CLIENT_ROOT_DIR}/core/networkUtilities.h + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsResolver.cpp + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsTunnel.cpp + ${CLIENT_ROOT_DIR}/core/transport/dns/dnsPacket.cpp ${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp ${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp ${QSIMPLECRYPTO_DIR}/sources/QX509.cpp @@ -55,6 +56,7 @@ qt_add_executable(${PROJECT_NAME} target_include_directories(${PROJECT_NAME} PRIVATE ${CLIENT_ROOT_DIR} ${CLIENT_ROOT_DIR}/core + ${CLIENT_ROOT_DIR}/core/transport ${QSIMPLECRYPTO_DIR} ${QSIMPLECRYPTO_DIR}/include ${OPENSSL_INCLUDE_DIR} diff --git a/client/tests/tst_transports.cpp b/client/tests/tst_transports.cpp index 45036dbc1..cdbf4c986 100644 --- a/client/tests/tst_transports.cpp +++ b/client/tests/tst_transports.cpp @@ -12,13 +12,16 @@ #include #include -#include "networkUtilities.h" +#include "transport/dns/dnsResolver.h" +#include "transport/dns/dnsTunnel.h" #include "QBlockCipher.h" #include "QRsa.h" #include #include +using amnezia::transport::dns::DnsProtocol; + struct TransportResult { QString name; bool success = false; @@ -32,7 +35,7 @@ struct TestConfig { QString httpEndpoint; struct DnsEntry { QString name; - NetworkUtilities::DnsTransport type; + DnsProtocol type; QString server; QString domain; quint16 port; @@ -57,7 +60,7 @@ static TestConfig buildConfigFromEnv() if (server.isEmpty() || domain.isEmpty()) return cfg; - auto addEntry = [&](NetworkUtilities::DnsTransport type, const QString &name, + auto addEntry = [&](DnsProtocol type, const QString &name, const char *portDefine, quint16 defaultPort, const QString &dohPath = QString()) { TestConfig::DnsEntry e; e.type = type; @@ -70,15 +73,15 @@ static TestConfig buildConfigFromEnv() cfg.dnsTransports.append(e); }; - addEntry(NetworkUtilities::DnsTransport::Udp, "UDP", AGW_DNS_PORT_UDP, 5353); - addEntry(NetworkUtilities::DnsTransport::Tcp, "TCP", AGW_DNS_PORT_UDP, 5353); - addEntry(NetworkUtilities::DnsTransport::Tls, "DoT", AGW_DNS_PORT_DOT, 853); + addEntry(DnsProtocol::Udp, "UDP", AGW_DNS_PORT_UDP, 5353); + addEntry(DnsProtocol::Tcp, "TCP", AGW_DNS_PORT_UDP, 5353); + addEntry(DnsProtocol::Tls, "DoT", AGW_DNS_PORT_DOT, 853); QString dohPath = QString(AGW_DNS_DOH_PATH); if (dohPath.isEmpty()) dohPath = "/dns-query"; - addEntry(NetworkUtilities::DnsTransport::Https, "DoH", AGW_DNS_PORT_DOH, 443, dohPath); + addEntry(DnsProtocol::Https, "DoH", AGW_DNS_PORT_DOH, 443, dohPath); - addEntry(NetworkUtilities::DnsTransport::Quic, "DoQ", AGW_DNS_PORT_DOQ, 8853); + addEntry(DnsProtocol::Quic, "DoQ", AGW_DNS_PORT_DOQ, 8853); return cfg; } @@ -93,7 +96,6 @@ static QString resolveHost(const QString &host) return host; } -// Replicate the RSA+AES encryption from GatewayController::prepareRequest struct EncryptedPayload { QByteArray body; QByteArray key; @@ -220,11 +222,10 @@ private: QElapsedTimer timer; timer.start(); - bool needsHostname = (entry.type == NetworkUtilities::DnsTransport::Https || - entry.type == NetworkUtilities::DnsTransport::Tls); + bool needsHostname = (entry.type == DnsProtocol::Https || entry.type == DnsProtocol::Tls); QString serverAddr = needsHostname ? entry.server : resolvedIp; - r.responseBody = NetworkUtilities::sendViaDnsTunnel( + r.responseBody = amnezia::transport::dns::DnsTunnel::send( payload, "services", entry.domain, serverAddr, entry.type, entry.port, m_config.timeoutMs, entry.dohPath); @@ -242,7 +243,7 @@ private slots: m_config = buildConfigFromEnv(); QVERIFY2(!m_config.dnsTransports.isEmpty(), - "AGW_DNS_SERVER / AGW_DNS_DOMAIN not set — cannot run transport tests"); + "AGW_DNS_SERVER / AGW_DNS_DOMAIN not set -- cannot run transport tests"); qDebug() << "HTTP endpoint:" << m_config.httpEndpoint; qDebug() << "DNS transports:" << m_config.dnsTransports.size(); @@ -263,8 +264,6 @@ private slots: } } - // ========== Transport-level tests (raw payload, no encryption) ========== - void test_transport_http() { QByteArray payload = R"({"test":true})"; @@ -280,7 +279,7 @@ private slots: QTest::addColumn("transportIndex"); for (int i = 0; i < m_config.dnsTransports.size(); ++i) { const auto &e = m_config.dnsTransports[i]; - if (e.type == NetworkUtilities::DnsTransport::Quic) continue; + if (e.type == DnsProtocol::Quic) continue; QTest::newRow(qPrintable(e.name)) << i; } } @@ -303,8 +302,6 @@ private slots: } } - // ========== E2E tests (RSA+AES encryption, full round-trip) ========== - void test_e2e_http() { if (!m_hasRsaKey) QSKIP("No RSA key -- skipping E2E"); @@ -339,7 +336,7 @@ private slots: QTest::addColumn("transportIndex"); for (int i = 0; i < m_config.dnsTransports.size(); ++i) { const auto &e = m_config.dnsTransports[i]; - if (e.type == NetworkUtilities::DnsTransport::Quic) continue; + if (e.type == DnsProtocol::Quic) continue; QTest::newRow(qPrintable(QString("E2E-%1").arg(e.name))) << i; } } @@ -382,8 +379,6 @@ private slots: } } - // ========== Summary ========== - void cleanupTestCase() { qDebug() << ""; diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 714c411a7..75035ca93 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -749,7 +749,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig()); - ErrorCode errorCode = gatewayController.postParallel(endpoint, apiPayload, responseBody); + ErrorCode errorCode = gatewayController.post(endpoint, apiPayload, responseBody); if (errorCode == ErrorCode::NoError) { errorCode = fillServerConfig(serviceProtocol, protocolData, responseBody, serverConfig); @@ -963,5 +963,5 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig()); - return gatewayController.postParallel(endpoint, apiPayload, responseBody); + return gatewayController.post(endpoint, apiPayload, responseBody); } diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp index 0ee7f57c6..6c22d99ef 100644 --- a/client/ui/controllers/api/apiNewsController.cpp +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -55,7 +55,7 @@ void ApiNewsController::fetchNews(bool showError) gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig()); QByteArray responseBody; - ErrorCode errorCode = gatewayController.postParallel(endpoint, payload, responseBody); + ErrorCode errorCode = gatewayController.post(endpoint, payload, responseBody); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode, showError); diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 55aa82b8d..2977b1002 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -79,7 +79,7 @@ bool ApiSettingsController::getAccountInfo(bool reload) gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig()); - ErrorCode errorCode = gatewayController.postParallel(endpoint, apiPayload, responseBody); + ErrorCode errorCode = gatewayController.post(endpoint, apiPayload, responseBody); if (errorCode != ErrorCode::NoError) { emit errorOccurred(errorCode);