Compare commits

..

8 Commits

Author SHA1 Message Date
svamnezia
7e6f029e18 cleanup 2026-04-28 17:58:36 +03:00
svamnezia
4f8e1da751 dns/http separation 2026-04-28 17:51:59 +03:00
svamnezia
eb1188ccd7 env upd 2026-04-17 13:56:06 +03:00
svamnezia
10289fdb26 Remove unnecessary changes in translations 2026-04-16 18:30:01 +03:00
svamnezia
511ce6f62a Update 2026-04-16 00:19:03 +03:00
svamnezia
3a7d285e55 DNS update 2026-02-05 06:46:18 +03:00
svamnezia
6123268846 Remove unnecessary changes in translations 2026-02-02 04:39:22 +03:00
SvyatoslavVer
49d111d1fe DNS 2026-01-20 17:37:22 +03:00
98 changed files with 4015 additions and 2125 deletions

View File

@@ -10,10 +10,10 @@ env:
jobs:
Build-Linux-Ubuntu:
runs-on: android-runner
runs-on: 4-core
env:
QT_VERSION: 6.10.1
QT_VERSION: 6.8.3
QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -58,7 +58,7 @@ jobs:
- name: 'Build project'
run: |
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
sudo apt-get install libxkbcommon-x11-0
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
bash deploy/build_linux.sh
@@ -537,7 +537,7 @@ jobs:
# ------------------------------------------------------
Build-Android:
runs-on: android-runner
runs-on: 4-core
env:
ANDROID_BUILD_PLATFORM: android-36

View File

@@ -24,7 +24,7 @@ jobs:
- name: Verify git tag
run: |
TAG_NAME=${{ inputs.RELEASE_VERSION }}
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
else

7
.gitignore vendored
View File

@@ -1,5 +1,9 @@
# User settings
*.user
# Gateway configs (contains sensitive endpoints)
gateway.json
client/gateway.json
macOSPackage/
AmneziaVPN.dmg
AmneziaVPN.exe
@@ -140,6 +144,3 @@ ios-ne-build.sh
macos-ne-build.sh
macos-signed-build.sh
macos-with-sign-build.sh
DeveloperIdApplicationCertificate.p12
DeveloperIdInstallerCertificate.p12

4
.gitmodules vendored
View File

@@ -14,7 +14,3 @@
[submodule "client/3rd/QSimpleCrypto"]
path = client/3rd/QSimpleCrypto
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
[submodule "client/3rd/qtgamepad"]
path = client/3rd/qtgamepad
url = https://github.com/amnezia-vpn/qtgamepad.git
branch = 6.6

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.13.0)
set(AMNEZIAVPN_VERSION 4.8.12.8)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2106)
set(APP_ANDROID_VERSION_CODE 2104)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

Submodule client/3rd/qtgamepad deleted from f72b3e0c62

View File

@@ -33,6 +33,21 @@ add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
add_definitions(-DAGW_DNS_SERVER="$ENV{AGW_DNS_SERVER}")
add_definitions(-DAGW_DNS_DOMAIN="$ENV{AGW_DNS_DOMAIN}")
add_definitions(-DAGW_DNS_PRIMARY="$ENV{AGW_DNS_PRIMARY}")
add_definitions(-DAGW_DNS_PORT_UDP="$ENV{AGW_DNS_PORT_UDP}")
add_definitions(-DAGW_DNS_PORT_DOT="$ENV{AGW_DNS_PORT_DOT}")
add_definitions(-DAGW_DNS_PORT_DOH="$ENV{AGW_DNS_PORT_DOH}")
add_definitions(-DAGW_DNS_PORT_DOQ="$ENV{AGW_DNS_PORT_DOQ}")
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()
@@ -59,6 +74,7 @@ target_include_directories(${PROJECT} PUBLIC
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
endif()
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
@@ -227,13 +243,10 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
qt_finalize_target(${PROJECT})
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
if(COMMAND qt_import_qml_plugins)
qt_import_qml_plugins(${PROJECT})
endif()
if(COMMAND qt_finalize_executable)
qt_finalize_executable(${PROJECT})
else()
qt_finalize_target(${PROJECT})
option(BUILD_TESTS "Build transport integration tests" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

View File

@@ -26,8 +26,6 @@ import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@@ -90,10 +88,6 @@ class AmneziaActivity : QtActivity() {
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private var isActivityResumed = false
private var hasWindowFocus = false
private val resumeHandler = Handler(Looper.getMainLooper())
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -266,10 +260,6 @@ class AmneziaActivity : QtActivity() {
}
override fun onStop() {
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity stops
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
mainScope.launch {
@@ -281,91 +271,35 @@ class AmneziaActivity : QtActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) {
resumeHandler.removeCallbacksAndMessages(null)
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val deviceId = event.deviceId
val keyCode = event.keyCode
val pressed = event.action == KeyEvent.ACTION_DOWN
val source = event.source
if (deviceId < 0 && pressed) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER -> {
nativeGamepadKeyEvent(0, keyCode, true)
nativeGamepadKeyEvent(0, keyCode, false)
return true
}
}
}
// Real gamepad events (deviceId >= 0)
if (deviceId >= 0) {
val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
if (isGamepad || isJoystick || isDpad) {
nativeGamepadKeyEvent(deviceId, keyCode, pressed)
return true
}
}
return super.dispatchKeyEvent(event)
}
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() {
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Pause Amnezia activity")
}
override fun onResume() {
super.onResume()
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
resumeHandler.postDelayed({
// Check if activity is still resumed and has focus before executing
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
postDelayed({
sendTouch(1f, 1f)
}, 100)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
}
postDelayed({
sendTouch(2f, 2f)
}, 200)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
requestLayout()
invalidate()
}
postDelayed({
requestLayout()
invalidate()
}, 250)
}
}
} */
Log.d(TAG, "Resume Amnezia activity")
}
private fun configureWindowForEdgeToEdge() {
@@ -428,10 +362,6 @@ class AmneziaActivity : QtActivity() {
}
override fun onDestroy() {
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity is destroyed
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null

View File

@@ -1,10 +1,7 @@
package org.amnezia.vpn
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@@ -14,25 +11,7 @@ private const val TAG = "TvFilePicker"
class TvFilePicker : ComponentActivity() {
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
if (activitiesToResolveIntent.all {
val name = it.activityInfo.packageName
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
}) {
throw ActivityNotFoundException()
}
return intent
}
}) {
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
setResult(RESULT_OK, Intent().apply { data = it })
finish()
}
@@ -52,7 +31,7 @@ class TvFilePicker : ComponentActivity() {
private fun getFile() {
try {
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch(arrayOf("*/*"))
fileChooseResultLauncher.launch("*/*")
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })

View File

@@ -83,26 +83,6 @@ add_compile_definitions(_WINSOCKAPI_)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_QT6 ON)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
if(ANDROID)
# Use qtgamepad from amnezia-vpn/qtgamepad repository
# Only if Qt6CorePrivate is available (required by qtgamepad)
find_package(Qt6CorePrivate CONFIG QUIET)
if(Qt6CorePrivate_FOUND)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
# Link both the C++ module and QML plugin
if(TARGET GamepadLegacy)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
endif()
if(TARGET GamepadLegacyQuickPrivate)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
endif()
message(STATUS "Gamepad support enabled for Android")
else()
message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.")
endif()
endif()
set(LIBS ${LIBS} qt6keychain)
include_directories(

View File

@@ -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
@@ -181,6 +192,7 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/ipcclient.h
${CLIENT_ROOT_DIR}/core/privileged_process.h
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
@@ -193,6 +205,7 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
${CLIENT_ROOT_DIR}/core/privileged_process.cpp
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp

View File

@@ -79,7 +79,7 @@ namespace apiDefs
constexpr QLatin1String adEndpoint("ad_endpoint");
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
const int requestTimeoutMsecs = 30 * 1000; // 30 secs (increased for DNS transport testing)
}
#endif // APIDEFS_H

View File

@@ -1,29 +1,22 @@
#include "gatewayController.h"
#include <algorithm>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPromise>
#include <QUrl>
#include <QMutexLocker>
#include <QSharedPointer>
#include <QThread>
#include <QtConcurrent>
#include "QBlockCipher.h"
#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
@@ -36,652 +29,330 @@ 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.");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
QStringList splitUrls(const QString &urls)
amnezia::transport::dns::DnsProtocol dnsProtocolFromPrimary(PrimaryTransport p)
{
QStringList parsedUrls = urls.split(",", Qt::SkipEmptyParts);
for (QString &url : parsedUrls) {
url = url.trimmed();
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;
}
parsedUrls.removeAll("");
return parsedUrls;
}
} // namespace
TransportsConfig TransportsConfig::fromJson(const QJsonObject &json)
{
using amnezia::transport::dns::DnsProtocol;
TransportsConfig config;
QString primaryStr = json.value("primary").toString("http").toLower();
if (primaryStr == "http") {
config.primary = PrimaryTransport::Http;
} else if (primaryStr == "dns_udp" || primaryStr == "udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "dns_tcp" || primaryStr == "tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dns_dot" || primaryStr == "dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "dns_doh" || primaryStr == "doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "dns_doq" || primaryStr == "doq") {
config.primary = PrimaryTransport::DnsDoq;
}
config.retryCount = json.value("retry_count").toInt(3);
config.timeoutMs = json.value("timeout_ms").toInt(10000);
if (json.contains("http")) {
QJsonObject httpObj = json["http"].toObject();
config.httpEnabled = httpObj.value("enabled").toBool(true);
config.httpEndpoint = httpObj.value("endpoint").toString();
}
if (json.contains("dns_transports")) {
QJsonArray transportsArray = json["dns_transports"].toArray();
for (const auto &transportVal : transportsArray) {
QJsonObject transportObj = transportVal.toObject();
DnsTransportEntry entry;
entry.server = transportObj.value("server").toString();
entry.domain = transportObj.value("domain").toString();
entry.port = static_cast<quint16>(transportObj.value("port").toInt(15353));
entry.dohPath = transportObj.value("path").toString("/dns-query");
QString typeStr = transportObj.value("type").toString().toLower();
if (typeStr == "udp") {
entry.type = DnsProtocol::Udp;
} else if (typeStr == "tcp") {
entry.type = DnsProtocol::Tcp;
} else if (typeStr == "dot" || typeStr == "tls") {
entry.type = DnsProtocol::Tls;
if (!transportObj.contains("port")) entry.port = 8853;
} else if (typeStr == "doh" || typeStr == "https") {
entry.type = DnsProtocol::Https;
if (!transportObj.contains("port")) entry.port = 443;
} else if (typeStr == "doq" || typeStr == "quic") {
entry.type = DnsProtocol::Quic;
if (!transportObj.contains("port")) entry.port = 8853;
} else {
continue;
}
if (entry.isValid()) {
config.dnsTransports.append(entry);
}
}
}
return config;
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, const QString &proxyStorageOverride,
const QString &proxyUrlOverride, QObject *parent)
GatewayController::GatewayController(const QString &gatewayEndpoint,
const bool isDevEnvironment,
const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled,
QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_proxyStorageOverride(proxyStorageOverride),
m_proxyUrlOverride(proxyUrlOverride),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
{
auto httpTransport = std::make_shared<amnezia::transport::HttpGatewayTransport>(
m_gatewayEndpoint, m_isDevEnvironment, m_requestTimeoutMsecs, m_isStrictKillSwitchEnabled);
{
QMutexLocker lock(&m_transportMutex);
m_transport = std::move(httpTransport);
}
}
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::buildTransport(
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled)
{
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
using namespace amnezia::transport;
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
auto makeHttp = [&](const QString &httpEndpoint) {
return std::make_shared<HttpGatewayTransport>(
httpEndpoint, isDevEnvironment, requestTimeoutMsecs, isStrictKillSwitchEnabled);
};
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());
QString selectedProxyUrl = m_proxyUrlOverride.isEmpty() ? m_proxyUrl : m_proxyUrlOverride;
encRequestData.request.setUrl(endpoint.arg(selectedProxyUrl.isEmpty() ? m_gatewayEndpoint : selectedProxyUrl));
if (config.primary == PrimaryTransport::Http) {
return makeHttp(config.httpEndpoint);
}
// 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<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue())
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
});
const auto wantedProtocol = dnsProtocolFromPrimary(config.primary);
for (const auto &entry : config.dnsTransports) {
if (entry.type == wantedProtocol && entry.isValid()) {
return std::make_shared<DnsGatewayTransport>(
entry.type, entry.server, entry.domain, entry.port,
requestTimeoutMsecs, isStrictKillSwitchEnabled, entry.dohPath);
}
}
#endif
return makeHttp(config.httpEndpoint);
}
void GatewayController::setTransportsConfig(const TransportsConfig &config)
{
if (config.timeoutMs > 0) {
m_requestTimeoutMsecs = 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 = 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).trimmed().toLower();
if (primaryStr == "udp" || primaryStr == "dns_udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "tcp" || primaryStr == "dns_tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dot" || primaryStr == "dns_dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "doh" || primaryStr == "dns_doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "doq" || primaryStr == "dns_doq") {
config.primary = PrimaryTransport::DnsDoq;
} else {
config.primary = PrimaryTransport::Http;
}
int retryCount = QString(AGW_DNS_RETRY_COUNT).trimmed().toInt();
config.retryCount = (retryCount > 0) ? retryCount : 3;
int timeoutMs = QString(AGW_DNS_TIMEOUT_MS).trimmed().toInt();
config.timeoutMs = (timeoutMs > 0) ? timeoutMs : 10000;
config.httpEnabled = true;
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).trimmed().toUShort();
entry.port = (port > 0) ? port : defaultPort;
if (!dohPath.isEmpty()) entry.dohPath = dohPath;
config.dnsTransports.append(entry);
};
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).trimmed();
if (dohPath.isEmpty()) dohPath = "/dns-query";
addTransport(DnsProtocol::Https, AGW_DNS_PORT_DOH, 443, dohPath);
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<int>(config.primary);
return config;
}
GatewayController::EncryptedRequest GatewayController::encryptRequest(const QJsonObject &apiPayload)
{
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;
QSimpleCrypto::QRsa rsa;
rsaKey = rsaKey.trimmed();
rsaKey.replace("\\n", "\n");
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<amnezia::transport::IGatewayTransport> 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<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &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<QSslError> &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<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::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<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &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<QSslError> &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 = m_proxyStorageOverride.isEmpty() ? splitUrls(DEV_S3_ENDPOINT) : splitUrls(m_proxyStorageOverride);
} else {
baseUrls = splitUrls(PROD_S3_ENDPOINT);
}
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<QSslError> &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<QSslError> sslErrors;
QNetworkReply *reply;
QStringList baseUrls;
if (m_isDevEnvironment) {
baseUrls = m_proxyStorageOverride.isEmpty() ? splitUrls(DEV_S3_ENDPOINT) : splitUrls(m_proxyStorageOverride);
} else {
baseUrls = splitUrls(PROD_S3_ENDPOINT);
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<QSslError> &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<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &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<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> 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<QSslError> &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);
};
QString selectedProxyUrl = m_proxyUrlOverride.isEmpty() ? m_proxyUrl : m_proxyUrlOverride;
if (selectedProxyUrl.isEmpty()) {
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> 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<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
m_proxyUrl = proxyUrl;
if (!m_proxyUrl.isEmpty()) {
selectedProxyUrl = m_proxyUrl;
break;
}
} else {
reply->deleteLater();
}
}
}
if (!selectedProxyUrl.isEmpty()) {
if (bypassFunction(endpoint, selectedProxyUrl, 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<void(const QStringList &)> onComplete)
QFuture<QPair<amnezia::ErrorCode, QByteArray>> 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<QSslError> &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<void(const QString &)> onComplete)
{
if (!m_proxyUrlOverride.isEmpty()) {
onComplete(m_proxyUrlOverride);
return;
}
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<QSslError> &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<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::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<QSslError> &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);
});
}

