This commit is contained in:
svamnezia
2026-04-16 00:19:03 +03:00
parent 3a7d285e55
commit 511ce6f62a
14 changed files with 10311 additions and 6270 deletions
+6
View File
@@ -240,3 +240,9 @@ endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
qt_finalize_target(${PROJECT})
option(BUILD_TESTS "Build transport integration tests" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
+50 -13
View File
@@ -16,7 +16,9 @@
#include <QPromise>
#include <QUrl>
#include <QHostAddress>
#include <QHostInfo>
#include <QDebug>
#include <QThread>
#include <QtConcurrent>
#include "QBlockCipher.h"
@@ -250,6 +252,26 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
endpointName.chop(1);
}
// Pre-resolve all DNS server hostnames to IPs (thread-safe: done before launching parallel threads)
QMap<QString, QString> 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) {
@@ -274,10 +296,13 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
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";
qDebug() << "[Transport] PRIMARY HTTP failed:" << replyError << errorStr
<< "status:" << httpStatus << "body size:" << encryptedBody.size();
return ErrorCode::AmneziaServiceConnectionFailed;
}
@@ -302,10 +327,13 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
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,
transport.server, transport.type, transport.port, m_requestTimeoutMsecs, transport.dohPath);
serverAddr, transport.type, transport.port, m_requestTimeoutMsecs, transport.dohPath);
if (dnsResponse.isEmpty()) {
qDebug() << "[Transport] PRIMARY DNS" << transportName << "failed";
@@ -358,15 +386,22 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
QByteArray successResult;
QString successTransport;
QMutex resultMutex;
QList<QFuture<void>> futures;
QList<QThread*> threads;
auto launchThread = [&](std::function<void()> 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) {
auto httpFuture = QtConcurrent::run([&]() {
launchThread([&]() {
if (gotSuccess.load()) return;
qDebug() << "[Transport] FALLBACK: Trying HTTP";
EncryptedRequestData httpRequestData = prepareRequest(endpoint, apiPayload, false);
EncryptedRequestData httpRequestData = prepareRequest(endpoint, apiPayload, true);
if (httpRequestData.errorCode != ErrorCode::NoError) return;
QNetworkAccessManager nam;
@@ -393,7 +428,6 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
}
} catch (...) {}
});
futures.append(httpFuture);
}
// DNS transports (skip the one that was primary)
@@ -412,7 +446,7 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
}
if (wasPrimary) continue;
auto dnsFuture = QtConcurrent::run([&, transport]() {
launchThread([&, transport]() {
if (gotSuccess.load()) return;
QString transportName;
@@ -424,10 +458,14 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
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,
transport.server, transport.type, transport.port, m_requestTimeoutMsecs, transport.dohPath);
serverAddr, transport.type, transport.port, m_requestTimeoutMsecs, transport.dohPath);
if (dnsResponse.isEmpty()) return;
@@ -441,13 +479,11 @@ ErrorCode GatewayController::postParallel(const QString &endpoint, const QJsonOb
}
} catch (...) {}
});
futures.append(dnsFuture);
}
// CRITICAL: Wait for ALL futures to complete to prevent use-after-free
// (lambdas capture references to local variables like resultMutex, successResult)
for (auto &future : futures) {
future.waitForFinished();
for (auto *t : threads) {
t->wait();
delete t;
}
if (gotSuccess.load()) {
@@ -620,6 +656,7 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
EVP_PKEY *publicKey = nullptr;
try {
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
rsaKey.replace("\\n", "\n");
QSimpleCrypto::QRsa rsa;
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
} catch (...) {
+110 -136
View File
@@ -63,6 +63,14 @@
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
@@ -716,7 +724,7 @@ QString NetworkUtilities::resolveDnsOverUdp(const QString &hostname, const QStri
}
// Отправляем запрос
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
@@ -764,7 +772,7 @@ QString NetworkUtilities::resolveDnsOverTcp(const QString &hostname, const QStri
QTcpSocket socket;
// Подключаемся к DNS серверу
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
@@ -848,12 +856,11 @@ QString NetworkUtilities::resolveDnsOverTls(const QString &hostname, const QStri
QSslSocket socket;
// Подключаемся к DNS серверу через TLS
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
// Настраиваем SSL (отключаем проверку сертификата для простоты, можно добавить проверку позже)
socket.setPeerVerifyMode(QSslSocket::QueryPeer);
socket.connectToHostEncrypted(dnsAddress.toString(), port);
@@ -1001,7 +1008,7 @@ QString NetworkUtilities::resolveDnsOverQuic(const QString &hostname, const QStr
QUdpSocket socket;
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
@@ -1447,7 +1454,7 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdp(const QByteArray &payload, cons
return QByteArray();
}
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
qDebug() << "[DNS Tunnel UDP] Invalid DNS server address:" << dnsServer;
return QByteArray();
@@ -1461,37 +1468,23 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdp(const QByteArray &payload, cons
qDebug() << "[DNS Tunnel UDP] Sent" << bytesWritten << "bytes to" << dnsServer << ":" << port;
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QElapsedTimer timer;
timer.start();
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();
while (timer.elapsed() < timeoutMsecs) {
if (socket.waitForReadyRead(qMax(1, timeoutMsecs - static_cast<int>(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());
}
}
}
});
timer.start();
loop.exec();
timer.stop();
if (!responseReceived || response.isEmpty()) {
qDebug() << "[DNS Tunnel UDP] No response received (timeout)";
return QByteArray();
}
qDebug() << "[DNS Tunnel UDP] Received response:" << response.size() << "bytes";
return parseDnsTxtResponse(response);
qDebug() << "[DNS Tunnel UDP] No response received (timeout)";
return QByteArray();
}
QByteArray NetworkUtilities::sendViaDnsTunnelTcp(const QByteArray &payload, const QString &queryName,
@@ -1499,7 +1492,7 @@ QByteArray NetworkUtilities::sendViaDnsTunnelTcp(const QByteArray &payload, cons
{
QTcpSocket socket;
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
qDebug() << "[DNS Tunnel TCP] Invalid DNS server address:" << dnsServer;
return QByteArray();
@@ -1535,45 +1528,48 @@ QByteArray NetworkUtilities::sendViaDnsTunnelTcp(const QByteArray &payload, cons
qDebug() << "[DNS Tunnel TCP] Sent" << bytesWritten << "bytes to" << dnsServer << ":" << port;
// Wait for response
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(&socket, &QTcpSocket::readyRead, [&]() {
if (socket.bytesAvailable() >= 2 && response.isEmpty()) {
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() == 2) {
quint16 responseLength = qFromBigEndian<quint16>(*reinterpret_cast<const quint16*>(lengthBytes.constData()));
while (socket.bytesAvailable() < responseLength) {
if (!socket.waitForReadyRead(timeoutMsecs / 2)) {
break;
}
}
if (socket.bytesAvailable() >= responseLength) {
response = socket.read(responseLength);
responseReceived = true;
loop.quit();
}
}
}
});
// Synchronous read: first get 2-byte length prefix
QElapsedTimer timer;
timer.start();
loop.exec();
timer.stop();
socket.close();
if (!responseReceived || response.isEmpty()) {
qDebug() << "[DNS Tunnel TCP] No response received (timeout)";
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<quint16>(*reinterpret_cast<const quint16*>(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);
}
@@ -1582,16 +1578,14 @@ QByteArray NetworkUtilities::sendViaDnsTunnelTls(const QByteArray &payload, cons
const QString &dnsServer, quint16 port, int timeoutMsecs)
{
QSslSocket socket;
// Disable certificate verification for local testing
socket.setPeerVerifyMode(QSslSocket::VerifyNone);
QHostAddress dnsAddress(dnsServer);
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();
@@ -1694,21 +1688,23 @@ QByteArray NetworkUtilities::sendViaDnsTunnelHttps(const QByteArray &payload, co
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;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
timer.start();
QTimer::singleShot(timeoutMsecs, &loop, &QEventLoop::quit);
loop.exec();
timer.stop();
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();
@@ -1733,7 +1729,7 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
{
qDebug() << "[DNS Tunnel UDP Chunked] Starting request to" << queryName;
QHostAddress dnsAddress(dnsServer);
QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
qDebug() << "[DNS Tunnel UDP Chunked] Invalid DNS server:" << dnsServer;
return QByteArray();
@@ -1753,31 +1749,21 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
return QByteArray();
}
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(requestTimeoutMs);
QElapsedTimer timer;
timer.start();
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();
while (timer.elapsed() < requestTimeoutMs) {
if (socket.waitForReadyRead(qMax(1, requestTimeoutMs - static_cast<int>(timer.elapsed())))) {
while (socket.hasPendingDatagrams()) {
QNetworkDatagram datagram = socket.receiveDatagram();
if (datagram.isValid()) {
return datagram.data();
}
}
}
});
}
timer.start();
loop.exec();
timer.stop();
return responseReceived ? response : QByteArray();
return QByteArray();
};
// Helper lambda with retry and exponential backoff
@@ -1854,7 +1840,7 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
QMap<QUdpSocket*, int> socketToIndex;
for (int idx : chunkIndices) {
if (chunks.contains(idx)) continue; // Already have this chunk
if (chunks.contains(idx)) continue;
quint16 chunkTxId = static_cast<quint16>((QDateTime::currentMSecsSinceEpoch() + idx) & 0xFFFF);
QByteArray chunkQuery = buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, idx);
@@ -1874,45 +1860,33 @@ QByteArray NetworkUtilities::sendViaDnsTunnelUdpChunked(const QByteArray &payloa
qDebug() << "[DNS Tunnel UDP Chunked] Sent" << sockets.size() << "parallel requests";
// Wait for responses with deadline
QEventLoop loop;
QTimer deadline;
deadline.setSingleShot(true);
deadline.setInterval(batchTimeout);
// Poll all sockets synchronously until timeout
QElapsedTimer deadline;
deadline.start();
int receivedCount = 0;
int expectedCount = sockets.size();
QObject::connect(&deadline, &QTimer::timeout, &loop, &QEventLoop::quit);
for (auto &socket : sockets) {
QObject::connect(socket.data(), &QUdpSocket::readyRead, [&, sock = socket.data()]() {
while (sock->hasPendingDatagrams()) {
QNetworkDatagram datagram = sock->receiveDatagram();
if (datagram.isValid()) {
QByteArray chunkTxtData = parseDnsTxtResponse(datagram.data());
if (!chunkTxtData.isEmpty()) {
ChunkMeta chunkMeta = parseChunkMeta(datagram.data());
int idx = (chunkMeta.totalChunks > 0) ? chunkMeta.chunkIndex : socketToIndex.value(sock, -1);
if (idx >= 0 && !chunks.contains(idx)) {
chunks[idx] = chunkTxtData;
qDebug() << "[DNS Tunnel UDP Chunked] Received chunk" << idx << ":" << chunkTxtData.size() << "bytes";
receivedCount++;
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++;
}
}
}
}
}
// Exit early if we have all chunks
if (receivedCount >= expectedCount || chunks.size() >= meta.totalChunks) {
loop.quit();
}
});
}
}
deadline.start();
loop.exec();
deadline.stop();
};
// Process chunks in batches
+78
View File
@@ -0,0 +1,78 @@
cmake_minimum_required(VERSION 3.25.0)
project(TransportTest)
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
find_package(Qt6 REQUIRED COMPONENTS Core Network Test)
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/")
if(WIN32)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/windows/include")
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/windows/win64/libssl.lib")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/windows/win64/libcrypto.lib")
else()
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/windows/win32/libssl.lib")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/windows/win32/libcrypto.lib")
endif()
elseif(APPLE AND NOT IOS)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a")
elseif(LINUX)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/linux/include")
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
endif()
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
qt_add_executable(${PROJECT_NAME}
tst_transports.cpp
${CLIENT_ROOT_DIR}/core/networkUtilities.cpp
${CLIENT_ROOT_DIR}/core/networkUtilities.h
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
)
target_include_directories(${PROJECT_NAME} PRIVATE
${CLIENT_ROOT_DIR}
${CLIENT_ROOT_DIR}/core
${QSIMPLECRYPTO_DIR}
${QSIMPLECRYPTO_DIR}/include
${OPENSSL_INCLUDE_DIR}
)
target_compile_definitions(${PROJECT_NAME} PRIVATE
CLIENT_SOURCE_DIR="${CLIENT_ROOT_DIR}"
)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt6::Core
Qt6::Network
Qt6::Test
${OPENSSL_LIB_SSL}
${OPENSSL_LIB_CRYPTO}
)
if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32)
endif()
# Copy gateway.json next to the test binary
if(EXISTS "${CLIENT_ROOT_DIR}/gateway.json")
add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CLIENT_ROOT_DIR}/gateway.json"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
)
endif()
add_test(NAME TransportTest COMMAND ${PROJECT_NAME})
+428
View File
@@ -0,0 +1,428 @@
#include <QCoreApplication>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QFile>
#include <QHostAddress>
#include <QHostInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslConfiguration>
#include <QSslError>
#include <QTest>
#include <QUrl>
#include "networkUtilities.h"
#include "QBlockCipher.h"
#include "QRsa.h"
#include <openssl/evp.h>
#include <openssl/rsa.h>
struct TransportResult {
QString name;
bool success = false;
int elapsedMs = 0;
int responseSize = 0;
QString error;
QByteArray responseBody;
};
struct TestConfig {
QString httpEndpoint;
struct DnsEntry {
QString name;
NetworkUtilities::DnsTransport type;
QString server;
QString domain;
quint16 port;
QString dohPath;
};
QList<DnsEntry> dnsTransports;
int timeoutMs = 15000;
};
static TestConfig loadConfig(const QString &path)
{
TestConfig cfg;
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << "Cannot open config:" << path;
return cfg;
}
QJsonObject json = QJsonDocument::fromJson(f.readAll()).object();
if (json.contains("http")) {
cfg.httpEndpoint = json["http"].toObject().value("endpoint").toString();
}
cfg.timeoutMs = json.value("timeout_ms").toInt(15000);
if (json.contains("dns_transports")) {
for (const auto &v : json["dns_transports"].toArray()) {
QJsonObject obj = v.toObject();
TestConfig::DnsEntry e;
e.server = obj.value("server").toString();
e.domain = obj.value("domain").toString();
e.port = static_cast<quint16>(obj.value("port").toInt(5353));
e.dohPath = obj.value("path").toString("/dns-query");
QString t = obj.value("type").toString().toLower();
if (t == "udp") {
e.type = NetworkUtilities::DnsTransport::Udp;
e.name = "UDP";
} else if (t == "tcp") {
e.type = NetworkUtilities::DnsTransport::Tcp;
e.name = "TCP";
} else if (t == "dot" || t == "tls") {
e.type = NetworkUtilities::DnsTransport::Tls;
e.name = "DoT";
} else if (t == "doh" || t == "https") {
e.type = NetworkUtilities::DnsTransport::Https;
e.name = "DoH";
} else if (t == "doq" || t == "quic") {
e.type = NetworkUtilities::DnsTransport::Quic;
e.name = "DoQ";
} else {
continue;
}
cfg.dnsTransports.append(e);
}
}
return cfg;
}
static QString resolveHost(const QString &host)
{
QHostAddress addr(host);
if (!addr.isNull()) return host;
QHostInfo info = QHostInfo::fromName(host);
if (!info.addresses().isEmpty())
return info.addresses().first().toString();
return host;
}
// Replicate the RSA+AES encryption from GatewayController::prepareRequest
struct EncryptedPayload {
QByteArray body;
QByteArray key;
QByteArray iv;
QByteArray salt;
bool ok = false;
QString error;
};
static EncryptedPayload encryptPayload(const QJsonObject &apiPayload, const QByteArray &rsaPubKeyPem)
{
EncryptedPayload result;
QSimpleCrypto::QBlockCipher blockCipher;
result.key = blockCipher.generatePrivateSalt(32);
result.iv = blockCipher.generatePrivateSalt(32);
result.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload;
keyPayload["aes_key"] = QString(result.key.toBase64());
keyPayload["aes_iv"] = QString(result.iv.toBase64());
keyPayload["aes_salt"] = QString(result.salt.toBase64());
try {
QSimpleCrypto::QRsa rsa;
QByteArray pemData = rsaPubKeyPem;
pemData.replace("\\n", "\n");
EVP_PKEY *pubKey = rsa.getPublicKeyFromByteArray(pemData);
if (!pubKey) {
result.error = "Failed to load RSA public key";
return result;
}
QByteArray encKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), pubKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(pubKey);
QByteArray encApiPayload = blockCipher.encryptAesBlockCipher(
QJsonDocument(apiPayload).toJson(), result.key, result.iv, "", result.salt);
QJsonObject requestBody;
requestBody["key_payload"] = QString(encKeyPayload.toBase64());
requestBody["api_payload"] = QString(encApiPayload.toBase64());
result.body = QJsonDocument(requestBody).toJson();
result.ok = true;
} catch (const std::exception &ex) {
result.error = QString("Encryption failed: %1").arg(ex.what());
} catch (...) {
result.error = "Encryption failed: unknown error";
}
return result;
}
static QByteArray decryptResponse(const QByteArray &encrypted, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
try {
QSimpleCrypto::QBlockCipher blockCipher;
return blockCipher.decryptAesBlockCipher(encrypted, key, iv, "", salt);
} catch (...) {
return QByteArray();
}
}
class TransportTest : public QObject
{
Q_OBJECT
private:
TestConfig m_config;
QByteArray m_rsaKey;
bool m_hasRsaKey = false;
QList<TransportResult> m_results;
void logResult(const TransportResult &r) {
QString status = r.success ? "OK" : "FAIL";
qDebug().noquote() << QString("[%1] %2 | %3ms | %4 bytes | %5")
.arg(status, -4)
.arg(r.name, -20)
.arg(r.elapsedMs, 5)
.arg(r.responseSize, 6)
.arg(r.error.isEmpty() ? "---" : r.error);
}
TransportResult doHttpTransport(const QString &endpoint, const QByteArray &payload) {
TransportResult r;
r.name = "HTTP";
QElapsedTimer timer;
timer.start();
QNetworkAccessManager nam;
QNetworkRequest request(QUrl(endpoint));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setTransferTimeout(m_config.timeoutMs);
QNetworkReply *reply = nam.post(request, payload);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
r.elapsedMs = static_cast<int>(timer.elapsed());
if (reply->error() != QNetworkReply::NoError) {
r.error = QString("HTTP %1: %2")
.arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())
.arg(reply->errorString());
r.responseBody = reply->readAll();
r.responseSize = r.responseBody.size();
} else {
r.responseBody = reply->readAll();
r.responseSize = r.responseBody.size();
r.success = !r.responseBody.isEmpty();
if (!r.success) r.error = "Empty response";
}
reply->deleteLater();
return r;
}
TransportResult doDnsTransport(const TestConfig::DnsEntry &entry, const QByteArray &payload,
const QString &resolvedIp) {
TransportResult r;
r.name = QString("DNS-%1").arg(entry.name);
QElapsedTimer timer;
timer.start();
bool needsHostname = (entry.type == NetworkUtilities::DnsTransport::Https ||
entry.type == NetworkUtilities::DnsTransport::Tls);
QString serverAddr = needsHostname ? entry.server : resolvedIp;
r.responseBody = NetworkUtilities::sendViaDnsTunnel(
payload, "services", entry.domain,
serverAddr, entry.type, entry.port,
m_config.timeoutMs, entry.dohPath);
r.elapsedMs = static_cast<int>(timer.elapsed());
r.responseSize = r.responseBody.size();
r.success = !r.responseBody.isEmpty();
if (!r.success) r.error = "Empty/no response";
return r;
}
private slots:
void initTestCase()
{
QString configPath = QCoreApplication::applicationDirPath() + "/gateway.json";
if (!QFile::exists(configPath)) {
configPath = QString(CLIENT_SOURCE_DIR) + "/gateway.json";
}
qDebug() << "Loading config from:" << configPath;
m_config = loadConfig(configPath);
QVERIFY2(!m_config.httpEndpoint.isEmpty(), "gateway.json: http endpoint missing");
QVERIFY2(!m_config.dnsTransports.isEmpty(), "gateway.json: no dns_transports configured");
qDebug() << "HTTP endpoint:" << m_config.httpEndpoint;
qDebug() << "DNS transports:" << m_config.dnsTransports.size();
qDebug() << "Timeout:" << m_config.timeoutMs << "ms";
QByteArray prodKey(PROD_AGW_PUBLIC_KEY);
QByteArray devKey(DEV_AGW_PUBLIC_KEY);
if (!prodKey.isEmpty()) {
m_rsaKey = prodKey;
m_hasRsaKey = true;
qDebug() << "Using PROD_AGW_PUBLIC_KEY for E2E tests";
} else if (!devKey.isEmpty()) {
m_rsaKey = devKey;
m_hasRsaKey = true;
qDebug() << "Using DEV_AGW_PUBLIC_KEY for E2E tests";
} else {
qWarning() << "No RSA public key found -- E2E tests will be SKIPPED";
}
}
// ========== Transport-level tests (raw payload, no encryption) ==========
void test_transport_http()
{
QByteArray payload = R"({"test":true})";
TransportResult r = doHttpTransport(m_config.httpEndpoint, payload);
m_results.append(r);
logResult(r);
QVERIFY2(r.success || r.responseSize > 0,
qPrintable(QString("HTTP transport failed: %1").arg(r.error)));
}
void test_transport_dns_data()
{
QTest::addColumn<int>("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;
QTest::newRow(qPrintable(e.name)) << i;
}
}
void test_transport_dns()
{
QFETCH(int, transportIndex);
const auto &entry = m_config.dnsTransports[transportIndex];
QString resolvedIp = resolveHost(entry.server);
qDebug() << "Server:" << entry.server << "-> IP:" << resolvedIp
<< "Port:" << entry.port;
QByteArray payload = R"({"test":true})";
TransportResult r = doDnsTransport(entry, payload, resolvedIp);
m_results.append(r);
logResult(r);
if (!r.success) {
qWarning() << "DNS" << entry.name << "transport failed (server may be down):" << r.error;
}
}
// ========== E2E tests (RSA+AES encryption, full round-trip) ==========
void test_e2e_http()
{
if (!m_hasRsaKey) QSKIP("No RSA key -- skipping E2E");
QJsonObject apiPayload;
apiPayload["protocol"] = "any";
EncryptedPayload enc = encryptPayload(apiPayload, m_rsaKey);
QVERIFY2(enc.ok, qPrintable(enc.error));
TransportResult r = doHttpTransport(m_config.httpEndpoint, enc.body);
r.name = "E2E-HTTP";
if (r.success) {
QByteArray decrypted = decryptResponse(r.responseBody, enc.key, enc.iv, enc.salt);
if (!decrypted.isEmpty()) {
r.responseBody = decrypted;
r.responseSize = decrypted.size();
qDebug() << "Decrypted response:" << decrypted.left(200);
} else {
r.error = "Decryption failed (raw body size: " + QString::number(r.responseBody.size()) + ")";
r.success = false;
}
}
m_results.append(r);
logResult(r);
QVERIFY2(r.success, qPrintable(QString("E2E HTTP failed: %1").arg(r.error)));
}
void test_e2e_dns_data()
{
QTest::addColumn<int>("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;
QTest::newRow(qPrintable(QString("E2E-%1").arg(e.name))) << i;
}
}
void test_e2e_dns()
{
if (!m_hasRsaKey) QSKIP("No RSA key -- skipping E2E");
QFETCH(int, transportIndex);
const auto &entry = m_config.dnsTransports[transportIndex];
QString resolvedIp = resolveHost(entry.server);
qDebug() << "E2E via" << entry.name << "server:" << entry.server
<< "-> IP:" << resolvedIp << "port:" << entry.port;
QJsonObject apiPayload;
apiPayload["protocol"] = "any";
EncryptedPayload enc = encryptPayload(apiPayload, m_rsaKey);
QVERIFY2(enc.ok, qPrintable(enc.error));
TransportResult r = doDnsTransport(entry, enc.body, resolvedIp);
r.name = QString("E2E-%1").arg(entry.name);
if (r.success) {
QByteArray decrypted = decryptResponse(r.responseBody, enc.key, enc.iv, enc.salt);
if (!decrypted.isEmpty()) {
r.responseBody = decrypted;
r.responseSize = decrypted.size();
qDebug() << "Decrypted response:" << decrypted.left(200);
} else {
r.error = "Decryption failed (raw body size: " + QString::number(r.responseBody.size()) + ")";
r.success = false;
}
}
m_results.append(r);
logResult(r);
if (!r.success) {
qWarning() << "E2E DNS" << entry.name << "failed:" << r.error;
}
}
// ========== Summary ==========
void cleanupTestCase()
{
qDebug() << "";
qDebug() << "============================================================";
qDebug() << " TRANSPORT TEST SUMMARY";
qDebug() << "============================================================";
qDebug().noquote() << QString(" %-4s | %-20s | %5s | %6s | %s")
.arg("", "Transport", "ms", "bytes", "Error");
qDebug() << "------------------------------------------------------------";
int passed = 0, failed = 0;
for (const auto &r : m_results) {
logResult(r);
if (r.success) ++passed; else ++failed;
}
qDebug() << "------------------------------------------------------------";
qDebug().noquote() << QString("Total: %1 passed, %2 failed, %3 total")
.arg(passed).arg(failed).arg(m_results.size());
qDebug() << "============================================================";
}
};
QTEST_MAIN(TransportTest)
#include "tst_transports.moc"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+116 -146
View File
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ru_RU">
<context>
<name>AdLabel</name>
</context>
<context>
<name>AllowedDnsController</name>
<message>
@@ -74,23 +71,23 @@
<context>
<name>ApiConfigsController</name>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="499"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="703"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="470"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="646"/>
<source>%1 installed successfully.</source>
<translation>%1 успешно установлен.</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="640"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="593"/>
<source>Subscription restored successfully.</source>
<translation>Подписка успешно восстановлена.</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="768"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="707"/>
<source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="772"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="711"/>
<source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation>
</message>
@@ -98,103 +95,83 @@
<context>
<name>ApiPremV1MigrationDrawer</name>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="53"/>
<source>Switch to the new Amnezia Premium subscription</source>
<translation>Перейдите на новый тип подписки Amnezia Premium</translation>
<translation type="vanished">Перейдите на новый тип подписки Amnezia Premium</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="73"/>
<source>We&apos;ll preserve all remaining days of your current subscription and give you an extra month as a thank you. </source>
<translation>Мы сохраним все оставшиеся дни текущей подписки и подарим дополнительный месяц в благодарность за переход. </translation>
<translation type="vanished">Мы сохраним все оставшиеся дни текущей подписки и подарим дополнительный месяц в благодарность за переход. </translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="74"/>
<source>This new subscription type will be actively developed with more locations and features added regularly. Currently available:</source>
<translation>Именно новый тип подписки будет активно развиваться и пополняться новыми локациями и функциями. Уже доступны:</translation>
<translation type="vanished">Именно новый тип подписки будет активно развиваться и пополняться новыми локациями и функциями. Уже доступны:</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="76"/>
<source>&lt;li&gt;20 locations (with more coming soon)&lt;/li&gt;</source>
<translation>&lt;li&gt;20 локаций (их число будет расти)&lt;/li&gt;</translation>
<translation type="vanished">&lt;li&gt;20 локаций (их число будет расти)&lt;/li&gt;</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="77"/>
<source>&lt;li&gt;Easier switching between countries in the app&lt;/li&gt;</source>
<translation>&lt;li&gt;Удобное переключение между странами в приложении&lt;/li&gt;</translation>
<translation type="vanished">&lt;li&gt;Удобное переключение между странами в приложении&lt;/li&gt;</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="78"/>
<source>&lt;li&gt;Personal dashboard to manage your subscription&lt;/li&gt;</source>
<translation>&lt;li&gt;Личный кабинет для управления подпиской&lt;/li&gt;</translation>
<translation type="vanished">&lt;li&gt;Личный кабинет для управления подпиской&lt;/li&gt;</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="80"/>
<source>Old keys will be deactivated after switching.</source>
<translation>После перехода старые ключи перестанут работать.</translation>
<translation type="vanished">После перехода старые ключи перестанут работать.</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="91"/>
<source>Email</source>
<translation>Email</translation>
<translation type="vanished">Email</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="92"/>
<source>mail@example.com</source>
<translation>mail@example.com</translation>
<translation type="vanished">mail@example.com</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="103"/>
<source>No old format subscriptions for a given email</source>
<translation>Для указанного адреса электронной почты нет подписок старого типа</translation>
<translation type="vanished">Для указанного адреса электронной почты нет подписок старого типа</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="114"/>
<source>Enter the email you used for your current subscription</source>
<translation>Укажите адрес почты, который использовали при заказе текущей подписки</translation>
<translation type="vanished">Укажите адрес почты, который использовали при заказе текущей подписки</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="136"/>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="178"/>
<source>Continue</source>
<translation>Продолжить</translation>
<translation type="vanished">Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="156"/>
<source>Remind me later</source>
<translation>Напомнить позже</translation>
<translation type="vanished">Напомнить позже</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="174"/>
<source>Don&apos;t remind me again</source>
<translation>Больше не напоминать</translation>
<translation type="vanished">Больше не напоминать</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="177"/>
<source>No more reminders? You can always switch to the new format in the server settings</source>
<translation>Отключить напоминания? Вы всегда сможете перейти на новый тип подписки в настройках сервера</translation>
<translation type="vanished">Отключить напоминания? Вы всегда сможете перейти на новый тип подписки в настройках сервера</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1MigrationDrawer.qml" line="179"/>
<source>Cancel</source>
<translation>Отменить</translation>
<translation type="vanished">Отменить</translation>
</message>
</context>
<context>
<name>ApiPremV1SubListDrawer</name>
<message>
<location filename="../ui/qml/Components/ApiPremV1SubListDrawer.qml" line="55"/>
<source>Choose Subscription</source>
<translation>Выбрать подписку</translation>
<translation type="vanished">Выбрать подписку</translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1SubListDrawer.qml" line="74"/>
<source>Order ID: </source>
<translation>ID заказа: </translation>
<translation type="vanished">ID заказа: </translation>
</message>
<message>
<location filename="../ui/qml/Components/ApiPremV1SubListDrawer.qml" line="76"/>
<source>Purchase Date: </source>
<translation>Дата покупки: </translation>
<translation type="vanished">Дата покупки: </translation>
</message>
</context>
<context>
@@ -323,7 +300,7 @@
<context>
<name>ContextMenuType</name>
<message>
<location filename="../ui/qml/Controls2/ContextMenuType.qml" line="9"/>
<location filename="../ui/qml/Controls2/ContextMenuType.qml" line="10"/>
<source>C&amp;ut</source>
<translation>Вырезать</translation>
</message>
@@ -333,12 +310,12 @@
<translation>Копировать</translation>
</message>
<message>
<location filename="../ui/qml/Controls2/ContextMenuType.qml" line="21"/>
<location filename="../ui/qml/Controls2/ContextMenuType.qml" line="20"/>
<source>&amp;Paste</source>
<translation>Вставить</translation>
</message>
<message>
<location filename="../ui/qml/Controls2/ContextMenuType.qml" line="29"/>
<location filename="../ui/qml/Controls2/ContextMenuType.qml" line="27"/>
<source>&amp;SelectAll</source>
<translation>Выбрать всё</translation>
</message>
@@ -444,47 +421,47 @@ Already installed containers were found on the server. All installed containers
На сервере обнаружены установленные протоколы и сервисы. Все они были добавлены в приложение</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="722"/>
<location filename="../ui/controllers/installController.cpp" line="729"/>
<source>Settings updated successfully</source>
<translation>Настройки успешно обновлены</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="739"/>
<location filename="../ui/controllers/installController.cpp" line="746"/>
<source>Server &apos;%1&apos; was rebooted</source>
<translation>Сервер &apos;%1&apos; был перезагружен</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="751"/>
<location filename="../ui/controllers/installController.cpp" line="758"/>
<source>Server &apos;%1&apos; was removed</source>
<translation>Сервер &apos;%1&apos; был удален</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="762"/>
<location filename="../ui/controllers/installController.cpp" line="769"/>
<source>All containers from server &apos;%1&apos; have been removed</source>
<translation>Все протоколы и сервисы были удалены с сервера &apos;%1&apos;</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="780"/>
<location filename="../ui/controllers/installController.cpp" line="787"/>
<source>%1 has been removed from the server &apos;%2&apos;</source>
<translation>%1 был удален с сервера &apos;%2&apos;</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="789"/>
<location filename="../ui/controllers/installController.cpp" line="796"/>
<source>Api config removed</source>
<translation>Конфигурация API удалена</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="811"/>
<location filename="../ui/controllers/installController.cpp" line="818"/>
<source>%1 cached profile cleared</source>
<translation>%1 закэшированный профиль очищен</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="952"/>
<location filename="../ui/controllers/installController.cpp" line="959"/>
<source>Please login as the user</source>
<translation>Пожалуйста, войдите в систему от имени пользователя</translation>
</message>
<message>
<location filename="../ui/controllers/installController.cpp" line="980"/>
<location filename="../ui/controllers/installController.cpp" line="987"/>
<source>Server added successfully</source>
<translation>Сервер успешно добавлен</translation>
</message>
@@ -557,19 +534,16 @@ Already installed containers were found on the server. All installed containers
<context>
<name>OtpCodeDrawer</name>
<message>
<location filename="../ui/qml/Components/OtpCodeDrawer.qml" line="44"/>
<source>OTP code was sent to your email</source>
<translation>Одноразовый код был отправлен на ваш email</translation>
<translation type="vanished">Одноразовый код был отправлен на ваш email</translation>
</message>
<message>
<location filename="../ui/qml/Components/OtpCodeDrawer.qml" line="55"/>
<source>OTP Code</source>
<translation>Одноразовый код</translation>
<translation type="vanished">Одноразовый код</translation>
</message>
<message>
<location filename="../ui/qml/Components/OtpCodeDrawer.qml" line="66"/>
<source>Continue</source>
<translation>Продолжить</translation>
<translation type="vanished">Продолжить</translation>
</message>
</context>
<context>
@@ -601,49 +575,46 @@ Already installed containers were found on the server. All installed containers
<context>
<name>PageHome</name>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="58"/>
<source>You&apos;ve successfully switched to the new Amnezia Premium subscription!</source>
<translation>Вы успешно перешли на новый тип подписки Amnezia Premium!</translation>
<translation type="vanished">Вы успешно перешли на новый тип подписки Amnezia Premium!</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="59"/>
<source>Old keys will no longer work. Please use your new subscription key to connect.
Thank you for staying with us!</source>
<translation>Старые ключи перестанут работать. Пожалуйста, используйте новый ключ для подключения.
<translation type="vanished">Старые ключи перестанут работать. Пожалуйста, используйте новый ключ для подключения.
Спасибо, что остаетесь с нами!</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="60"/>
<source>Continue</source>
<translation>Продолжить</translation>
<translation type="vanished">Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="107"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="83"/>
<source>Logging enabled</source>
<translation>Логирование включено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="135"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="111"/>
<source>Dev gateway enabled</source>
<translation>Dev gateway enabled</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="177"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="153"/>
<source>Split tunneling enabled</source>
<translation>Раздельное туннелирование включено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="177"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="153"/>
<source>Split tunneling disabled</source>
<translation>Раздельное туннелирование выключено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="424"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="400"/>
<source>VPN protocol</source>
<translation>VPN-протокол</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="477"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="453"/>
<source>Servers</source>
<translation>Серверы</translation>
</message>
@@ -744,37 +715,37 @@ Thank you for staying with us!</source>
<translation>I1 - Special junk 1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="413"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="414"/>
<source>I2 - Special junk 2</source>
<translation>I2 - Special junk 2</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="435"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="437"/>
<source>I3 - Special junk 3</source>
<translation>I3 - Special junk 3</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="457"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="460"/>
<source>I4 - Special junk 4</source>
<translation>I4 - Special junk 4</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="479"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="483"/>
<source>I5 - Special junk 5</source>
<translation>I5 - Special junk 5</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="540"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="545"/>
<source>The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + S3 + cookie reply size (64) + S4 + transport packet size (32)</source>
<translation>Значение поля S1 + размер инициализации сообщения (148) не должно равняться S2 + размер ответа сообщения (92) + S3 + размер ответа cookie (64) + S4 + размер транспортного пакета (32)</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="546"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="551"/>
<source>All users with whom you shared a connection with will no longer be able to connect to it.</source>
<translation>Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="518"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="522"/>
<source>Save</source>
<translation>Сохранить</translation>
</message>
@@ -839,27 +810,27 @@ Thank you for staying with us!</source>
<translation>H3 - Underload packet magic header</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="532"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="537"/>
<source>The values of the H1-H4 fields must be unique</source>
<translation>Значения в полях H1-H4 должны быть уникальными</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="545"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="550"/>
<source>Save settings?</source>
<translation>Сохранить настройки?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="547"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="552"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="548"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="553"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="552"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="557"/>
<source>Unable change settings while there is an active connection</source>
<translation>Невозможно изменить настройки во время активного соединения</translation>
</message>
@@ -2800,108 +2771,107 @@ Thank you for staying with us!</source>
<translation>Новые установленные протоколы и сервисы не обнаружены</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="131"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="161"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="191"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="220"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="130"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="160"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="190"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="219"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="132"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="162"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="192"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="221"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="131"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="161"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="191"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="220"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="111"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="110"/>
<source>Check the server for previously installed Amnezia services</source>
<translation>Проверить сервер на наличие ранее установленных сервисов Amnezia</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="112"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="111"/>
<source>Add them to the application if they were not displayed</source>
<translation>Добавить их в приложение, если они не отображаются</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="125"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="124"/>
<source>Reboot server</source>
<translation>Перезагрузить сервер</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="129"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="128"/>
<source>Do you want to reboot the server?</source>
<translation>Вы уверены, что хотите перезагрузить сервер?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="130"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="129"/>
<source>The reboot process may take approximately 30 seconds. Are you sure you wish to proceed?</source>
<translation>Процесс перезагрузки может занять около 30 секунд. Вы уверены, что хотите продолжить?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="136"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="135"/>
<source>Cannot reboot server during active connection</source>
<translation>Невозможно перезагрузить сервер во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="159"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="158"/>
<source>Do you want to remove the server from application?</source>
<translation>Вы уверены, что хотите удалить сервер из приложения?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="166"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="165"/>
<source>Cannot remove server during active connection</source>
<translation>Невозможно удалить сервер во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="189"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="188"/>
<source>Do you want to clear server from Amnezia software?</source>
<translation>Вы хотите очистить сервер от всех сервисов Amnezia?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="190"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="189"/>
<source>All users whom you shared a connection with will no longer be able to connect to it.</source>
<translation>Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="196"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="195"/>
<source>Cannot clear server from Amnezia software during active connection</source>
<translation>Невозможно очистить сервер от сервисов Amnezia во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="214"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="213"/>
<source>Reset API config</source>
<translation>Сбросить конфигурацию API</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="218"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="217"/>
<source>Do you want to reset API config?</source>
<translation>Вы хотите сбросить конфигурацию API?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="225"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="224"/>
<source>Cannot reset API config during active connection</source>
<translation>Невозможно сбросить конфигурацию API во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="244"/>
<source>Switch to the new Amnezia Premium subscription</source>
<translation>Перейти на новый тип подписки Amnezia Premium</translation>
<translation type="vanished">Перейти на новый тип подписки Amnezia Premium</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="155"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="154"/>
<source>Remove server from application</source>
<translation>Удалить сервер из приложения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="160"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="159"/>
<source>All installed AmneziaVPN services will still remain on the server.</source>
<translation>Все установленные сервисы и протоколы Amnezia останутся на сервере.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="185"/>
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="184"/>
<source>Clear server from Amnezia software</source>
<translation>Очистить сервер от протоколов и сервисов Amnezia</translation>
</message>
@@ -3128,27 +3098,27 @@ Thank you for staying with us!</source>
<context>
<name>PageSetupWizardApiServiceInfo</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="143"/>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="139"/>
<source>For the region</source>
<translation>Для региона</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="152"/>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="148"/>
<source>Price</source>
<translation>Цена</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="161"/>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="157"/>
<source>Work period</source>
<translation>Период работы</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="170"/>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="166"/>
<source>Speed</source>
<translation>Скорость</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="179"/>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="175"/>
<source>Features</source>
<translation>Особенности</translation>
</message>
@@ -4461,17 +4431,17 @@ Thank you for staying with us!</source>
<translation>IPsec</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="138"/>
<location filename="../containers/containers_defs.cpp" line="139"/>
<source>IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2/IPsec современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. Он имеет встроенную поддержку в последних версиях Android и iOS.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="145"/>
<location filename="../containers/containers_defs.cpp" line="146"/>
<source>Create a file vault on your server to securely store and transfer files.</source>
<translation>Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="233"/>
<location filename="../containers/containers_defs.cpp" line="234"/>
<source>DNS Service</source>
<translation>Сервис DNS</translation>
</message>
@@ -4482,7 +4452,7 @@ Thank you for staying with us!</source>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="110"/>
<location filename="../containers/containers_defs.cpp" line="232"/>
<location filename="../containers/containers_defs.cpp" line="233"/>
<source>Website in Tor network</source>
<translation>Веб-сайт в сети Tor</translation>
</message>
@@ -4512,22 +4482,23 @@ Thank you for staying with us!</source>
<translation>WireGuard популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="132"/>
<location filename="../containers/containers_defs.cpp" line="130"/>
<location filename="../containers/containers_defs.cpp" line="133"/>
<source>AmneziaWG is a special protocol from Amnezia based on WireGuard. It provides high connection speed and ensures stable operation even in the most challenging network conditions.</source>
<translation>AmneziaWG специальный протокол от Amnezia, основанный на WireGuard. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="135"/>
<location filename="../containers/containers_defs.cpp" line="136"/>
<source>XRay with REALITY masks VPN traffic as web traffic and protects against active probing. It is highly resistant to detection and offers high speed.</source>
<translation>XRay с REALITY маскирует VPN-трафик под веб-трафик. Обладает высокой устойчивостью к обнаружению и обеспечивает высокую скорость соединения.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="147"/>
<location filename="../containers/containers_defs.cpp" line="148"/>
<source></source>
<translation></translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="154"/>
<location filename="../containers/containers_defs.cpp" line="155"/>
<source>OpenVPN is one of the most popular and reliable VPN protocols. It uses SSL/TLS encryption, supports a wide variety of devices and operating systems, and is continuously improved by the community due to its open-source nature. It provides a good balance between speed and security but is easily recognized by DPI systems, making it susceptible to blocking.
Features:
@@ -4544,7 +4515,7 @@ Features:
* Работает по TCP и UDP</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="165"/>
<location filename="../containers/containers_defs.cpp" line="166"/>
<source>Shadowsocks is based on the SOCKS5 protocol and encrypts connections using AEAD cipher. Although designed to be discreet, it doesn&apos;t mimic a standard HTTPS connection and can be detected by some DPI systems. Due to limited support in Amnezia, we recommend using the AmneziaWG protocol.
Features:
@@ -4562,7 +4533,7 @@ Features:
* Работает по протоколу TCP</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="174"/>
<location filename="../containers/containers_defs.cpp" line="175"/>
<source>This combination includes the OpenVPN protocol and the Cloak plugin, specifically designed to protect against blocking.
OpenVPN securely encrypts all internet traffic between your device and the server.
@@ -4593,7 +4564,7 @@ OpenVPN надёжно шифрует весь интернет-трафик м
* Использует протокол TCP на порту 443</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="187"/>
<location filename="../containers/containers_defs.cpp" line="188"/>
<source>WireGuard is a modern, streamlined VPN protocol offering stable connectivity and excellent performance across all devices. It uses fixed encryption settings, delivering lower latency and higher data transfer speeds compared to OpenVPN. However, WireGuard is easily identifiable by DPI systems due to its distinctive packet signatures, making it susceptible to blocking.
Features:
@@ -4614,7 +4585,7 @@ Features:
* Работает по протоколу UDP</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="197"/>
<location filename="../containers/containers_defs.cpp" line="198"/>
<source>AmneziaWG is a modern VPN protocol based on WireGuard, combining simplified architecture with high performance across all devices. It addresses WireGuard&apos;s main vulnerability (easy detection by DPI systems) through advanced obfuscation techniques, making VPN traffic indistinguishable from regular internet traffic.
AmneziaWG is an excellent choice for those seeking a fast, stealthy VPN connection.
@@ -4637,7 +4608,7 @@ Features:
* Работает по протоколу UDP</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="209"/>
<location filename="../containers/containers_defs.cpp" line="210"/>
<source>REALITY is an innovative protocol developed by the creators of XRay, designed specifically to combat high levels of internet censorship. REALITY identifies censorship systems during the TLS handshake, redirecting suspicious traffic seamlessly to legitimate websites like google.com while providing genuine TLS certificates. This allows VPN traffic to blend indistinguishably with regular web traffic without special configuration.
Unlike older protocols such as VMess, VLESS, and XTLS-Vision, REALITY incorporates an advanced built-in &quot;friend-or-foe&quot; detection mechanism, effectively protecting against DPI and other traffic analysis methods.
@@ -4661,7 +4632,7 @@ REALITY распознаёт системы блокировки во время
* Работает по протоколу TCP</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="222"/>
<location filename="../containers/containers_defs.cpp" line="223"/>
<source>IKEv2, combined with IPSec encryption, is a modern and reliable VPN protocol. It reconnects quickly when switching networks or devices, making it ideal for dynamic network environments. While it provides good security and speed, it&apos;s easily recognized by DPI systems and susceptible to blocking.
Features:
@@ -4680,7 +4651,7 @@ Features:
* Работает по UDP (порты 500 и 4500)</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="235"/>
<location filename="../containers/containers_defs.cpp" line="236"/>
<source>After installation, Amnezia will create a
file storage on your server. You will be able to access it using
@@ -4726,12 +4697,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="141"/>
<location filename="../containers/containers_defs.cpp" line="142"/>
<source>Deploy a WordPress site on the Tor network in two clicks.</source>
<translation>Разверните сайт на WordPress в сети Tor в два клика.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="143"/>
<location filename="../containers/containers_defs.cpp" line="144"/>
<source>Replace the current DNS server with your own. This will increase your privacy level.</source>
<translation>Замените текущий DNS-сервер на свой собственный. Это повысит уровень вашей конфиденциальности.</translation>
</message>
@@ -4804,7 +4775,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<message>
<location filename="../protocols/protocols_defs.cpp" line="82"/>
<location filename="../containers/containers_defs.cpp" line="113"/>
<location filename="../containers/containers_defs.cpp" line="239"/>
<location filename="../containers/containers_defs.cpp" line="240"/>
<source>SOCKS5 proxy server</source>
<translation>Прокси-сервер SOCKS5</translation>
</message>
@@ -4951,7 +4922,6 @@ FileZilla или другие SFTP-клиенты, а также смонтир
</message>
<message>
<location filename="../ui/models/containers_model.cpp" line="34"/>
<location filename="../containers/containers_defs.cpp" line="130"/>
<source>AmneziaWG Legacy is a outdated version of AmneziaWG protocol. To upgrade, install AmneziaWG and recreate users.</source>
<translation>AmneziaWG Legacy является устаревшей версией протокола AmneziaWG. Для обновления установите AmneziaWG и пересоздайте пользователей.</translation>
</message>
@@ -4988,13 +4958,13 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>Settings</name>
<message>
<location filename="../settings.cpp" line="35"/>
<location filename="../settings.cpp" line="36"/>
<source>Server #1</source>
<translation>Сервер #1</translation>
</message>
<message>
<location filename="../settings.cpp" line="213"/>
<location filename="../settings.cpp" line="220"/>
<location filename="../settings.cpp" line="214"/>
<location filename="../settings.cpp" line="221"/>
<source>Server</source>
<translation>Сервер</translation>
</message>
@@ -5104,7 +5074,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>VpnConnection</name>
<message>
<location filename="../vpnconnection.cpp" line="535"/>
<location filename="../vpnconnection.cpp" line="537"/>
<source>Mbps</source>
<translation>Мбит/с</translation>
</message>
@@ -5155,12 +5125,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>amnezia::ContainerProps</name>
<message>
<location filename="../containers/containers_defs.cpp" line="364"/>
<location filename="../containers/containers_defs.cpp" line="365"/>
<source>Automatic</source>
<translation>Автоматическая</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="372"/>
<location filename="../containers/containers_defs.cpp" line="373"/>
<source>AmneziaWG protocol will be installed. It provides high connection speed and ensures stable operation even in the most challenging network conditions.</source>
<translation>Будет установлен протокол AmneziaWG. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях.</translation>
</message>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff