mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-22 10:56:08 +03:00
Compare commits
27 Commits
feature/dn
...
bugfix/lin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582f21b0b1 | ||
|
|
f6277cdbb2 | ||
|
|
99312e61d3 | ||
|
|
9f0ae75a2f | ||
|
|
7960d8015d | ||
|
|
5dcc64e5e5 | ||
|
|
964436ad43 | ||
|
|
4fc3900fd5 | ||
|
|
8f5e42dd61 | ||
|
|
24895752c1 | ||
|
|
87eccfb4ca | ||
|
|
a983d0504e | ||
|
|
d0b8535395 | ||
|
|
f84480cf56 | ||
|
|
de7a026ec1 | ||
|
|
a128c7d247 | ||
|
|
f316f0e25a | ||
|
|
ea5242e29b | ||
|
|
b31a62c55f | ||
|
|
02e3107a23 | ||
|
|
1862850108 | ||
|
|
f73792844c | ||
|
|
a7199ca6f5 | ||
|
|
5e757cdd3b | ||
|
|
92af1f3268 | ||
|
|
aad9d6dae2 | ||
|
|
423fe3fd4f |
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -10,10 +10,10 @@ env:
|
||||
|
||||
jobs:
|
||||
Build-Linux-Ubuntu:
|
||||
runs-on: 4-core
|
||||
runs-on: android-runner
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.8.3
|
||||
QT_VERSION: 6.10.1
|
||||
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
|
||||
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||
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: 4-core
|
||||
runs-on: android-runner
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-36
|
||||
|
||||
2
.github/workflows/tag-upload.yml
vendored
2
.github/workflows/tag-upload.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Verify git tag
|
||||
run: |
|
||||
TAG_NAME=${{ inputs.RELEASE_VERSION }}
|
||||
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
|
||||
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([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
7
.gitignore
vendored
@@ -1,9 +1,5 @@
|
||||
# User settings
|
||||
*.user
|
||||
|
||||
# Gateway configs (contains sensitive endpoints)
|
||||
gateway.json
|
||||
client/gateway.json
|
||||
macOSPackage/
|
||||
AmneziaVPN.dmg
|
||||
AmneziaVPN.exe
|
||||
@@ -144,3 +140,6 @@ ios-ne-build.sh
|
||||
macos-ne-build.sh
|
||||
macos-signed-build.sh
|
||||
macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -14,3 +14,7 @@
|
||||
[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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.12.8)
|
||||
set(AMNEZIAVPN_VERSION 4.8.13.0)
|
||||
|
||||
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 2104)
|
||||
set(APP_ANDROID_VERSION_CODE 2106)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
Submodule client/3rd-prebuilt updated: 51bb4703a4...579673b2ed
1
client/3rd/qtgamepad
vendored
Submodule
1
client/3rd/qtgamepad
vendored
Submodule
Submodule client/3rd/qtgamepad added at f72b3e0c62
@@ -33,25 +33,14 @@ 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()
|
||||
|
||||
if(LINUX AND NOT ANDROID)
|
||||
list(APPEND PACKAGES QuickTemplates2 QmlModels OpenGL)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
@@ -67,6 +56,23 @@ endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
qt_add_executable(${PROJECT} MANUAL_FINALIZATION)
|
||||
|
||||
if(LINUX AND NOT ANDROID)
|
||||
target_link_options(${PROJECT} PRIVATE "-Wl,--no-as-needed")
|
||||
target_link_options(${PROJECT} PRIVATE "LINKER:--disable-new-dtags")
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
BUILD_RPATH "\$ORIGIN/../lib"
|
||||
INSTALL_RPATH "\$ORIGIN/../lib"
|
||||
INSTALL_RPATH_USE_LINK_PATH FALSE
|
||||
)
|
||||
set_property(TARGET ${PROJECT} PROPERTY BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE
|
||||
Qt6::QuickTemplates2
|
||||
Qt6::QmlModels
|
||||
Qt6::OpenGL
|
||||
)
|
||||
endif()
|
||||
target_include_directories(${PROJECT} PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
||||
)
|
||||
@@ -210,6 +216,17 @@ elseif(APPLE)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
|
||||
if(LINUX AND NOT ANDROID)
|
||||
target_link_libraries(${PROJECT} PRIVATE
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
Qt6::QuickTemplates2
|
||||
Qt6::QmlModels
|
||||
Qt6::OpenGL
|
||||
"-Wl,--pop-state"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||
|
||||
# deploy artifacts required to run the application to the debug build folder
|
||||
@@ -243,10 +260,13 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
endif()
|
||||
|
||||
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||
qt_finalize_target(${PROJECT})
|
||||
|
||||
option(BUILD_TESTS "Build transport integration tests" OFF)
|
||||
if(BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
# 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})
|
||||
endif()
|
||||
|
||||
@@ -26,6 +26,8 @@ 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
|
||||
@@ -274,6 +276,44 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||
}
|
||||
|
||||
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()
|
||||
Log.d(TAG, "Pause Amnezia activity")
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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
|
||||
@@ -11,7 +14,25 @@ private const val TAG = "TvFilePicker"
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
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
|
||||
}
|
||||
}) {
|
||||
setResult(RESULT_OK, Intent().apply { data = it })
|
||||
finish()
|
||||
}
|
||||
@@ -31,7 +52,7 @@ class TvFilePicker : ComponentActivity() {
|
||||
private fun getFile() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch("*/*")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
|
||||
@@ -83,6 +83,26 @@ 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(
|
||||
|
||||
@@ -23,12 +23,6 @@ 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
|
||||
@@ -74,11 +68,6 @@ 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
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace apiDefs
|
||||
constexpr QLatin1String adEndpoint("ad_endpoint");
|
||||
}
|
||||
|
||||
const int requestTimeoutMsecs = 30 * 1000; // 30 secs (increased for DNS transport testing)
|
||||
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
}
|
||||
|
||||
#endif // APIDEFS_H
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
#include "gatewayController.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMutexLocker>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <QtConcurrent>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QUrl>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
|
||||
#include "amnezia_application.h"
|
||||
#include "core/transport/dnsGatewayTransport.h"
|
||||
#include "core/transport/httpGatewayTransport.h"
|
||||
#include "core/api/apiUtils.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include "utilities.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/ipcclient.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace configKey
|
||||
@@ -29,330 +36,630 @@ namespace
|
||||
constexpr char keyPayload[] = "key_payload";
|
||||
}
|
||||
|
||||
amnezia::transport::dns::DnsProtocol dnsProtocolFromPrimary(PrimaryTransport p)
|
||||
{
|
||||
switch (p) {
|
||||
case PrimaryTransport::DnsUdp: return amnezia::transport::dns::DnsProtocol::Udp;
|
||||
case PrimaryTransport::DnsTcp: return amnezia::transport::dns::DnsProtocol::Tcp;
|
||||
case PrimaryTransport::DnsDot: return amnezia::transport::dns::DnsProtocol::Tls;
|
||||
case PrimaryTransport::DnsDoh: return amnezia::transport::dns::DnsProtocol::Https;
|
||||
case PrimaryTransport::DnsDoq: return amnezia::transport::dns::DnsProtocol::Quic;
|
||||
default: return amnezia::transport::dns::DnsProtocol::Udp;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
|
||||
TransportsConfig TransportsConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
using amnezia::transport::dns::DnsProtocol;
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
|
||||
TransportsConfig config;
|
||||
constexpr int httpStatusCodeNotFound = 404;
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
|
||||
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;
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint,
|
||||
const bool isDevEnvironment,
|
||||
const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled,
|
||||
QObject *parent)
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
{
|
||||
auto httpTransport = std::make_shared<amnezia::transport::HttpGatewayTransport>(
|
||||
m_gatewayEndpoint, m_isDevEnvironment, m_requestTimeoutMsecs, m_isStrictKillSwitchEnabled);
|
||||
{
|
||||
QMutexLocker lock(&m_transportMutex);
|
||||
m_transport = std::move(httpTransport);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::buildTransport(
|
||||
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled)
|
||||
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
|
||||
{
|
||||
using namespace amnezia::transport;
|
||||
EncryptedRequestData encRequestData;
|
||||
encRequestData.errorCode = ErrorCode::NoError;
|
||||
|
||||
auto makeHttp = [&](const QString &httpEndpoint) {
|
||||
return std::make_shared<HttpGatewayTransport>(
|
||||
httpEndpoint, isDevEnvironment, requestTimeoutMsecs, isStrictKillSwitchEnabled);
|
||||
};
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
if (config.primary == PrimaryTransport::Http) {
|
||||
return makeHttp(config.httpEndpoint);
|
||||
}
|
||||
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());
|
||||
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||
|
||||
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);
|
||||
// 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";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
#endif
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.key = blockCipher.generatePrivateSalt(32);
|
||||
result.iv = blockCipher.generatePrivateSalt(16);
|
||||
result.salt = blockCipher.generatePrivateSalt(8);
|
||||
encRequestData.key = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.iv = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.salt = blockCipher.generatePrivateSalt(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[configKey::aesKey] = QString(result.key.toBase64());
|
||||
keyPayload[configKey::aesIv] = QString(result.iv.toBase64());
|
||||
keyPayload[configKey::aesSalt] = QString(result.salt.toBase64());
|
||||
keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
|
||||
keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
|
||||
keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||
|
||||
QByteArray encryptedKeyPayload;
|
||||
QByteArray encryptedApiPayload;
|
||||
try {
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
|
||||
EVP_PKEY *publicKey = nullptr;
|
||||
try {
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
rsaKey = rsaKey.trimmed();
|
||||
rsaKey.replace("\\n", "\n");
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
result.errorCode = amnezia::ErrorCode::ApiMissingAgwPublicKey;
|
||||
return result;
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(QJsonDocument::Compact),
|
||||
publicKey, RSA_PKCS1_PADDING);
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(QJsonDocument::Compact),
|
||||
result.key, result.iv, "", result.salt);
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
|
||||
"", encRequestData.salt);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when encrypting the request body";
|
||||
result.errorCode = amnezia::ErrorCode::ApiConfigDecryptionError;
|
||||
return result;
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
QJsonObject requestBody;
|
||||
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||
|
||||
result.body = QJsonDocument(requestBody).toJson(QJsonDocument::Compact);
|
||||
return result;
|
||||
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
amnezia::transport::DecryptionResult GatewayController::decryptResponse(const QByteArray &encryptedResponseBody,
|
||||
const QByteArray &key,
|
||||
const QByteArray &iv,
|
||||
const QByteArray &salt) const
|
||||
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
{
|
||||
amnezia::transport::DecryptionResult result;
|
||||
result.decrypted = encryptedResponseBody;
|
||||
result.isOk = false;
|
||||
|
||||
if (encryptedResponseBody.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
DecryptionResult result;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decrypted = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
result.isOk = true;
|
||||
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
result.isDecryptionSuccessful = true;
|
||||
} catch (...) {
|
||||
result.decrypted = encryptedResponseBody;
|
||||
result.isOk = false;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::currentTransport() const
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
QMutexLocker lock(&m_transportMutex);
|
||||
return m_transport;
|
||||
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;
|
||||
}
|
||||
|
||||
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
EncryptedRequest enc = encryptRequest(apiPayload);
|
||||
if (enc.errorCode != amnezia::ErrorCode::NoError) {
|
||||
return enc.errorCode;
|
||||
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();
|
||||
}
|
||||
|
||||
auto transport = currentTransport();
|
||||
if (!transport) {
|
||||
return amnezia::ErrorCode::AmneziaServiceConnectionFailed;
|
||||
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 = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
|
||||
+ ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls)
|
||||
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
const QList<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 = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
auto decryptionHook = [this, key = enc.key, iv = enc.iv, salt = enc.salt](const QByteArray &encrypted) {
|
||||
return decryptResponse(encrypted, key, iv, salt);
|
||||
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;
|
||||
};
|
||||
|
||||
return transport->send(endpoint, enc.body, responseBody, decryptionHook);
|
||||
if (m_proxyUrl.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()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
m_proxyUrl = proxyUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
return QtConcurrent::run([this, endpoint, apiPayload]() {
|
||||
QByteArray responseBody;
|
||||
amnezia::ErrorCode errorCode = post(endpoint, apiPayload, responseBody);
|
||||
return qMakePair(errorCode, responseBody);
|
||||
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 (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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,87 +2,69 @@
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <QFuture>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <memory>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "core/defs.h"
|
||||
#include "core/transport/dns/dnsResolver.h"
|
||||
#include "core/transport/igatewaytransport.h"
|
||||
|
||||
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);
|
||||
};
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
class GatewayController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint,
|
||||
const bool isDevEnvironment,
|
||||
const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled,
|
||||
QObject *parent = nullptr);
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
|
||||
static TransportsConfig buildTransportsConfig();
|
||||
void setTransportsConfig(const TransportsConfig &config);
|
||||
|
||||
private:
|
||||
struct EncryptedRequest
|
||||
struct EncryptedRequestData
|
||||
{
|
||||
QByteArray body;
|
||||
QNetworkRequest request;
|
||||
QByteArray requestBody;
|
||||
QByteArray key;
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode = amnezia::ErrorCode::NoError;
|
||||
amnezia::ErrorCode errorCode;
|
||||
};
|
||||
|
||||
EncryptedRequest encryptRequest(const QJsonObject &apiPayload);
|
||||
amnezia::transport::DecryptionResult decryptResponse(const QByteArray &encryptedResponseBody,
|
||||
const QByteArray &key,
|
||||
const QByteArray &iv,
|
||||
const QByteArray &salt) const;
|
||||
struct DecryptionResult
|
||||
{
|
||||
QByteArray decryptedBody;
|
||||
bool isDecryptionSuccessful;
|
||||
};
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
|
||||
mutable QMutex m_transportMutex;
|
||||
std::shared_ptr<amnezia::transport::IGatewayTransport> m_transport;
|
||||
inline static QString m_proxyUrl;
|
||||
};
|
||||
|
||||
#endif // GATEWAYCONTROLLER_H
|
||||
|
||||
@@ -419,6 +419,18 @@ 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"))
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace amnezia
|
||||
ServerDockerOnCgroupsV2 = 211,
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
|
||||
@@ -29,6 +29,7 @@ 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;
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QHostInfo>
|
||||
#include <QDebug>
|
||||
|
||||
QRegularExpression NetworkUtilities::ipAddressRegExp()
|
||||
{
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
#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
|
||||
@@ -1,38 +0,0 @@
|
||||
#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
|
||||
@@ -1,354 +0,0 @@
|
||||
#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
|
||||
@@ -1,29 +0,0 @@
|
||||
#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
|
||||
@@ -1,817 +0,0 @@
|
||||
#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
|
||||
@@ -1,35 +0,0 @@
|
||||
#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
|
||||
@@ -1,157 +0,0 @@
|
||||
#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
|
||||
@@ -1,49 +0,0 @@
|
||||
#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
|
||||
@@ -1,345 +0,0 @@
|
||||
#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
|
||||
@@ -1,58 +0,0 @@
|
||||
#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
|
||||
@@ -1,36 +0,0 @@
|
||||
#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
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -270,12 +270,7 @@ 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::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()) {
|
||||
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).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));
|
||||
|
||||
@@ -42,6 +42,65 @@ ErrorCode XrayProtocol::start()
|
||||
return startTun2Sock();
|
||||
}
|
||||
|
||||
ErrorCode XrayProtocol::setupRouting() {
|
||||
return IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
|
||||
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 AMNEZIA_DESKTOP
|
||||
#ifdef Q_OS_MACOS
|
||||
const QString tunName = "utun22";
|
||||
#else
|
||||
const QString tunName = "tun2";
|
||||
#endif
|
||||
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
|
||||
if (!createTun.waitForFinished(1000) || !createTun.returnValue()) {
|
||||
qWarning() << "Failed to assign IP address for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto updateResolvers = iface->updateResolvers(tunName, dnsAddr);
|
||||
if (!updateResolvers.waitForFinished(1000) || !updateResolvers.returnValue()) {
|
||||
qWarning() << "Failed to set DNS resolvers for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
#endif
|
||||
|
||||
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(1000) || routeAddList.returnValue() != subnets.count()) {
|
||||
qWarning() << "Failed to set routes for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
|
||||
if (!StopRoutingIpv6.waitForFinished(1000) || !StopRoutingIpv6.returnValue()) {
|
||||
qWarning() << "Failed to disable IPv6 routing";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
auto enablePeerTraffic = iface->enablePeerTraffic(m_xrayConfig);
|
||||
if (!enablePeerTraffic.waitForFinished(5000) || !enablePeerTraffic.returnValue()) {
|
||||
qWarning() << "Failed to enable peer traffic";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
#endif
|
||||
return ErrorCode::NoError;
|
||||
},
|
||||
[] () {
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
});
|
||||
}
|
||||
|
||||
ErrorCode XrayProtocol::startTun2Sock()
|
||||
{
|
||||
m_t2sProcess->start();
|
||||
@@ -50,48 +109,23 @@ ErrorCode XrayProtocol::startTun2Sock()
|
||||
[&](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) {
|
||||
QMetaObject::invokeMethod(this, [this, vpnState]() {
|
||||
qDebug() << "PrivilegedProcess setConnectionState " << vpnState;
|
||||
|
||||
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 (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(res);
|
||||
} else
|
||||
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
|
||||
});
|
||||
|
||||
if (vpnState == Vpn::ConnectionState::Disconnected)
|
||||
stop();
|
||||
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -103,19 +137,19 @@ void XrayProtocol::stop()
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
QRemoteObjectPendingReply<bool> StartRoutingIpv6Resp = iface->StartRoutingIpv6();
|
||||
if (!StartRoutingIpv6Resp.waitForFinished(1000)) {
|
||||
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
|
||||
if (!StartRoutingIpv6.waitForFinished(1000) || !StartRoutingIpv6.returnValue()) {
|
||||
qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6";
|
||||
}
|
||||
|
||||
QRemoteObjectPendingReply<bool> restoreResolvers = iface->restoreResolvers();
|
||||
if (!restoreResolvers.waitForFinished(1000)) {
|
||||
auto restoreResolvers = iface->restoreResolvers();
|
||||
if (!restoreResolvers.waitForFinished(1000) || !restoreResolvers.returnValue()) {
|
||||
qWarning() << "XrayProtocol::stop(): Failed to restore resolvers";
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_MACOS)
|
||||
QRemoteObjectPendingReply<bool> deleteTunResp = iface->deleteTun("tun2");
|
||||
if (!deleteTunResp.waitForFinished(1000)) {
|
||||
auto deleteTun = iface->deleteTun("tun2");
|
||||
if (!deleteTun.waitForFinished(1000) || !deleteTun.returnValue()) {
|
||||
qWarning() << "XrayProtocol::stop(): Failed to delete tun";
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -14,10 +14,11 @@ public:
|
||||
virtual ~XrayProtocol() override;
|
||||
|
||||
ErrorCode start() override;
|
||||
ErrorCode startTun2Sock();
|
||||
void stop() override;
|
||||
|
||||
private:
|
||||
ErrorCode setupRouting();
|
||||
ErrorCode startTun2Sock();
|
||||
void readXrayConfiguration(const QJsonObject &configuration);
|
||||
|
||||
QJsonObject m_xrayConfig;
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
<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>
|
||||
|
||||
@@ -21,4 +21,5 @@ 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
|
||||
docker --version;\
|
||||
uname -sr
|
||||
|
||||
@@ -14,8 +14,7 @@ namespace
|
||||
const char cloudFlareNs1[] = "1.1.1.1";
|
||||
const char cloudFlareNs2[] = "1.0.0.1";
|
||||
|
||||
//constexpr char gatewayEndpoint[] = "http://localhost:80/";
|
||||
constexpr char gatewayEndpoint[] = "http://localhost:80/";
|
||||
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
|
||||
}
|
||||
|
||||
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
|
||||
|
||||
@@ -94,6 +94,15 @@ 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();
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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})
|
||||
@@ -1,406 +0,0 @@
|
||||
#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
@@ -5,7 +5,6 @@
|
||||
#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"
|
||||
@@ -383,6 +382,51 @@ 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);
|
||||
@@ -724,6 +768,9 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||
auto installationUuid = m_settings->getInstallationUuid(true);
|
||||
|
||||
@@ -739,17 +786,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
||||
apiPayload[configKey::apiEndpoint] = serverConfig.value(configKey::apiEndpoint).toString();
|
||||
|
||||
QByteArray 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);
|
||||
ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = fillServerConfig(serviceProtocol, protocolData, responseBody, serverConfig);
|
||||
@@ -956,12 +993,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
||||
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
|
||||
bool isTestPurchase)
|
||||
{
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase),
|
||||
m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
|
||||
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#include "apiNewsController.h"
|
||||
|
||||
#include "core/api/apiUtils.h"
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
@@ -33,6 +31,8 @@ void ApiNewsController::fetchNews(bool showError)
|
||||
return;
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
||||
|
||||
@@ -44,35 +44,26 @@ void ApiNewsController::fetchNews(bool showError)
|
||||
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
m_newsModel->updateModel(newsArray);
|
||||
emit fetchNewsFinished();
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
#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
|
||||
@@ -60,6 +56,8 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
auto authData = serverConfig.value(configKey::authData).toObject();
|
||||
|
||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||
@@ -69,18 +67,8 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
|
||||
|
||||
QByteArray 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);
|
||||
|
||||
|
||||
ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
|
||||
@@ -308,6 +308,15 @@ 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();
|
||||
|
||||
@@ -73,6 +73,9 @@ public slots:
|
||||
bool isStartMinimizedEnabled();
|
||||
void toggleStartMinimized(bool enable);
|
||||
|
||||
bool isNewsNotificationsEnabled();
|
||||
void toggleNewsNotificationsEnabled(bool enable);
|
||||
|
||||
bool isScreenshotsEnabled();
|
||||
void toggleScreenshotsEnabled(bool enable);
|
||||
|
||||
|
||||
@@ -112,7 +112,11 @@ 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");
|
||||
|
||||
38
client/ui/qml/Components/GamepadLoader.qml
Normal file
38
client/ui/qml/Components/GamepadLoader.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,11 +111,11 @@ Button {
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
if (root.pressed) {
|
||||
return pressedColor
|
||||
return root.pressedColor
|
||||
}
|
||||
return root.hovered ? hoveredColor : defaultColor
|
||||
return root.hovered ? root.hoveredColor : root.defaultColor
|
||||
} else {
|
||||
return disabledColor
|
||||
return root.disabledColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,55 @@ 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
|
||||
|
||||
@@ -93,6 +142,8 @@ Item {
|
||||
|
||||
aboutToHide()
|
||||
|
||||
setParentInteractive(true)
|
||||
|
||||
closed()
|
||||
}
|
||||
|
||||
@@ -118,6 +169,8 @@ Item {
|
||||
|
||||
root.aboutToShow()
|
||||
|
||||
setParentInteractive(false)
|
||||
|
||||
root.opened()
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ Item {
|
||||
implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: root.enabled
|
||||
@@ -296,13 +298,13 @@ Item {
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (clickedFunction && typeof clickedFunction === "function") {
|
||||
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
|
||||
clickedFunction()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (clickedFunction && typeof clickedFunction === "function") {
|
||||
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
|
||||
clickedFunction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ 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
|
||||
@@ -70,7 +67,7 @@ Item {
|
||||
border.width: 1
|
||||
|
||||
Behavior on border.color {
|
||||
PropertyAnimation { duration: 100 }
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -124,7 +121,7 @@ Item {
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor
|
||||
color: root.backgroundDisabledColor
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
@@ -189,14 +186,6 @@ 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
|
||||
@@ -204,7 +193,7 @@ Item {
|
||||
|
||||
height: content.implicitHeight
|
||||
width: content.implicitHeight
|
||||
squareLeftSide: false
|
||||
squareLeftSide: true
|
||||
|
||||
clickedFunc: function() {
|
||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||
|
||||
@@ -140,6 +140,16 @@ 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
|
||||
@@ -147,13 +157,14 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cipherDropDown.text = cipher
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||
selectedIndex = i
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
cipherListView.updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,16 @@ 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
|
||||
@@ -199,13 +209,14 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
hashDropDown.text = hash
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < hashListView.model.count; i++) {
|
||||
if (hashListView.model.get(i).name === hashDropDown.text) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
hashListView.updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,6 +253,16 @@ 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
|
||||
@@ -249,13 +270,14 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cipherDropDown.text = cipher
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
cipherListView.updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,16 @@ 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
|
||||
@@ -116,13 +126,14 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cipherDropDown.text = cipher
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
cipherListView.updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ PageType {
|
||||
id: news
|
||||
|
||||
property string title: qsTr("News & Notifications")
|
||||
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||
readonly property string leftImagePath: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled() ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||
property bool isVisible: ServersModel.hasServersFromGatewayApi
|
||||
readonly property var clickedHandler: function() {
|
||||
if (!ServersModel.hasServersFromGatewayApi) {
|
||||
|
||||
@@ -224,7 +224,6 @@ PageType {
|
||||
height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
|
||||
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
opacity: 0.8
|
||||
|
||||
RowLayout {
|
||||
id: addAppButton
|
||||
|
||||
@@ -168,6 +168,29 @@ 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 {
|
||||
|
||||
@@ -240,7 +240,6 @@ PageType {
|
||||
height: addSiteButton.implicitHeight + 48
|
||||
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
opacity: 0.8
|
||||
|
||||
RowLayout {
|
||||
id: addSiteButton
|
||||
|
||||
@@ -97,16 +97,32 @@ 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: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("Connect")
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
@@ -121,6 +137,37 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ PageType {
|
||||
|
||||
onClicked: function() {
|
||||
isEasySetup = true
|
||||
checked = true
|
||||
var defaultContainerProto = ContainerProps.defaultProtocol(dockerContainer)
|
||||
|
||||
listView.dockerContainer = dockerContainer
|
||||
|
||||
@@ -383,7 +383,7 @@ PageType {
|
||||
objectName: "settingsTabButton"
|
||||
|
||||
isSelected: tabBar.currentIndex === 2
|
||||
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
|
||||
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled()) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
|
||||
Binding {
|
||||
target: settingsTabButton
|
||||
property: "defaultColor"
|
||||
|
||||
@@ -83,6 +83,11 @@ Window {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Qt.platform.os === "android"
|
||||
source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
objectName: "pageControllerConnections"
|
||||
|
||||
|
||||
@@ -499,7 +499,7 @@ bool VpnConnection::startNetworkCheckIfReady()
|
||||
|
||||
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->startNetworkCheck(gateway, localAddress);
|
||||
return reply.waitForFinished() && reply.returnValue();
|
||||
return reply.waitForFinished(1000) && reply.returnValue();
|
||||
});
|
||||
#else
|
||||
return false;
|
||||
|
||||
@@ -31,6 +31,7 @@ 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:"=%
|
||||
@@ -43,6 +44,7 @@ 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%"
|
||||
@@ -74,7 +76,7 @@ if %errorlevel% neq 0 exit /b %errorlevel%
|
||||
echo "Deploying..."
|
||||
|
||||
mkdir "%OUT_APP_DIR%"
|
||||
copy "%WORK_DIR%\service\server\release\%APP_NAME%-service.exe" "%OUT_APP_DIR%"
|
||||
copy "%WORK_DIR%\service\server\release\%SERVICE_FILENAME%" "%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
|
||||
@@ -83,7 +85,8 @@ 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 "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
|
||||
"%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:"=%"
|
||||
|
||||
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>Sockets</key>
|
||||
<dict>
|
||||
<key>Listeners</key>
|
||||
|
||||
@@ -7,42 +7,54 @@ 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
|
||||
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"
|
||||
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"
|
||||
fi
|
||||
|
||||
if launchctl list "$APP_NAME-service" &> /dev/null; then
|
||||
launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
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"
|
||||
|
||||
sudo chmod -R a-w "$APP_PATH/"
|
||||
sudo chown -R root "$APP_PATH/"
|
||||
sudo chgrp -R wheel "$APP_PATH/"
|
||||
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/"
|
||||
|
||||
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'
|
||||
log "Requesting ${APP_NAME} to quit gracefully"
|
||||
run_cmd osascript -e 'tell application "AmneziaVPN" to quit' || true
|
||||
|
||||
PLIST_SOURCE="$APP_PATH/Contents/Resources/$PLIST_NAME"
|
||||
if [ -f "$PLIST_SOURCE" ]; then
|
||||
mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME" 2>> $LOG_FILE
|
||||
run_cmd mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
else
|
||||
echo "`date` ERROR: service plist not found at $PLIST_SOURCE" >> $LOG_FILE
|
||||
log "ERROR: service plist not found at $PLIST_SOURCE"
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
echo "`date` Service status: $?" >> $LOG_FILE
|
||||
echo "`date` Script finished" >> $LOG_FILE
|
||||
log "Script finished"
|
||||
|
||||
@@ -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 unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
sudo launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
fi
|
||||
sudo rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if [[ $VERSION = '' ]]; then
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo '::error::VERSION does not set. Exiting with error...'
|
||||
exit 1
|
||||
fi
|
||||
@@ -14,25 +14,39 @@ 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
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
cd ../
|
||||
|
||||
rclone sync ./dist/ r2:/updates/
|
||||
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!"
|
||||
|
||||
@@ -29,9 +29,7 @@ void IpcProcessTun2Socks::start()
|
||||
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)});
|
||||
QStringList arguments({"-device", "tun://tun2?guid={081A8A84-8D12-4DF5-B8C4-396D5B0053E4}", "-proxy", XrayConStr });
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr});
|
||||
@@ -47,8 +45,6 @@ void IpcProcessTun2Socks::start()
|
||||
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")) {
|
||||
|
||||
@@ -6,6 +6,13 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION})
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(LINUX)
|
||||
set(CMAKE_BUILD_RPATH "\$ORIGIN/../lib")
|
||||
set(CMAKE_INSTALL_RPATH "\$ORIGIN/../lib")
|
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE)
|
||||
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
endif()
|
||||
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_subdirectory(server)
|
||||
endif()
|
||||
|
||||
@@ -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)
|
||||
find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat Concurrent)
|
||||
qt_standard_project_setup()
|
||||
|
||||
|
||||
@@ -353,7 +353,17 @@ 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 ${LIBS})
|
||||
|
||||
if(LINUX)
|
||||
target_link_options(${PROJECT} PRIVATE "LINKER:--disable-new-dtags")
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
BUILD_RPATH "\$ORIGIN/../lib"
|
||||
INSTALL_RPATH "\$ORIGIN/../lib"
|
||||
INSTALL_RPATH_USE_LINK_PATH FALSE
|
||||
)
|
||||
set_property(TARGET ${PROJECT} PROPERTY BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
endif()
|
||||
target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus Qt6::Concurrent ${LIBS})
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
|
||||
@@ -66,6 +66,9 @@ 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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <tchar.h>
|
||||
|
||||
#include <QProcess>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include <core/networkUtilities.h>
|
||||
|
||||
@@ -308,6 +309,37 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RouterWin::suspendWcmSvc(bool suspend)
|
||||
{
|
||||
if (suspend == m_suspended) return;
|
||||
@@ -465,11 +497,19 @@ bool RouterWin::StopRoutingIpv6()
|
||||
qDebug() << "RouterWin::StopRoutingIpv6";
|
||||
|
||||
if (auto loopback = findLoopbackIface(); loopback.isValid()) {
|
||||
for (auto subnet : kIpv6Subnets) {
|
||||
QProcess{}.execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" });
|
||||
}
|
||||
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();
|
||||
}
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RouterWin::StartRoutingIpv6()
|
||||
@@ -477,9 +517,14 @@ bool RouterWin::StartRoutingIpv6()
|
||||
qDebug() << "RouterWin::StartRoutingIpv6";
|
||||
|
||||
if (auto loopback = findLoopbackIface(); loopback.isValid()) {
|
||||
for (auto subnet : kIpv6Subnets) {
|
||||
QProcess{}.execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) });
|
||||
}
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ 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();
|
||||
|
||||
Reference in New Issue
Block a user