View File

@@ -2,72 +2,87 @@
#define GATEWAYCONTROLLER_H
#include <QFuture>
#include <QNetworkReply>
#include <QJsonArray>
#include <QJsonObject>
#include <QMutex>
#include <QObject>
#include <QPair>
#include <QPromise>
#include <QSharedPointer>
#include <memory>
#include "core/defs.h"
#include "core/transport/dns/dnsResolver.h"
#include "core/transport/igatewaytransport.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
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(); }
};
enum class PrimaryTransport { Http, DnsUdp, DnsTcp, DnsDot, DnsDoh, DnsDoq };
struct TransportsConfig
{
PrimaryTransport primary = PrimaryTransport::Http;
bool httpEnabled = true;
QString httpEndpoint;
QList<DnsTransportEntry> dnsTransports;
int retryCount = 3;
int timeoutMs = 10000;
bool isValid() const { return httpEnabled || !dnsTransports.isEmpty(); }
static TransportsConfig fromJson(const QJsonObject &json);
};
class GatewayController : public QObject
{
Q_OBJECT
public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, const QString &proxyStorageOverride = "",
const QString &proxyUrlOverride = "", 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<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
static TransportsConfig buildTransportsConfig();
void setTransportsConfig(const TransportsConfig &config);
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);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
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<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
std::shared_ptr<amnezia::transport::IGatewayTransport> currentTransport() const;
static std::shared_ptr<amnezia::transport::IGatewayTransport> buildTransport(
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
QString m_proxyStorageOverride;
QString m_proxyUrlOverride;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
inline static QString m_proxyUrl;
mutable QMutex m_transportMutex;
std::shared_ptr<amnezia::transport::IGatewayTransport> m_transport;
};
#endif // GATEWAYCONTROLLER_H

View File

@@ -419,18 +419,6 @@ ErrorCode ServerController::installDockerWorker(const ServerCredentials &credent
cbReadStdOut, cbReadStdErr);
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
return ErrorCode::ServerLinuxKernelTooOld;
}
}
}
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))

View File

@@ -61,7 +61,6 @@ namespace amnezia
ServerDockerOnCgroupsV2 = 211,
ServerCgroupMountpoint = 212,
DockerPullRateLimit = 213,
ServerLinuxKernelTooOld = 214,
// Ssh connection errors
SshRequestDeniedError = 300,

View File

@@ -29,7 +29,6 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;

View File

@@ -7,6 +7,7 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent)
{
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
m_tun2socks.reset(m_node.acquire<IpcProcessTun2SocksReplica>());
}
IpcClient& IpcClient::Instance()
@@ -32,43 +33,68 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
return rep;
}
QSharedPointer<IpcProcessInterfaceReplica> IpcClient::CreatePrivilegedProcess()
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
{
return withInterface([](QSharedPointer<IpcInterfaceReplica> &iface) -> QSharedPointer<IpcProcessInterfaceReplica> {
auto createPrivilegedProcess = iface->createPrivilegedProcess();
if (!createPrivilegedProcess.waitForFinished()) {
qCritical() << "Failed to create privileged process";
return nullptr;
}
const int pid = createPrivilegedProcess.returnValue();
auto* node = new QRemoteObjectNode();
node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid))));
QSharedPointer<IpcProcessInterfaceReplica> rep(
node->acquire<IpcProcessInterfaceReplica>(),
[node] (IpcProcessInterfaceReplica *ptr) {
delete ptr;
node->deleteLater();
}
);
if (rep.isNull()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica";
return nullptr;
}
if (!rep->waitForSource()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica";
return nullptr;
}
if (!rep->isReplicaValid()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid";
return nullptr;
}
return rep;
},
[]() -> QSharedPointer<IpcProcessInterfaceReplica> {
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
if (rep.isNull()) {
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
return nullptr;
});
}
if (!rep->waitForSource(1000)) {
qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica";
return nullptr;
}
if (!rep->isReplicaValid()) {
qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid";
}
return rep;
}
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
{
QSharedPointer<IpcInterfaceReplica> rep = Interface();
if (!rep) {
qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid";
return nullptr;
}
QRemoteObjectPendingReply<int> pidReply = rep->createPrivilegedProcess();
if (!pidReply.waitForFinished(5000)){
qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call";
return nullptr;
}
int pid = pidReply.returnValue();
QSharedPointer<ProcessDescriptor> pd(new ProcessDescriptor());
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() {
pd->replicaNode->addClientSideConnection(pd->localSocket.data());
IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire<IpcProcessInterfaceReplica>();
// TODO: rework the unsafe cast below
PrivilegedProcess *priv = static_cast<PrivilegedProcess *>(repl);
pd->ipcProcess.reset(priv);
if (!pd->ipcProcess) {
qWarning() << "Acquire PrivilegedProcess failed";
} else {
pd->ipcProcess->waitForSource(1000);
if (!pd->ipcProcess->isReplicaValid()) {
qWarning() << "PrivilegedProcess replica is not connected!";
}
QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(),
[pd]() { pd->replicaNode->deleteLater(); });
}
});
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
if (!pd->localSocket->waitForConnected()) {
qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket";
return nullptr;
}
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
return processReplica;
}

View File

@@ -5,7 +5,9 @@
#include <QObject>
#include "rep_ipc_interface_replica.h"
#include "rep_ipc_process_interface_replica.h"
#include "rep_ipc_process_tun2socks_replica.h"
#include "privileged_process.h"
class IpcClient : public QObject
{
@@ -16,7 +18,8 @@ public:
static IpcClient& Instance();
static QSharedPointer<IpcInterfaceReplica> Interface();
static QSharedPointer<IpcProcessInterfaceReplica> CreatePrivilegedProcess();
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
template <typename Func>
static auto withInterface(Func func)
@@ -51,6 +54,18 @@ signals:
private:
QRemoteObjectNode m_node;
QSharedPointer<IpcInterfaceReplica> m_interface;
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
struct ProcessDescriptor {
ProcessDescriptor () {
replicaNode = QSharedPointer<QRemoteObjectNode>(new QRemoteObjectNode());
ipcProcess = QSharedPointer<PrivilegedProcess>();
localSocket = QSharedPointer<QLocalSocket>();
}
QSharedPointer<PrivilegedProcess> ipcProcess;
QSharedPointer<QRemoteObjectNode> replicaNode;
QSharedPointer<QLocalSocket> localSocket;
};
};
#endif // IPCCLIENT_H

View File

@@ -44,6 +44,7 @@
#include <QHostAddress>
#include <QHostInfo>
#include <QDebug>
QRegularExpression NetworkUtilities::ipAddressRegExp()
{

View File

@@ -0,0 +1,27 @@
#include "privileged_process.h"
PrivilegedProcess::PrivilegedProcess() :
IpcProcessInterfaceReplica()
{
}
PrivilegedProcess::~PrivilegedProcess()
{
qDebug() << "PrivilegedProcess::~PrivilegedProcess()";
}
void PrivilegedProcess::waitForFinished(int msecs)
{
QSharedPointer<QEventLoop> loop(new QEventLoop);
connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{
loop->quit();
loop.clear();
});
QTimer::singleShot(msecs, this, [this, loop]() mutable {
loop->quit();
loop.clear();
});
loop->exec();
}

View File

@@ -0,0 +1,24 @@
#ifndef PRIVILEGED_PROCESS_H
#define PRIVILEGED_PROCESS_H
#include <QObject>
#include "rep_ipc_process_interface_replica.h"
// This class is dangerous - instance of this class casted from base class,
// so it support only functions
// Do not add any members into it
//
class PrivilegedProcess : public IpcProcessInterfaceReplica
{
Q_OBJECT
public:
PrivilegedProcess();
~PrivilegedProcess() override;
void waitForFinished(int msecs);
};
#endif // PRIVILEGED_PROCESS_H

View File

@@ -0,0 +1,153 @@
#include "dnsPacket_p.h"
#include <QHostInfo>
#include <cstring>
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<char>(part.length()));
result.append(part.toUtf8());
}
result.append(static_cast<char>(0));
return result;
}
QByteArray buildDnsQuery(const QString &hostname, quint16 transactionId)
{
QByteArray packet;
DnsHeader header;
header.id = qToBigEndian(transactionId);
header.flags = qToBigEndian<quint16>(0x0100);
header.qdcount = qToBigEndian<quint16>(1);
header.ancount = 0;
header.nscount = 0;
header.arcount = 0;
packet.append(reinterpret_cast<const char *>(&header), sizeof(DnsHeader));
const QByteArray qname = encodeDnsName(hostname);
if (qname.isEmpty()) {
return QByteArray();
}
packet.append(qname);
quint16 qtype = qToBigEndian<quint16>(DNS_TYPE_A);
packet.append(reinterpret_cast<const char *>(&qtype), sizeof(quint16));
quint16 qclass = qToBigEndian<quint16>(DNS_CLASS_IN);
packet.append(reinterpret_cast<const char *>(&qclass), sizeof(quint16));
return packet;
}
QString parseDnsResponse(const QByteArray &response, bool isTcp)
{
if (response.size() < static_cast<int>(sizeof(DnsHeader))) {
return QString();
}
int offset = isTcp ? 2 : 0;
if (response.size() < offset + static_cast<int>(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<quint8>(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<quint8>(response.at(offset));
if ((nameByte & 0xC0) == 0xC0) {
offset += 2;
} else {
while (offset < response.size() && response.at(offset) != 0) {
const quint8 length = static_cast<quint8>(response.at(offset));
if (length > 63) {
return QString();
}
offset += length + 1;
}
offset++;
}
if (offset + 10 > response.size()) {
break;
}
const quint16 type =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(response.constData() + offset));
offset += 2;
offset += 2;
offset += 4;
const quint16 rdlength =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(response.constData() + offset));
offset += 2;
if (type == DNS_TYPE_A && rdlength == 4) {
if (offset + 4 > response.size()) {
break;
}
QHostAddress ip;
ip.setAddress(
qFromBigEndian<quint32>(*reinterpret_cast<const quint32 *>(response.constData() + offset)));
return ip.toString();
}
offset += rdlength;
}
return QString();
}
} // namespace amnezia::transport::dns::detail

View File

@@ -0,0 +1,38 @@
#ifndef DNSPACKET_P_H
#define DNSPACKET_P_H
#include <QByteArray>
#include <QHostAddress>
#include <QString>
#include <QtEndian>
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

View File

@@ -0,0 +1,354 @@
#include "dnsResolver.h"
#include "dnsPacket_p.h"
#include <QDateTime>
#include <QEventLoop>
#include <QHostAddress>
#include <QNetworkAccessManager>
#include <QNetworkDatagram>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslSocket>
#include <QTcpSocket>
#include <QTimer>
#include <QUdpSocket>
#include <QUrl>
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<quint16>(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<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
socket.close();
return QString();
}
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tcpQuery;
tcpQuery.append(reinterpret_cast<const char *>(&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<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();
}
}
}
});
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<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
socket.close();
return QString();
}
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tlsQuery;
tlsQuery.append(reinterpret_cast<const char *>(&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<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();
}
}
}
});
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<quint16>(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<quint16>(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

View File

@@ -0,0 +1,29 @@
#ifndef DNSRESOLVER_H
#define DNSRESOLVER_H
#include <QString>
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

View File

@@ -0,0 +1,817 @@
#include "dnsTunnel.h"
#include "dnsPacket_p.h"
#include <QDateTime>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QHostAddress>
#include <QList>
#include <QMap>
#include <QNetworkAccessManager>
#include <QNetworkDatagram>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSharedPointer>
#include <QSslError>
#include <QSslSocket>
#include <QStringList>
#include <QTcpSocket>
#include <QThread>
#include <QTimer>
#include <QUdpSocket>
#include <QUrl>
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<char>((value >> 8) & 0xFF));
data.append(static_cast<char>(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<char>(labelBytes.size()));
query.append(labelBytes);
}
query.append(static_cast<char>(0));
appendUint16BE(query, 16);
appendUint16BE(query, 1);
const quint16 optionDataLen = 4 + 18;
query.append(static_cast<char>(0));
appendUint16BE(query, 41);
appendUint16BE(query, 4096);
query.append(static_cast<char>(0));
query.append(static_cast<char>(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<const quint8 *>(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<const char *>(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<quint32>(data[pos + 20]) << 24)
| (static_cast<quint32>(data[pos + 21]) << 16)
| (static_cast<quint32>(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<char>(labelBytes.size()));
query.append(labelBytes);
}
query.append(static_cast<char>(0));
appendUint16BE(query, 16);
appendUint16BE(query, 1);
const QByteArray payloadBase64 = payload.toBase64();
const quint16 optionDataLen = 4 + payloadBase64.size();
query.append(static_cast<char>(0));
appendUint16BE(query, 41);
appendUint16BE(query, 4096);
query.append(static_cast<char>(0));
query.append(static_cast<char>(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<const uchar *>(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<const char *>(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<quint16>(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<int>(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<quint16>(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;
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tcpQuery;
tcpQuery.append(reinterpret_cast<const char *>(&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<quint16>(*reinterpret_cast<const quint16 *>(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<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
&socket, [&socket](const QList<QSslError> &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<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
if (query.isEmpty()) {
socket.close();
return QByteArray();
}
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tcpQuery;
tcpQuery.append(reinterpret_cast<const char *>(&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<quint16>(*reinterpret_cast<const quint16 *>(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<quint16>(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<QSslError> &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<int>(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<quint16>(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<int, QByteArray> chunks;
chunks[0] = firstTxtData;
auto requestChunksBatch = [&](const QList<int> &chunkIndices, int batchTimeout) {
if (chunkIndices.isEmpty()) return;
QList<QSharedPointer<QUdpSocket>> sockets;
QMap<QUdpSocket *, int> socketToIndex;
for (int idx : chunkIndices) {
if (chunks.contains(idx)) continue;
const quint16 chunkTxId =
static_cast<quint16>((QDateTime::currentMSecsSinceEpoch() + idx) & 0xFFFF);
const QByteArray chunkQuery =
buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, idx);
if (chunkQuery.isEmpty()) {
continue;
}
auto socket = QSharedPointer<QUdpSocket>::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<int> 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<int> batch = missing.mid(batchStart, MAX_CONCURRENT_REQUESTS);
requestChunksBatch(batch, batchTimeout);
}
}
QList<int> 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

View File

@@ -0,0 +1,35 @@
#ifndef DNSTUNNEL_H
#define DNSTUNNEL_H
#include <QByteArray>
#include <QString>
#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

View File

@@ -0,0 +1,157 @@
#include "dnsGatewayTransport.h"
#include <QDebug>
#include <QHostAddress>
#include <QHostInfo>
#include <QSharedPointer>
#include <QStringList>
#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<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> 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

View File

@@ -0,0 +1,49 @@
#ifndef DNSGATEWAYTRANSPORT_H
#define DNSGATEWAYTRANSPORT_H
#include <QString>
#include <atomic>
#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

View File

@@ -0,0 +1,345 @@
#include "httpGatewayTransport.h"
#include <algorithm>
#include <random>
#include <QCryptographicHash>
#include <QDebug>
#include <QEventLoop>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMutexLocker>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSharedPointer>
#include <QThread>
#include <QUrl>
#include <QUuid>
#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<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> 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<QSslError> &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

View File

@@ -0,0 +1,58 @@
#ifndef HTTPGATEWAYTRANSPORT_H
#define HTTPGATEWAYTRANSPORT_H
#include <QByteArray>
#include <QList>
#include <QMutex>
#include <QNetworkReply>
#include <QSslError>
#include <QString>
#include <QStringList>
#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<QSslError> 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

View File

@@ -0,0 +1,36 @@
#ifndef IGATEWAYTRANSPORT_H
#define IGATEWAYTRANSPORT_H
#include <QByteArray>
#include <QString>
#include <functional>
#include "core/defs.h"
namespace amnezia::transport
{
struct DecryptionResult
{
QByteArray decrypted;
bool isOk = false;
};
using DecryptionHook = std::function<DecryptionResult(const QByteArray &encrypted)>;
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

View File

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

View File

@@ -270,7 +270,12 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) {
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()) {
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));

View File

@@ -62,9 +62,6 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
}
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
if (m_splitTunnelManager == nullptr)
return;
if (config.m_vpnDisabledApps.length() > 0) {
m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex);
m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps);

View File

@@ -232,6 +232,12 @@ ErrorCode OpenVpnProtocol::start()
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->waitForSource(5000);
if (!m_openVpnProcess->isInitialized()) {
qWarning() << "IpcProcess replica is not connected!";
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->setProgram(PermittedProcess::OpenVPN);
QStringList arguments({
"--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort),
@@ -240,13 +246,13 @@ ErrorCode OpenVpnProtocol::start()
m_openVpnProcess->setArguments(arguments);
qDebug() << arguments.join(" ");
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred,
connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred,
[&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; });
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged,
connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged,
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this,
connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this,
[&]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
m_openVpnProcess->start();

View File

@@ -53,7 +53,7 @@ private:
void updateRouteGateway(QString line);
void updateVpnGateway(const QString &line);
QSharedPointer<IpcProcessInterfaceReplica> m_openVpnProcess;
QSharedPointer<PrivilegedProcess> m_openVpnProcess;
};
#endif // OPENVPNPROTOCOL_H

View File

@@ -233,7 +233,7 @@ namespace amnezia
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
constexpr char defaultSpecialJunk1[] = "<b 0x084481800001000300000000077469636b65747306776964676574096b696e6f706f69736b0272750000010001c00c0005000100000039001806776964676574077469636b6574730679616e646578c025c0390005000100000039002b1765787465726e616c2d7469636b6574732d776964676574066166697368610679616e646578036e657400c05d000100010000001c000457fafe25>";
constexpr char defaultSpecialJunk2[] = "";
constexpr char defaultSpecialJunk3[] = "";
constexpr char defaultSpecialJunk4[] = "";

View File

@@ -1,7 +1,6 @@
#include "xrayprotocol.h"
#include "core/ipcclient.h"
#include "ipc.h"
#include "utilities.h"
#include "core/networkUtilities.h"
@@ -10,37 +9,14 @@
#include <QJsonObject>
#include <QNetworkInterface>
#include <QJsonDocument>
#include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h>
#ifdef Q_OS_MACOS
static const QString tunName = "utun22";
#else
static const QString tunName = "tun2";
#endif
XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent)
{
readXrayConfiguration(configuration);
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr;
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::config_key::hostName).toString());
const QString primaryDns = configuration.value(amnezia::config_key::dns1).toString();
m_dnsServers.push_back(QHostAddress(primaryDns));
if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) {
const QString secondaryDns = configuration.value(amnezia::config_key::dns2).toString();
m_dnsServers.push_back(QHostAddress(secondaryDns));
}
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
}
m_xrayConfig = xrayConfiguration;
m_t2sProcess = IpcClient::InterfaceTun2Socks();
}
XrayProtocol::~XrayProtocol()
@@ -52,197 +28,117 @@ XrayProtocol::~XrayProtocol()
ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
setConnectionState(Vpn::ConnectionState::Connecting);
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
return startTun2Socks();
const ErrorCode err = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
return ErrorCode::NoError;
}, [] () {
return ErrorCode::AmneziaServiceConnectionFailed;
});
if (err != ErrorCode::NoError)
return err;
setConnectionState(Vpn::ConnectionState::Connecting);
return startTun2Sock();
}
ErrorCode XrayProtocol::startTun2Sock()
{
m_t2sProcess->start();
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this,
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) {
qDebug() << "PrivilegedProcess setConnectionState " << vpnState;
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (vpnState == Vpn::ConnectionState::Connected) {
setConnectionState(Vpn::ConnectionState::Connecting);
QList<QHostAddress> dnsAddr;
dnsAddr.push_back(QHostAddress(m_primaryDNS));
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) {
dnsAddr.push_back(QHostAddress(m_secondaryDNS));
}
#ifdef Q_OS_WIN
QThread::msleep(8000);
#endif
#ifdef Q_OS_MACOS
QThread::msleep(5000);
iface->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr);
iface->updateResolvers("utun22", dnsAddr);
#endif
#ifdef Q_OS_LINUX
QThread::msleep(1000);
iface->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr);
iface->updateResolvers("tun2", dnsAddr);
#endif
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
iface->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1");
}
iface->StopRoutingIpv6();
#ifdef Q_OS_WIN
iface->updateResolvers("tun2", dnsAddr);
#endif
setConnectionState(Vpn::ConnectionState::Connected);
}
#if !defined(Q_OS_MACOS)
if (vpnState == Vpn::ConnectionState::Disconnected) {
setConnectionState(Vpn::ConnectionState::Disconnected);
iface->deleteTun("tun2");
iface->StartRoutingIpv6();
iface->clearSavedRoutes();
}
#endif
});
});
return ErrorCode::NoError;
}
void XrayProtocol::stop()
{
qDebug() << "XrayProtocol::stop()";
setConnectionState(Vpn::ConnectionState::Disconnecting);
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
#ifdef AMNEZIA_DESKTOP
QRemoteObjectPendingReply<bool> StartRoutingIpv6Resp = iface->StartRoutingIpv6();
if (!StartRoutingIpv6Resp.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6";
}
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue())
qWarning() << "Failed to start routing ipv6";
QRemoteObjectPendingReply<bool> restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to restore resolvers";
}
auto restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue())
qWarning() << "Failed to restore resolvers";
auto deleteTun = iface->deleteTun(tunName);
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
#if !defined(Q_OS_MACOS)
QRemoteObjectPendingReply<bool> deleteTunResp = iface->deleteTun("tun2");
if (!deleteTunResp.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to delete tun";
}
#endif
#endif
iface->xrayStop();
});
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
}
#else
// terminate does not do anything useful on Windows
// so just kill the process
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
if (m_t2sProcess) {
m_t2sProcess->stop();
QThread::msleep(200);
}
setConnectionState(Vpn::ConnectionState::Disconnected);
}
ErrorCode XrayProtocol::startTun2Socks()
void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration)
{
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed;
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
}
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" });
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() {
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
if (!readAllStandardOutput.waitForFinished()) {
qWarning() << "Failed to read output from tun2socks";
return;
}
const QString line = readAllStandardOutput.returnValue();
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
setConnectionState(Vpn::ConnectionState::Connected);
}
}
}, Qt::QueuedConnection);
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
if (exitStatus == QProcess::ExitStatus::CrashExit) {
qCritical() << "Tun2socks process crashed!";
} else {
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
}
stop();
setLastError(ErrorCode::Tun2SockExecutableCrashed);
}, Qt::QueuedConnection);
m_tun2socksProcess->start();
return ErrorCode::NoError;
}
ErrorCode XrayProtocol::setupRouting() {
return IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
#ifdef Q_OS_WIN
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
#endif
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
if (!createTun.waitForFinished() || !createTun.returnValue()) {
qCritical() << "Failed to assign IP address for TUN";
return ErrorCode::InternalError;
}
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
qCritical() << "Failed to set DNS resolvers for TUN";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
int vpnAdapterIndex = -1;
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (auto& netInterface : netInterfaces) {
for (auto& address : netInterface.addressEntries()) {
if (m_vpnLocalAddress == address.ip().toString())
vpnAdapterIndex = netInterface.index();
}
}
#else
static const int vpnAdapterIndex = 0;
#endif
const bool killSwitchEnabled = QVariant(m_rawConfig.value(config_key::killSwitchOption).toString()).toBool();
if (killSwitchEnabled) {
if (vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("vpnServer", m_remoteAddress);
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
qCritical() << "Failed to enable killswitch";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
}
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
return ErrorCode::InternalError;
}
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("inetAdapterIndex", inetAdapterIndex);
config.insert("vpnAdapterIndex", vpnAdapterIndex);
config.insert("vpnGateway", m_vpnGateway);
config.insert("vpnServer", m_remoteAddress);
auto enablePeerTraffic = iface->enablePeerTraffic(config);
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
qCritical() << "Failed to enable peer traffic";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
#endif
return ErrorCode::NoError;
},
[] () {
return ErrorCode::AmneziaServiceConnectionFailed;
});
m_xrayConfig = xrayConfiguration;
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString();
m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString();
}

View File

@@ -6,7 +6,6 @@
#include "core/ipcclient.h"
#include "vpnprotocol.h"
#include "settings.h"
#include <QtCore/qsharedpointer.h>
class XrayProtocol : public VpnProtocol
{
@@ -15,18 +14,19 @@ public:
virtual ~XrayProtocol() override;
ErrorCode start() override;
ErrorCode startTun2Sock();
void stop() override;
private:
ErrorCode setupRouting();
ErrorCode startTun2Socks();
void readXrayConfiguration(const QJsonObject &configuration);
QJsonObject m_xrayConfig;
Settings::RouteMode m_routeMode;
QList<QHostAddress> m_dnsServers;
QString m_remoteAddress;
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
QString m_primaryDNS;
QString m_secondaryDNS;
#ifndef Q_OS_IOS
QSharedPointer<IpcProcessTun2SocksReplica> m_t2sProcess;
#endif
};
#endif // XRAYPROTOCOL_H

View File

@@ -129,7 +129,6 @@
<file>ui/qml/Components/AdLabel.qml</file>
<file>ui/qml/Components/ConnectButton.qml</file>
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
<file>ui/qml/Components/GamepadLoader.qml</file>
<file>ui/qml/Components/HomeContainersListView.qml</file>
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>

View File

@@ -21,5 +21,4 @@ if [ "$(systemctl is-active docker)" != "active" ]; then \
sleep 5; sudo systemctl start docker; sleep 5;\
fi;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
docker --version;\
uname -sr
docker --version

View File

@@ -14,7 +14,8 @@ namespace
const char cloudFlareNs1[] = "1.1.1.1";
const char cloudFlareNs2[] = "1.0.0.1";
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
//constexpr char gatewayEndpoint[] = "http://localhost:80/";
constexpr char gatewayEndpoint[] = "http://localhost:80/";
}
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
@@ -539,26 +540,6 @@ QString Settings::getGatewayEndpoint(bool isTestPurchase)
return isTestPurchase ? DEV_AGW_ENDPOINT : m_gatewayEndpoint;
}
QString Settings::getDevProxyStorageEndpoint() const
{
return value("Conf/devProxyStorageEndpoint", "").toString();
}
void Settings::setDevProxyStorageEndpoint(const QString &endpoint)
{
setValue("Conf/devProxyStorageEndpoint", endpoint);
}
QString Settings::getDevProxyUrl() const
{
return value("Conf/devProxyUrl", "").toString();
}
void Settings::setDevProxyUrl(const QString &url)
{
setValue("Conf/devProxyUrl", url);
}
bool Settings::isDevGatewayEnv(bool isTestPurchase)
{
return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();

View File

@@ -94,15 +94,6 @@ public:
setValue("Conf/startMinimized", enabled);
}
bool isNewsNotifications() const
{
return value("Conf/newsNotifications", true).toBool();
}
void setNewsNotifications(bool enabled)
{
setValue("Conf/newsNotifications", enabled);
}
bool isSaveLogs() const
{
return value("Conf/saveLogs", false).toBool();
@@ -233,10 +224,6 @@ public:
void setGatewayEndpoint(const QString &endpoint);
void setDevGatewayEndpoint();
QString getGatewayEndpoint(bool isTestPurchase = false);
QString getDevProxyStorageEndpoint() const;
void setDevProxyStorageEndpoint(const QString &endpoint);
QString getDevProxyUrl() const;
void setDevProxyUrl(const QString &url);
bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled);

View File

@@ -0,0 +1,81 @@
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}")
add_definitions(-DAGW_DNS_SERVER="$ENV{AGW_DNS_SERVER}")
add_definitions(-DAGW_DNS_DOMAIN="$ENV{AGW_DNS_DOMAIN}")
add_definitions(-DAGW_DNS_PRIMARY="$ENV{AGW_DNS_PRIMARY}")
add_definitions(-DAGW_DNS_PORT_UDP="$ENV{AGW_DNS_PORT_UDP}")
add_definitions(-DAGW_DNS_PORT_DOT="$ENV{AGW_DNS_PORT_DOT}")
add_definitions(-DAGW_DNS_PORT_DOH="$ENV{AGW_DNS_PORT_DOH}")
add_definitions(-DAGW_DNS_PORT_DOQ="$ENV{AGW_DNS_PORT_DOQ}")
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}")
qt_add_executable(${PROJECT_NAME}
tst_transports.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
${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
${CLIENT_ROOT_DIR}/core/transport
${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()
add_test(NAME TransportTest COMMAND ${PROJECT_NAME})

View File

@@ -0,0 +1,406 @@
#include <QCoreApplication>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QHostAddress>
#include <QHostInfo>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslConfiguration>
#include <QSslError>
#include <QTest>
#include <QUrl>
#include "transport/dns/dnsResolver.h"
#include "transport/dns/dnsTunnel.h"
#include "QBlockCipher.h"
#include "QRsa.h"
#include <openssl/evp.h>
#include <openssl/rsa.h>
using amnezia::transport::dns::DnsProtocol;
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;
DnsProtocol type;
QString server;
QString domain;
quint16 port;
QString dohPath;
};
QList<DnsEntry> dnsTransports;
int timeoutMs = 15000;
};
static TestConfig buildConfigFromEnv()
{
TestConfig cfg;
QString server(AGW_DNS_SERVER);
QString domain(AGW_DNS_DOMAIN);
cfg.httpEndpoint = QString(DEV_AGW_PUBLIC_KEY).isEmpty()
? QString() : QString("http://%1/").arg(server);
int timeout = QString(AGW_DNS_TIMEOUT_MS).toInt();
cfg.timeoutMs = (timeout > 0) ? timeout : 15000;
if (server.isEmpty() || domain.isEmpty()) return cfg;
auto addEntry = [&](DnsProtocol type, const QString &name,
const char *portDefine, quint16 defaultPort, const QString &dohPath = QString()) {
TestConfig::DnsEntry e;
e.type = type;
e.name = name;
e.server = server;
e.domain = domain;
quint16 port = QString(portDefine).toUShort();
e.port = (port > 0) ? port : defaultPort;
if (!dohPath.isEmpty()) e.dohPath = dohPath;
cfg.dnsTransports.append(e);
};
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(DnsProtocol::Https, "DoH", AGW_DNS_PORT_DOH, 443, dohPath);
addEntry(DnsProtocol::Quic, "DoQ", AGW_DNS_PORT_DOQ, 8853);
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;
}
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 == DnsProtocol::Https || entry.type == DnsProtocol::Tls);
QString serverAddr = needsHostname ? entry.server : resolvedIp;
r.responseBody = amnezia::transport::dns::DnsTunnel::send(
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()
{
m_config = buildConfigFromEnv();
QVERIFY2(!m_config.dnsTransports.isEmpty(),
"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();
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";
}
}
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 == DnsProtocol::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;
}
}
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 == DnsProtocol::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;
}
}
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

View File

@@ -5,6 +5,7 @@
#include "core/api/apiDefs.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/networkUtilities.h"
#include "core/qrCodeUtils.h"
#include "ui/controllers/systemController.h"
#include "version.h"
@@ -382,51 +383,6 @@ bool ApiConfigsController::fillAvailableServices()
}
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
#if defined(Q_OS_IOS) || defined(MACOS_NE)
QEventLoop waitProducts;
bool productsFetched = false;
QString productPrice;
QString productCurrency;
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
[&](const QList<QVariantMap> &products,
const QStringList &invalidIds,
const QString &errorString) {
if (!errorString.isEmpty() || products.isEmpty()) {
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
} else {
const auto &product = products.first();
productPrice = product.value("price").toString();
productCurrency = product.value("currencyCode").toString();
productsFetched = true;
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
}
waitProducts.quit();
});
waitProducts.exec();
if (productsFetched && !productPrice.isEmpty()) {
QJsonArray services = data.value("services").toArray();
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services[i].toObject();
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
QString formattedPrice = productPrice;
if (!productCurrency.isEmpty()) {
formattedPrice += " " + productCurrency;
}
serviceInfo["price"] = formattedPrice;
service[configKey::serviceInfo] = serviceInfo;
services[i] = service;
data["services"] = services;
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
break;
}
}
}
#endif
m_apiServicesModel->updateModel(data);
if (m_apiServicesModel->rowCount() > 0) {
m_apiServicesModel->setServiceIndex(0);
@@ -768,10 +724,6 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
QThread::msleep(10);
#endif
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled(), m_settings->getDevProxyStorageEndpoint(),
m_settings->getDevProxyUrl());
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
@@ -787,7 +739,17 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
apiPayload[configKey::apiEndpoint] = serverConfig.value(configKey::apiEndpoint).toString();
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody);
QString endpoint = QString("%1v1/proxy_config");
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
ErrorCode errorCode = gatewayController.post(endpoint, apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) {
errorCode = fillServerConfig(serviceProtocol, protocolData, responseBody, serverConfig);
@@ -994,8 +956,12 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(),
m_settings->getDevProxyStorageEndpoint(), m_settings->getDevProxyUrl());
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
return gatewayController.post(endpoint, apiPayload, responseBody);
}

View File

@@ -1,6 +1,8 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/networkUtilities.h"
#include <QJsonDocument>
#include <QJsonObject>
@@ -31,11 +33,6 @@ void ApiNewsController::fetchNews(bool showError)
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled(),
m_settings->getDevProxyStorageEndpoint(),
m_settings->getDevProxyUrl());
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
@@ -47,26 +44,35 @@ void ApiNewsController::fetchNews(bool showError)
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
future.then(this, [this, showError, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
QString endpoint = QString("%1v1/news");
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(endpoint, payload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
}
// Parse response
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
});
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
}

View File

@@ -3,9 +3,13 @@
#include <QEventLoop>
#include <QTimer>
#include "core/api/apiDefs.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/networkUtilities.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
#include "version.h"
namespace
@@ -56,9 +60,6 @@ bool ApiSettingsController::getAccountInfo(bool reload)
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(),
m_settings->getDevProxyStorageEndpoint(), m_settings->getDevProxyUrl());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
@@ -68,8 +69,18 @@ bool ApiSettingsController::getAccountInfo(bool reload)
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody);
QString endpoint = QString("%1v1/account_info");
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
ErrorCode errorCode = gatewayController.post(endpoint, apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;

View File

@@ -291,8 +291,6 @@ void ImportController::processNativeWireGuardConfig()
clientProtocolConfig[config_key::cookieReplyPacketJunkSize] = "0";
clientProtocolConfig[config_key::transportPacketJunkSize] = "0";
clientProtocolConfig[config_key::specialJunk1] = protocols::awg::defaultSpecialJunk1;
clientProtocolConfig[config_key::isObfuscationEnabled] = true;
serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson());

View File

@@ -308,15 +308,6 @@ void SettingsController::toggleStartMinimized(bool enable)
emit startMinimizedChanged();
}
bool SettingsController::isNewsNotificationsEnabled()
{
return m_settings->isNewsNotifications();
}
void SettingsController::toggleNewsNotificationsEnabled(bool enable)
{
m_settings->setNewsNotifications(enable);
}
bool SettingsController::isScreenshotsEnabled()
{
return m_settings->isScreenshotsEnabled();
@@ -426,28 +417,6 @@ QString SettingsController::getGatewayEndpoint()
return m_settings->isDevGatewayEnv() ? "Dev endpoint" : m_settings->getGatewayEndpoint();
}
void SettingsController::setDevProxyStorageEndpoint(const QString &endpoint)
{
m_settings->setDevProxyStorageEndpoint(endpoint);
emit devProxyStorageEndpointChanged(endpoint);
}
QString SettingsController::getDevProxyStorageEndpoint()
{
return m_settings->getDevProxyStorageEndpoint();
}
void SettingsController::setDevProxyUrl(const QString &url)
{
m_settings->setDevProxyUrl(url);
emit devProxyUrlChanged(url);
}
QString SettingsController::getDevProxyUrl()
{
return m_settings->getDevProxyUrl();
}
bool SettingsController::isDevGatewayEnv()
{
return m_settings->isDevGatewayEnv();

View File

@@ -30,9 +30,6 @@ public:
Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled)
Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged)
Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged)
Q_PROPERTY(QString devProxyStorageEndpoint READ getDevProxyStorageEndpoint WRITE setDevProxyStorageEndpoint NOTIFY
devProxyStorageEndpointChanged)
Q_PROPERTY(QString devProxyUrl READ getDevProxyUrl WRITE setDevProxyUrl NOTIFY devProxyUrlChanged)
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
@@ -76,9 +73,6 @@ public slots:
bool isStartMinimizedEnabled();
void toggleStartMinimized(bool enable);
bool isNewsNotificationsEnabled();
void toggleNewsNotificationsEnabled(bool enable);
bool isScreenshotsEnabled();
void toggleScreenshotsEnabled(bool enable);
@@ -101,10 +95,6 @@ public slots:
void resetGatewayEndpoint();
void setGatewayEndpoint(const QString &endpoint);
QString getGatewayEndpoint();
void setDevProxyStorageEndpoint(const QString &endpoint);
QString getDevProxyStorageEndpoint();
void setDevProxyUrl(const QString &url);
QString getDevProxyUrl();
bool isDevGatewayEnv();
void toggleDevGatewayEnv(bool enabled);
@@ -143,8 +133,6 @@ signals:
void devModeEnabled();
void gatewayEndpointChanged(const QString &endpoint);
void devGatewayEnvChanged(bool enabled);
void devProxyStorageEndpointChanged(const QString &endpoint);
void devProxyUrlChanged(const QString &url);
void imeHeightChanged(int height);
void safeAreaTopMarginChanged();

View File

@@ -112,11 +112,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
if (price == "free") {
return tr("Free");
}
#if defined(Q_OS_IOS) || defined(MACOS_NE)
return tr("%1 $").arg(price);
#else
return tr("%1 $/month").arg(price);
#endif
}
case EndDateRole: {
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");

View File

@@ -1,38 +0,0 @@
import QtQuick
import QtGamepadLegacy
Item {
id: root
property alias gamepad: gamepad
property alias gamepadKeyNav: gamepadKeyNav
Gamepad {
id: gamepad
deviceId: GamepadManager.connectedGamepads.length > 0 ? GamepadManager.connectedGamepads[0] : -1
onButtonStartChanged: {
if (buttonStart) {
ServersModel.setProcessedServerIndex(ServersModel.defaultIndex)
ConnectionController.connectButtonClicked()
}
}
}
GamepadKeyNavigation {
id: gamepadKeyNav
gamepad: gamepad
active: true
}
Connections {
target: GamepadManager
function onConnectedGamepadsChanged() {
if (GamepadManager.connectedGamepads.length > 0) {
gamepad.deviceId = GamepadManager.connectedGamepads[0]
} else {
gamepad.deviceId = -1
}
}
}
}

View File

@@ -111,11 +111,11 @@ Button {
color: {
if (root.enabled) {
if (root.pressed) {
return root.pressedColor
return pressedColor
}
return root.hovered ? root.hoveredColor : root.defaultColor
return root.hovered ? hoveredColor : defaultColor
} else {
return root.disabledColor
return disabledColor
}
}

View File

@@ -49,55 +49,6 @@ Item {
return drawerContent.state === stateName
}
function isDrawerType2(obj) {
return obj && typeof obj.drawerExpandedStateName !== "undefined" &&
typeof obj.drawerCollapsedStateName !== "undefined"
}
function isDescendantOfDrawer(obj) {
var current = obj
while (current && current !== root.parent) {
if (isDrawerType2(current)) {
return true
}
current = current.parent
}
return false
}
function findComponent(obj, typeCtor) {
if (!obj)
return null
if (isDrawerType2(obj) || isDescendantOfDrawer(obj))
return null
if (obj instanceof typeCtor)
return obj
if (obj.children && obj.children.length > 0) {
for (var i = 0; i < obj.children.length; i++) {
var matchingChildren = findComponent(obj.children[i], typeCtor)
if (matchingChildren) return matchingChildren
}
}
if (obj.contentItem) {
var matchingContentItem = findComponent(obj.contentItem, typeCtor)
if (matchingContentItem) return matchingContentItem
}
return null
}
function setParentInteractive(value) {
var flickableType = findComponent(root.parent, Flickable)
var listViewType = findComponent(root.parent, ListView)
if (flickableType) flickableType.interactive = value
if (listViewType) listViewType.interactive = value
}
Connections {
target: Qt.application
@@ -142,8 +93,6 @@ Item {
aboutToHide()
setParentInteractive(true)
closed()
}
@@ -169,8 +118,6 @@ Item {
root.aboutToShow()
setParentInteractive(false)
root.opened()
}

View File

@@ -71,8 +71,6 @@ Item {
implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: root.enabled
@@ -298,13 +296,13 @@ Item {
}
Keys.onEnterPressed: {
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
if (clickedFunction && typeof clickedFunction === "function") {
clickedFunction()
}
}
Keys.onReturnPressed: {
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
if (clickedFunction && typeof clickedFunction === "function") {
clickedFunction()
}
}

View File

@@ -19,6 +19,9 @@ Item {
property string buttonText
property string buttonImageSource
property string buttonImageColor: AmneziaStyle.color.midnightBlack
property string buttonBackgroundColor: AmneziaStyle.color.paleGray
property string buttonHoveredColor: AmneziaStyle.color.lightGray
property var clickedFunc
property alias textField: textField
@@ -67,7 +70,7 @@ Item {
border.width: 1
Behavior on border.color {
PropertyAnimation { duration: 200 }
PropertyAnimation { duration: 100 }
}
RowLayout {
@@ -121,7 +124,7 @@ Item {
background: Rectangle {
anchors.fill: parent
color: root.backgroundDisabledColor
color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor
}
onTextChanged: {
@@ -186,6 +189,14 @@ Item {
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
leftImageColor: root.buttonImageColor
defaultColor: root.buttonBackgroundColor
hoveredColor: root.buttonHoveredColor
pressedColor: root.buttonHoveredColor
disabledColor: AmneziaStyle.color.transparent
borderWidth: 0
anchors.top: content.top
anchors.bottom: content.bottom
@@ -193,7 +204,7 @@ Item {
height: content.implicitHeight
width: content.implicitHeight
squareLeftSide: true
squareLeftSide: false
clickedFunc: function() {
if (root.clickedFunc && typeof root.clickedFunc === "function") {

View File

@@ -73,50 +73,6 @@ PageType {
}
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Proxy storage endpoint")
textField.text: SettingsController.devProxyStorageEndpoint
buttonImageSource: textField.text !== "" ? "qrc:/images/controls/refresh-cw.svg" : ""
clickedFunc: function() {
SettingsController.devProxyStorageEndpoint = ""
}
textField.onEditingFinished: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
if (textField.text !== SettingsController.devProxyStorageEndpoint) {
SettingsController.devProxyStorageEndpoint = textField.text
}
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Proxy endpoint")
textField.text: SettingsController.devProxyUrl
buttonImageSource: textField.text !== "" ? "qrc:/images/controls/refresh-cw.svg" : ""
clickedFunc: function() {
SettingsController.devProxyUrl = ""
}
textField.onEditingFinished: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
if (textField.text !== SettingsController.devProxyUrl) {
SettingsController.devProxyUrl = textField.text
}
}
}
}
footer: ColumnLayout {

View File

@@ -140,16 +140,6 @@ PageType {
ListElement { name : "aes-128-gcm" }
}
function updateSelectedIndex() {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipher) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
@@ -157,14 +147,13 @@ PageType {
}
Component.onCompleted: {
updateSelectedIndex()
}
}
cipherDropDown.text = cipher
Connections {
target: listView.model
function onDataChanged() {
cipherListView.updateSelectedIndex()
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
selectedIndex = i
}
}
}
}
}

View File

@@ -192,16 +192,6 @@ PageType {
ListElement { name : qsTr("SHA1") }
}
function updateSelectedIndex() {
hashDropDown.text = hash
for (var i = 0; i < hashListView.model.count; i++) {
if (hashListView.model.get(i).name === hash) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
hashDropDown.text = selectedText
hash = hashDropDown.text
@@ -209,14 +199,13 @@ PageType {
}
Component.onCompleted: {
updateSelectedIndex()
}
}
hashDropDown.text = hash
Connections {
target: listView.model
function onDataChanged() {
hashListView.updateSelectedIndex()
for (var i = 0; i < hashListView.model.count; i++) {
if (hashListView.model.get(i).name === hashDropDown.text) {
currentIndex = i
}
}
}
}
}
@@ -253,16 +242,6 @@ PageType {
ListElement { name : qsTr("none") }
}
function updateSelectedIndex() {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipher) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
@@ -270,14 +249,13 @@ PageType {
}
Component.onCompleted: {
updateSelectedIndex()
}
}
cipherDropDown.text = cipher
Connections {
target: listView.model
function onDataChanged() {
cipherListView.updateSelectedIndex()
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
currentIndex = i
}
}
}
}
}

View File

@@ -109,16 +109,6 @@ PageType {
ListElement { name : "aes-128-gcm" }
}
function updateSelectedIndex() {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipher) {
selectedIndex = i
break
}
}
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
@@ -126,14 +116,13 @@ PageType {
}
Component.onCompleted: {
updateSelectedIndex()
}
}
cipherDropDown.text = cipher
Connections {
target: listView.model
function onDataChanged() {
cipherListView.updateSelectedIndex()
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
currentIndex = i
}
}
}
}
}

View File

@@ -148,7 +148,7 @@ PageType {
id: news
property string title: qsTr("News & Notifications")
readonly property string leftImagePath: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled() ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
property bool isVisible: ServersModel.hasServersFromGatewayApi
readonly property var clickedHandler: function() {
if (!ServersModel.hasServersFromGatewayApi) {

View File

@@ -224,6 +224,7 @@ PageType {
height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
RowLayout {
id: addAppButton

View File

@@ -168,29 +168,6 @@ PageType {
DividerType {
visible: !GC.isMobile()
}
SwitcherType {
id: switcherNewsNotificationEnabled
visible: ServersModel.hasServersFromGatewayApi
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("News Notification")
descriptionText: qsTr("Show notification icon when has unread news")
checked: SettingsController.isNewsNotificationsEnabled()
onToggled: function() {
if (checked !== SettingsController.isNewsNotificationsEnabled()) {
SettingsController.toggleNewsNotificationsEnabled(checked)
}
}
}
DividerType {
visible: !GC.isMobile()
}
}
footer: ColumnLayout {

View File

@@ -240,6 +240,7 @@ PageType {
height: addSiteButton.implicitHeight + 48
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
RowLayout {
id: addSiteButton

View File

@@ -97,32 +97,16 @@ PageType {
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 16
Layout.bottomMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
text: qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
@@ -137,37 +121,6 @@ PageType {
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}

View File

@@ -107,7 +107,6 @@ PageType {
onClicked: function() {
isEasySetup = true
checked = true
var defaultContainerProto = ContainerProps.defaultProtocol(dockerContainer)
listView.dockerContainer = dockerContainer

View File

@@ -383,7 +383,7 @@ PageType {
objectName: "settingsTabButton"
isSelected: tabBar.currentIndex === 2
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled()) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
Binding {
target: settingsTabButton
property: "defaultColor"

View File

@@ -83,11 +83,6 @@ Window {
}
}
Loader {
active: Qt.platform.os === "android"
source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : ""
}
Connections {
objectName: "pageControllerConnections"

View File

@@ -99,7 +99,21 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
}
}
}
if (container != DockerContainer::Ipsec) {
if (startNetworkCheckIfReady()) {
m_pendingNetworkCheck = false;
} else {
m_pendingNetworkCheck = true;
qWarning() << "Deferring startNetworkCheck; missing gateway/local address"
<< m_vpnProtocol->vpnGateway() << m_vpnProtocol->vpnLocalAddress();
}
} else {
m_pendingNetworkCheck = false;
}
} else if (state == Vpn::ConnectionState::Error) {
m_pendingNetworkCheck = false;
iface->flushDns();
if (m_settings->isSitesSplitTunnelingEnabled()) {
@@ -107,6 +121,12 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
iface->clearSavedRoutes();
}
}
} else if (state == Vpn::ConnectionState::Connecting) {
} else if (state == Vpn::ConnectionState::Disconnected) {
m_pendingNetworkCheck = false;
auto result = iface->stopNetworkCheck();
result.waitForFinished(3000);
}
});
#endif
@@ -245,7 +265,7 @@ ErrorCode VpnConnection::lastError() const
void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &vpnConfiguration)
{
qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is")
qDebug() << QString("ConnectToVpn, Server index is %1, container is %2, route mode is")
.arg(serverIndex)
.arg(ContainerProps::containerToString(container))
<< m_settings->routeMode();
@@ -253,7 +273,11 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName);
emit connectionStateChanged(Vpn::ConnectionState::Connecting);
m_pendingNetworkCheck = false;
m_vpnConfiguration = vpnConfiguration;
m_serverIndex = serverIndex;
m_serverCredentials = credentials;
m_dockerContainer = container;
#ifdef AMNEZIA_DESKTOP
if (m_vpnProtocol) {
@@ -292,12 +316,71 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
emit connectionStateChanged(Vpn::ConnectionState::Error);
}
void VpnConnection::restartConnection()
{
// Only reconnect if VPN was connected before sleep/network change
if (!m_wasConnectedBeforeSleep) {
qDebug() << "VPN was not connected before sleep/network change, skipping reconnection";
return;
}
qDebug() << "VPN was connected before sleep/network change, attempting reconnection";
this->disconnectFromVpn();
#ifdef Q_OS_LINUX
QThread::msleep(5000);
#endif
this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration);
// Reset the flag after reconnection attempt
m_wasConnectedBeforeSleep = false;
}
void VpnConnection::createProtocolConnections()
{
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this,
SLOT(onConnectionStateChanged(Vpn::ConnectionState)));
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
#ifdef AMNEZIA_DESKTOP
if (m_connectionLoseHandle)
disconnect(m_connectionLoseHandle);
if (m_networkChangeHandle)
disconnect(m_networkChangeHandle);
m_connectionLoseHandle = QMetaObject::Connection();
m_networkChangeHandle = QMetaObject::Connection();
// TODO: replace unsafe IpcClient::Interface() calls
m_connectionLoseHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::connectionLose,
this, [this]() {
qDebug() << "Connection Lose";
auto result = IpcClient::Interface()->stopNetworkCheck();
result.waitForFinished(3000);
// Track VPN state before connection loss
m_wasConnectedBeforeSleep = isConnected();
qDebug() << "VPN was connected before connection loss:" << m_wasConnectedBeforeSleep;
this->restartConnection();
});
m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange,
this, [this]() {
qDebug() << "Network change";
// Track VPN state before network change (including sleep/wake)
m_wasConnectedBeforeSleep = isConnected();
qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep;
this->restartConnection();
});
connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated,
this, [this](const QString& gateway, const QString& localAddress) {
Q_UNUSED(gateway)
Q_UNUSED(localAddress)
if (connectionState() != Vpn::ConnectionState::Connected) {
return;
}
if (startNetworkCheckIfReady()) {
m_pendingNetworkCheck = false;
}
});
#endif
}
void VpnConnection::appendKillSwitchConfig()
@@ -399,13 +482,28 @@ void VpnConnection::appendSplitTunnelingConfig()
m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode);
m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray);
}
qDebug() << QString("Site split tunneling is %1, route mode is %2")
.arg(m_settings->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled")
.arg(routeMode);
qDebug() << QString("App split tunneling is %1, route mode is %2")
.arg(m_settings->isAppsSplitTunnelingEnabled() ? "enabled" : "disabled")
.arg(appsRouteMode);
bool VpnConnection::startNetworkCheckIfReady()
{
#ifdef AMNEZIA_DESKTOP
if (!m_vpnProtocol || m_dockerContainer == DockerContainer::Ipsec) {
return false;
}
const QString gateway = m_vpnProtocol->vpnGateway();
const QString localAddress = m_vpnProtocol->vpnLocalAddress();
if (gateway.isEmpty() || localAddress.isEmpty()) {
return false;
}
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->startNetworkCheck(gateway, localAddress);
return reply.waitForFinished() && reply.returnValue();
});
#else
return false;
#endif
}
#ifdef Q_OS_ANDROID

View File

@@ -52,6 +52,7 @@ public slots:
const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
void disconnectFromVpn();
void restartConnection();
void addRoutes(const QStringList &ips);
void deleteRoutes(const QStringList &ips);
@@ -72,6 +73,8 @@ protected slots:
protected:
QSharedPointer<VpnProtocol> m_vpnProtocol;
QMetaObject::Connection m_connectionLoseHandle;
QMetaObject::Connection m_networkChangeHandle;
private:
std::shared_ptr<Settings> m_settings;
@@ -79,6 +82,14 @@ private:
QJsonObject m_routeMode;
QString m_remoteAddress;
ServerCredentials m_serverCredentials;
int m_serverIndex;
DockerContainer m_dockerContainer;
// Track VPN state before sleep for smart reconnection
bool m_wasConnectedBeforeSleep = false;
bool m_pendingNetworkCheck = false;
// Only for iOS for now, check counters
QTimer m_checkTimer;
@@ -93,6 +104,7 @@ private:
void appendSplitTunnelingConfig();
void appendKillSwitchConfig();
bool startNetworkCheckIfReady();
};
#endif // VPNCONNECTION_H

View File

@@ -31,7 +31,6 @@ set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy
set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=%
set APP_NAME=AmneziaVPN
set APP_FILENAME=%APP_NAME:"=%.exe
set SERVICE_FILENAME=%APP_NAME:"=%-service.exe
set APP_DOMAIN=org.amneziavpn.package
set OUT_APP_DIR=%WORK_DIR:"=%\client\release
set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=%
@@ -44,7 +43,6 @@ set STAGE_DIR=%WORK_DIR:"=%\stage
echo "Environment:"
echo "WORK_DIR: %WORK_DIR%"
echo "APP_FILENAME: %APP_FILENAME%"
echo "SERVICE_FILENAME: %SERVICE_FILENAME%"
echo "PROJECT_DIR: %PROJECT_DIR%"
echo "SCRIPT_DIR: %SCRIPT_DIR%"
echo "OUT_APP_DIR: %OUT_APP_DIR%"
@@ -76,7 +74,7 @@ if %errorlevel% neq 0 exit /b %errorlevel%
echo "Deploying..."
mkdir "%OUT_APP_DIR%"
copy "%WORK_DIR%\service\server\release\%SERVICE_FILENAME%" "%OUT_APP_DIR%"
copy "%WORK_DIR%\service\server\release\%APP_NAME%-service.exe" "%OUT_APP_DIR%"
rem copy "%WORK_DIR%\client\%APP_FILENAME%" "%OUT_APP_DIR%"
copy /Y "%PROJECT_DIR%\client\images\app.ico" "%OUT_APP_DIR%\AmneziaVPN.ico" >nul
@@ -85,8 +83,7 @@ echo "Signing exe"
cd %OUT_APP_DIR%
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe
"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations --force-openssl "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
"%QT_BIN_DIR:"=%\windeployqt" --release "%OUT_APP_DIR:"=%\%SERVICE_FILENAME:"=%"
"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll

View File

@@ -10,8 +10,6 @@
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>Sockets</key>
<dict>
<key>Listeners</key>

View File

@@ -7,54 +7,42 @@ LOG_FOLDER=/var/log/$APP_NAME
LOG_FILE="$LOG_FOLDER/post-install.log"
APP_PATH=/Applications/$APP_NAME.app
rm -rf "$LOG_FOLDER"
mkdir -p "$LOG_FOLDER"
echo "`date` Script started" > "$LOG_FILE"
log() {
echo "`date` $*" >> "$LOG_FILE"
}
run_cmd() {
log "CMD: $*"
"$@" >> "$LOG_FILE" 2>&1
local ec=$?
log "EXIT: $ec"
return $ec
}
# Handle new installations unpacked into localized folder
if [ -d "/Applications/${APP_NAME}.localized" ]; then
log "Detected ${APP_NAME}.localized, migrating to standard path"
run_cmd sudo rm -rf "$APP_PATH"
run_cmd sudo mv "/Applications/${APP_NAME}.localized/${APP_NAME}.app" "$APP_PATH"
run_cmd sudo rm -rf "/Applications/${APP_NAME}.localized"
echo "`date` Detected ${APP_NAME}.localized, migrating to standard path" >> $LOG_FILE
sudo rm -rf "$APP_PATH"
sudo mv "/Applications/${APP_NAME}.localized/${APP_NAME}.app" "$APP_PATH"
sudo rm -rf "/Applications/${APP_NAME}.localized"
fi
run_cmd launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
if launchctl list "$APP_NAME-service" &> /dev/null; then
launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
fi
run_cmd sudo chmod -R a-w "$APP_PATH/"
run_cmd sudo chown -R root "$APP_PATH/"
run_cmd sudo chgrp -R wheel "$APP_PATH/"
sudo chmod -R a-w "$APP_PATH/"
sudo chown -R root "$APP_PATH/"
sudo chgrp -R wheel "$APP_PATH/"
log "Requesting ${APP_NAME} to quit gracefully"
run_cmd osascript -e 'tell application "AmneziaVPN" to quit' || true
rm -rf $LOG_FOLDER
mkdir -p $LOG_FOLDER
echo "`date` Script started" > $LOG_FILE
echo "Requesting ${APP_NAME} to quit gracefully" >> "$LOG_FILE"
osascript -e 'tell application "AmneziaVPN" to quit'
PLIST_SOURCE="$APP_PATH/Contents/Resources/$PLIST_NAME"
if [ -f "$PLIST_SOURCE" ]; then
run_cmd mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME"
mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME" 2>> $LOG_FILE
else
log "ERROR: service plist not found at $PLIST_SOURCE"
echo "`date` ERROR: service plist not found at $PLIST_SOURCE" >> $LOG_FILE
fi
run_cmd chown root:wheel "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd chmod 644 "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd launchctl bootstrap system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl load "$LAUNCH_DAEMONS_PLIST_NAME"
run_cmd launchctl enable "system/$APP_NAME-service" || true
run_cmd launchctl kickstart -k "system/$APP_NAME-service" || true
run_cmd launchctl print "system/$APP_NAME-service" || true
log "Launching ${APP_NAME} application"
run_cmd open -a "$APP_PATH" || true
chown root:wheel "$LAUNCH_DAEMONS_PLIST_NAME"
launchctl load "$LAUNCH_DAEMONS_PLIST_NAME"
echo "`date` Launching ${APP_NAME} application" >> $LOG_FILE
open -a "$APP_PATH" 2>> $LOG_FILE || true
log "Script finished"
echo "`date` Service status: $?" >> $LOG_FILE
echo "`date` Script finished" >> $LOG_FILE

View File

@@ -29,7 +29,7 @@ fi
# Unload the service if loaded and remove its plist file regardless
if launchctl list "${APP_NAME}-service" &> /dev/null; then
sudo launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
fi
sudo rm -f "$LAUNCH_DAEMONS_PLIST_NAME"

View File

@@ -1,9 +1,9 @@
#!/bin/bash
#!/bin/sh
set -e
VERSION=$1
if [[ -z "$VERSION" ]]; then
if [[ $VERSION = '' ]]; then
echo '::error::VERSION does not set. Exiting with error...'
exit 1
fi
@@ -14,39 +14,25 @@ cd dist
echo $VERSION >> VERSION
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE
if [[ $(cat CHANGELOG) = null ]]; then
echo '::error::Release does not exists. Exiting with error...'
exit 1
fi
# Download files with error handling
download_file() {
local url=$1
local filename=$(basename "$url")
echo "Downloading $filename..."
if ! wget -q "$url"; then
echo "::error::Failed to download $filename from $url"
exit 8
fi
echo "Successfully downloaded $filename"
}
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_x86.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_x86_64.apk
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux_x64.tar
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.pkg
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_x64.exe
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_arm64-v8a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_armeabi-v7a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86_64.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_arm64-v8a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_armeabi-v7a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86_64.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux_x64.tar.zip
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.dmg
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos_old.dmg
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_windows_x64.exe
cd ../
echo "Syncing to R2..."
if ! rclone sync ./dist/ r2:/updates/; then
echo "::error::Failed to sync files to R2"
exit 8
fi
echo "Deployment completed successfully!"
rclone sync ./dist/ r2:/updates/

View File

@@ -11,7 +11,6 @@
namespace amnezia {
enum PermittedProcess {
Invalid,
OpenVPN,
Wireguard,
Tun2Socks,
@@ -20,18 +19,16 @@ enum PermittedProcess {
inline QString permittedProcessPath(PermittedProcess pid)
{
switch (pid) {
case PermittedProcess::OpenVPN:
return Utils::openVpnExecPath();
case PermittedProcess::Wireguard:
return Utils::wireguardExecPath();
case PermittedProcess::CertUtil:
return Utils::certUtilPath();
case PermittedProcess::Tun2Socks:
return Utils::tun2socksPath();
default:
return "";
if (pid == PermittedProcess::OpenVPN) {
return Utils::openVpnExecPath();
} else if (pid == PermittedProcess::Wireguard) {
return Utils::wireguardExecPath();
} else if (pid == PermittedProcess::CertUtil) {
return Utils::certUtilPath();
} else if (pid == PermittedProcess::Tun2Socks) {
return Utils::tun2socksPath();
}
return "";
}
@@ -51,51 +48,6 @@ inline QString getIpcProcessUrl(int pid) {
#endif
}
inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &args) {
using Validator = std::function<bool(const QString&)>;
QMap<QString, Validator> namedArgs;
QList<Validator> positionalArgs;
switch (proc) {
case Tun2Socks:
namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); };
namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); };
break;
default:
//FIXME
return args;
}
QStringList sanitized;
for (int i = 0, pos = 0; i < args.size(); i++) {
const auto& key = args[i];
if (const auto found = namedArgs.find(key); found != namedArgs.end()) {
const auto validator = found.value();
if (validator) {
if (i + 1 < args.size()) {
const auto& value = args[i+1];
if (validator(value)) {
sanitized << key << value;
i++;
}
}
} else {
sanitized << key;
}
} else if (pos < positionalArgs.size()) {
if (const auto validator = positionalArgs[pos]; validator && validator(key)) {
sanitized << key;
pos++;
}
}
}
return sanitized;
}
} // namespace amnezia

View File

@@ -38,8 +38,8 @@ class IpcInterface
SLOT( bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) );
SLOT( bool restoreResolvers() );
SLOT(bool xrayStart(const QString &config));
SLOT(bool xrayStop());
SLOT(void xrayStart(const QString &config));
SLOT(void xrayStop());
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
SLOT( bool stopNetworkCheck() );

View File

@@ -4,8 +4,6 @@
class IpcProcessInterface
{
SLOT( start() );
SLOT( terminate() );
SLOT( kill() );
SLOT( close() );
SLOT( setArguments(const QStringList &arguments) );
@@ -19,11 +17,6 @@ class IpcProcessInterface
SLOT( QByteArray readAllStandardError() );
SLOT( QByteArray readAllStandardOutput() );
SLOT( bool waitForFinished() );
SLOT( bool waitForFinished(int msecs) );
SLOT( bool waitForStarted() );
SLOT( bool waitForStarted(int msecs) );
SIGNAL( errorOccurred(QProcess::ProcessError error) );
SIGNAL( finished(int exitCode, QProcess::ExitStatus exitStatus) );

View File

@@ -0,0 +1,11 @@
#include <QtCore>
#include <QString>
class IpcProcessTun2Socks
{
SLOT( start() );
SLOT( stop() );
SIGNAL( setConnectionState(int state) );
SIGNAL( stateChanged(QProcess::ProcessState newState) );
};

View File

@@ -304,7 +304,7 @@ bool IpcServer::refreshKillSwitch(bool enabled)
return KillSwitch::instance()->refresh(enabled);
}
bool IpcServer::xrayStart(const QString& cfg)
void IpcServer::xrayStart(const QString& cfg)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStart";
@@ -313,7 +313,7 @@ bool IpcServer::xrayStart(const QString& cfg)
return Xray::getInstance().startXray(cfg);
}
bool IpcServer::xrayStop()
void IpcServer::xrayStop()
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStop";

View File

@@ -10,8 +10,10 @@
#include "ipc.h"
#include "ipcserverprocess.h"
#include "ipctun2socksprocess.h"
#include "rep_ipc_interface_source.h"
#include "rep_ipc_process_tun2socks_source.h"
class IpcServer : public IpcInterfaceSource
{
@@ -42,8 +44,8 @@ public:
virtual bool refreshKillSwitch( bool enabled ) override;
virtual bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) override;
virtual bool restoreResolvers() override;
virtual bool xrayStart(const QString& cfg) override;
virtual bool xrayStop() override;
virtual void xrayStart(const QString& cfg) override;
virtual void xrayStop() override;
virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override;
virtual bool stopNetworkCheck() override;
@@ -54,10 +56,12 @@ private:
ProcessDescriptor (QObject *parent = nullptr) {
serverNode = QSharedPointer<QRemoteObjectHost>(new QRemoteObjectHost(parent));
ipcProcess = QSharedPointer<IpcServerProcess>(new IpcServerProcess(parent));
tun2socksProcess = QSharedPointer<IpcProcessTun2Socks>(new IpcProcessTun2Socks(parent));
localServer = QSharedPointer<QLocalServer>(new QLocalServer(parent));
}
QSharedPointer<IpcServerProcess> ipcProcess;
QSharedPointer<IpcProcessTun2Socks> tun2socksProcess;
QSharedPointer<QRemoteObjectHost> serverNode;
QSharedPointer<QLocalServer> localServer;
};

View File

@@ -40,14 +40,6 @@ void IpcServerProcess::start()
m_process->waitForStarted();
}
void IpcServerProcess::terminate() {
m_process->terminate();
}
void IpcServerProcess::kill() {
m_process->kill();
}
void IpcServerProcess::close()
{
m_process->close();
@@ -55,7 +47,7 @@ void IpcServerProcess::close()
void IpcServerProcess::setArguments(const QStringList &arguments)
{
m_process->setArguments(amnezia::sanitizeArguments(m_program, arguments));
m_process->setArguments(arguments);
}
void IpcServerProcess::setInputChannelMode(QProcess::InputChannelMode mode)
@@ -77,9 +69,7 @@ void IpcServerProcess::setProcessChannelMode(QProcess::ProcessChannelMode mode)
void IpcServerProcess::setProgram(int programId)
{
m_program = static_cast<amnezia::PermittedProcess>(programId);
m_process->setProgram(amnezia::permittedProcessPath(m_program));
m_process->setArguments({});
m_process->setProgram(amnezia::permittedProcessPath(static_cast<amnezia::PermittedProcess>(programId)));
}
void IpcServerProcess::setWorkingDirectory(const QString &dir)
@@ -102,20 +92,4 @@ QByteArray IpcServerProcess::readAllStandardOutput()
return m_process->readAllStandardOutput();
}
bool IpcServerProcess::waitForStarted() {
return m_process->waitForStarted();
}
bool IpcServerProcess::waitForStarted(int msecs) {
return m_process->waitForStarted(msecs);
}
bool IpcServerProcess::waitForFinished() {
return m_process->waitForFinished();
}
bool IpcServerProcess::waitForFinished(int msecs) {
return m_process->waitForFinished(msecs);
}
#endif

View File

@@ -1,7 +1,6 @@
#ifndef IPCSERVERPROCESS_H
#define IPCSERVERPROCESS_H
#include "ipc.h"
#include <QObject>
#ifndef Q_OS_IOS
@@ -15,8 +14,6 @@ public:
virtual ~IpcServerProcess();
void start() override;
void terminate() override;
void kill() override;
void close() override;
void setArguments(const QStringList &arguments) override;
@@ -30,15 +27,9 @@ public:
QByteArray readAllStandardError() override;
QByteArray readAllStandardOutput() override;
bool waitForStarted() override;
bool waitForStarted(int msecs) override;
bool waitForFinished() override;
bool waitForFinished(int msecs) override;
signals:
private:
amnezia::PermittedProcess m_program = amnezia::PermittedProcess::Invalid;
QSharedPointer<QProcess> m_process;
};

View File

@@ -0,0 +1,79 @@
#include "ipctun2socksprocess.h"
#include "ipc.h"
#include <QProcess>
#include <QString>
#include "../protocols/protocols_defs.h"
#ifndef Q_OS_IOS
IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) :
IpcProcessTun2SocksSource(parent),
m_t2sProcess(QSharedPointer<QProcess>(new QProcess()))
{
qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()";
}
IpcProcessTun2Socks::~IpcProcessTun2Socks()
{
qDebug() << "IpcProcessTun2Socks::~IpcProcessTun2Socks()";
}
void IpcProcessTun2Socks::start()
{
connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged);
qDebug() << "IpcProcessTun2Socks::start()";
m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast<amnezia::PermittedProcess>(amnezia::PermittedProcess::Tun2Socks)));
QString XrayConStr = "socks5://127.0.0.1:10808";
#ifdef Q_OS_WIN
QStringList arguments({"-device", "tun://tun2?guid={081A8A84-8D12-4DF5-B8C4-396D5B0053E4}", "-proxy", XrayConStr, "-tun-post-up",
QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255")
.arg(amnezia::protocols::xray::defaultLocalAddr)});
#endif
#ifdef Q_OS_LINUX
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr});
#endif
#ifdef Q_OS_MAC
QStringList arguments({"-device", "utun22", "-proxy", XrayConStr});
#endif
m_t2sProcess->setArguments(arguments);
if (Utils::processIsRunning(Utils::executable("tun2socks", false))) {
qDebug().noquote() << "kill previos tun2socks";
Utils::killProcessByName(Utils::executable("tun2socks", false));
}
m_t2sProcess->start();
connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() {
QString line = m_t2sProcess.data()->readAllStandardOutput();
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
emit setConnectionState(Vpn::ConnectionState::Connected);
}
});
connect(m_t2sProcess.data(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus;
emit setConnectionState(Vpn::ConnectionState::Disconnected);
if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) {
emit setConnectionState(Vpn::ConnectionState::Error);
}
});
m_t2sProcess->start();
m_t2sProcess->waitForStarted();
}
void IpcProcessTun2Socks::stop()
{
qDebug() << "IpcProcessTun2Socks::stop()";
m_t2sProcess->disconnect();
m_t2sProcess->kill();
m_t2sProcess->waitForFinished(3000);
}
#endif

52
ipc/ipctun2socksprocess.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef IPCTUN2SOCKSPROCESS_H
#define IPCTUN2SOCKSPROCESS_H
#include <QObject>
#ifndef Q_OS_IOS
#include "rep_ipc_process_tun2socks_source.h"
namespace Vpn
{
Q_NAMESPACE
enum ConnectionState {
Unknown,
Disconnected,
Preparing,
Connecting,
Connected,
Disconnecting,
Reconnecting,
Error
};
Q_ENUM_NS(ConnectionState)
}
class IpcProcessTun2Socks : public IpcProcessTun2SocksSource
{
Q_OBJECT
public:
explicit IpcProcessTun2Socks(QObject *parent = nullptr);
virtual ~IpcProcessTun2Socks();
void start() override;
void stop() override;
signals:
private:
QSharedPointer<QProcess> m_t2sProcess;
};
#else
class IpcProcessTun2Socks : public QObject
{
Q_OBJECT
public:
explicit IpcProcessTun2Socks(QObject *parent = nullptr);
};
#endif
#endif // IPCTUN2SOCKSPROCESS_H

View File

@@ -6,7 +6,7 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION})
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat Concurrent)
find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat)
qt_standard_project_setup()
@@ -75,6 +75,7 @@ set(HEADERS
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.h
${CMAKE_CURRENT_LIST_DIR}/localserver.h
${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h
${CMAKE_CURRENT_LIST_DIR}/router.h
@@ -96,6 +97,7 @@ set(SOURCES
${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.cpp
${CMAKE_CURRENT_LIST_DIR}/localserver.cpp
${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp
${CMAKE_CURRENT_LIST_DIR}/main.cpp
@@ -351,7 +353,7 @@ include_directories(
add_executable(${PROJECT} ${SOURCES} ${HEADERS} ${RESOURCES})
target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus Qt6::Concurrent ${LIBS})
target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus ${LIBS})
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
@@ -387,6 +389,7 @@ endif()
qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep)
qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep)
qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_tun2socks.rep)
# copy deploy artifacts required to run the application to the debug build folder
if(WIN32)

View File

@@ -40,6 +40,7 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
if (!m_isRemotingEnabled) {
m_isRemotingEnabled = true;
m_serverNode.enableRemoting(&m_ipcServer);
m_serverNode.enableRemoting(&m_tun2socks);
}
});

View File

@@ -38,6 +38,7 @@ public:
~LocalServer();
QSharedPointer<QLocalServer> m_server;
IpcServer m_ipcServer;
IpcProcessTun2Socks m_tun2socks;
QRemoteObjectHost m_serverNode;
bool m_isRemotingEnabled = false;

View File

@@ -66,9 +66,6 @@ void Router::resetIpStack()
bool Router::createTun(const QString &dev, const QString &subnet)
{
#ifdef Q_OS_WIN
return RouterWin::Instance().createTun(dev, subnet);
#endif
#ifdef Q_OS_LINUX
return RouterLinux::Instance().createTun(dev, subnet);
#endif

View File

@@ -5,7 +5,6 @@
#include <tchar.h>
#include <QProcess>
#include <QtConcurrent>
#include <core/networkUtilities.h>
@@ -309,77 +308,6 @@ void RouterWin::resetIpStack()
}
}
bool RouterWin::createTun(const QString &dev, const QString &subnet)
{
NET_LUID luid;
DWORD res = ConvertInterfaceAliasToLuid(reinterpret_cast<const wchar_t*>(dev.utf16()), &luid);
if (res != NO_ERROR) {
qCritical() << "Failed to convert luid: " << res;
return false;
}
HANDLE hEvent = CreateEvent(nullptr, true, false, nullptr);
if (!hEvent) {
qCritical() << "Failed to allocate event object";
return false;
}
auto _guardEvent = qScopeGuard([hEvent](){ CloseHandle(hEvent); });
struct {
HANDLE hEvent;
NET_LUID luid;
const QString &subnet;
bool found;
} ctx = { .hEvent = hEvent, .luid = luid, .subnet = subnet, .found = false };
auto cb = [](void *priv, MIB_UNICASTIPADDRESS_ROW *row, MIB_NOTIFICATION_TYPE NotificationType) {
auto* c = reinterpret_cast<decltype(ctx)*>(priv);
if (row != nullptr && row->InterfaceLuid.Value == c->luid.Value && row->Address.si_family == AF_INET) {
char ip[INET_ADDRSTRLEN];
inet_ntop(row->Address.Ipv4.sin_family, &row->Address.Ipv4.sin_addr, ip, INET_ADDRSTRLEN);
if (c->subnet == ip) {
c->found = true;
SetEvent(c->hEvent);
}
}
};
HANDLE hNotif;
res = NotifyUnicastIpAddressChange(AF_INET, cb, &ctx, false, &hNotif);
if (res != NO_ERROR) {
qCritical() << "Failed to subscribe to interface change";
return false;
}
auto _guardNotif = qScopeGuard([hNotif](){ CancelMibChangeNotify2(hNotif); });
MIB_UNICASTIPADDRESS_ROW row;
InitializeUnicastIpAddressEntry(&row);
row.InterfaceLuid = luid;
row.Address.si_family = AF_INET;
inet_pton(AF_INET, subnet.toStdString().c_str(), &row.Address.Ipv4.sin_addr);
row.OnLinkPrefixLength = 32;
row.ValidLifetime = 0xffffffff;
row.PreferredLifetime = 0xffffffff;
row.DadState = IpDadStatePreferred;
res = CreateUnicastIpAddressEntry(&row);
if (res != NO_ERROR && res != ERROR_OBJECT_ALREADY_EXISTS) {
qDebug() << "Failed to create IP address:" << res;
return false;
}
res = WaitForSingleObject(hEvent, 10000);
if (res == WAIT_TIMEOUT) {
qCritical() << "Timeout of waiting for IP assignment for " << dev << " device";
return false;
}
return ctx.found;
}
void RouterWin::suspendWcmSvc(bool suspend)
{
if (suspend == m_suspended) return;
@@ -537,19 +465,11 @@ bool RouterWin::StopRoutingIpv6()
qDebug() << "RouterWin::StopRoutingIpv6";
if (auto loopback = findLoopbackIface(); loopback.isValid()) {
QFuture<bool> res = QtConcurrent::mappedReduced(kIpv6Subnets, [loopback](const QString &subnet) -> bool {
int res = QProcess::execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" });
return res == 0;
},
[](bool &result, bool success) {
result = result && success;
}, true);
res.waitForFinished();
return res.result();
for (auto subnet : kIpv6Subnets) {
QProcess{}.execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" });
}
}
return false;
return true;
}
bool RouterWin::StartRoutingIpv6()
@@ -557,14 +477,9 @@ bool RouterWin::StartRoutingIpv6()
qDebug() << "RouterWin::StartRoutingIpv6";
if (auto loopback = findLoopbackIface(); loopback.isValid()) {
QFuture<bool> res = QtConcurrent::mappedReduced(kIpv6Subnets, [loopback](const QString &subnet) -> bool {
int res = QProcess::execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) });
return res == 0;
},
[](bool &result, bool success) {
result = result && success;
}, true);
for (auto subnet : kIpv6Subnets) {
QProcess{}.execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) });
}
}
return false;
return true;
}

View File

@@ -45,7 +45,6 @@ public:
bool StartRoutingIpv6();
bool StopRoutingIpv6();
bool createTun(const QString &dev, const QString &subnet);
void suspendWcmSvc(bool suspend);
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
bool restoreResolvers();

View File

@@ -27,7 +27,7 @@
#include <sys/socket.h>
#endif
bool Xray::startXray(const QString &cfg)
void Xray::startXray(const QString &cfg)
{
qDebug() << "Xray::startXray()";
@@ -40,38 +40,34 @@ bool Xray::startXray(const QString &cfg)
if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) {
qDebug() << "[xray] sockopt failed: " << err;
amnezia_xray_free(err);
return false;
free(err);
return;
}
amnezia_xray_setloghandler(ctxLogHandler, this);
QByteArray bytes = cfg.toUtf8();
if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) {
qDebug() << "[xray] configuration failed: " << err;
amnezia_xray_free(err);
return false;
free(err);
return;
}
amnezia_xray_setloghandler(ctxLogHandler, this);
if (auto err = amnezia_xray_start(); err != nullptr) {
qDebug() << "[xray] failed to start: " << err;
amnezia_xray_free(err);
return false;
free(err);
return;
}
return true;
}
bool Xray::stopXray()
void Xray::stopXray()
{
qDebug() << "Xray::stopXray()";
if (auto err = amnezia_xray_stop(); err != nullptr) {
qDebug() << "[xray] failed to stop: " << err;
amnezia_xray_free(err);
return false;
free(err);
return;
}
return true;
}
void Xray::logHandler(char* str)

View File

@@ -12,8 +12,8 @@ public:
return instance;
}
bool startXray(const QString& cfg);
bool stopXray();
void startXray(const QString& cfg);
void stopXray();
private:
static void ctxSockCallback(uintptr_t fd, void* ctx) {