diff --git a/.github/workflows/tag-upload.yml b/.github/workflows/tag-upload.yml index bc88933d..11ad7524 100644 --- a/.github/workflows/tag-upload.yml +++ b/.github/workflows/tag-upload.yml @@ -24,7 +24,7 @@ jobs: - name: Verify git tag run: | TAG_NAME=${{ inputs.RELEASE_VERSION }} - CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/') + CMAKE_TAG=$(grep 'set(DEFAULTVPN_VERSION' CMakeLists.txt | sed -E 's/.*DEFAULTVPN_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 diff --git a/.gitignore b/.gitignore index 50d648be..02a4ae08 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,6 @@ ios-ne-build.sh macos-ne-build.sh macos-signed-build.sh macos-with-sign-build.sh +DeveloperIdApplicationCertificate.p12 +DeveloperIdInstallerCertificate.p12 + diff --git a/.gitmodules b/.gitmodules index 90edb582..11845060 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb8c9e0..cbf4be65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT DefaultVPN) -set(DEFAULTVPN_VERSION 1.0.6.2) +set(DEFAULTVPN_VERSION 1.0.6.6) project(${PROJECT} VERSION ${DEFAULTVPN_VERSION} DESCRIPTION "DefaultVPN" @@ -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 2105) +set(APP_ANDROID_VERSION_CODE 2112) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") @@ -57,13 +57,16 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE) set(CPACK_GENERATOR "WIX") set(CPACK_WIX_VERSION 4) - set(CPACK_PACKAGE_NAME "AmneziaVPN") - set(CPACK_PACKAGE_VENDOR "AmneziaVPN") - set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION}) - set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client") - set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN") + set(CPACK_PACKAGE_NAME "DefaultVPN") + set(CPACK_PACKAGE_VENDOR "DefaultVPN") + set(CPACK_PACKAGE_VERSION ${DEFAULTVPN_VERSION}) + set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "DefaultVPN client") + set(DEFAULTVPN_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt") + configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${DEFAULTVPN_LICENSE_TXT}" COPYONLY) + set(CPACK_RESOURCE_FILE_LICENSE "${DEFAULTVPN_LICENSE_TXT}") + set(CPACK_PACKAGE_INSTALL_DIRECTORY "DefaultVPN") set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}") - set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN") + set(CPACK_PACKAGE_EXECUTABLES "DefaultVPN" "DefaultVPN") set(CPACK_WIX_UPGRADE_GUID "{2D55AC62-96D6-4692-8C05-0D85BBF95485}") set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/client/images/app.ico") diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 579673b2..568b8d72 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit 579673b2ed044fbe064e3680775dd7773db71386 +Subproject commit 568b8d720dedf3c58e215a029280eb8d0e2fa70e diff --git a/client/3rd/qtgamepad b/client/3rd/qtgamepad new file mode 160000 index 00000000..f72b3e0c --- /dev/null +++ b/client/3rd/qtgamepad @@ -0,0 +1 @@ +Subproject commit f72b3e0c6229d1622b6bc7f6a5ec5ccff83460eb diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0d9ced79..0ad766a0 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -59,7 +59,6 @@ target_include_directories(${PROJECT} PUBLIC if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep) - qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep) endif() qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc) @@ -79,6 +78,7 @@ set(DEFAULTVPN_TS_FILES ) file(GLOB_RECURSE DEFAULTVPN_TS_SOURCES *.qrc *.cpp *.h *.ui) +list(FILTER DEFAULTVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples") qt_create_translation(DEFAULTVPN_QM_FILES ${DEFAULTVPN_TS_SOURCES} ${DEFAULTVPN_TS_FILES}) @@ -228,4 +228,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}) + +# 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() diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 66600dde..c1b29e1d 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -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 @@ -73,6 +75,8 @@ private const val OPEN_FILE_ACTION_CODE = 3 private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4 private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED" +private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L +private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri" class AmneziaActivity : QtActivity() { @@ -88,6 +92,12 @@ class AmneziaActivity : QtActivity() { private val actionResultHandlers = mutableMapOf() private val permissionRequestHandlers = mutableMapOf() + + private var isActivityResumed = false + private var hasWindowFocus = false + private val resumeHandler = Handler(Looper.getMainLooper()) + private var pendingOpenFileUri: String? = null + private var openFileDeliveryScheduled = false private val vpnServiceEventHandler: Handler by lazy(NONE) { object : Handler(Looper.getMainLooper()) { @@ -190,11 +200,18 @@ class AmneziaActivity : QtActivity() { doBindService() } ) + pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI) + openFileDeliveryScheduled = false registerBroadcastReceivers() intent?.let(::processIntent) runBlocking { vpnProto = proto.await() } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + pendingOpenFileUri?.let { outState.putString(KEY_PENDING_OPEN_FILE_URI, it) } + } + private fun loadLibs() { listOf( "rsapss", @@ -260,6 +277,11 @@ class AmneziaActivity : QtActivity() { } override fun onStop() { + isActivityResumed = false + hasWindowFocus = false + // Cancel all pending operations when activity stops + resumeHandler.removeCallbacksAndMessages(null) + openFileDeliveryScheduled = false Log.d(TAG, "Stop Amnezia activity") doUnbindService() mainScope.launch { @@ -271,35 +293,140 @@ class AmneziaActivity : QtActivity() { override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) + hasWindowFocus = hasFocus Log.d(TAG, "Window focus changed: hasFocus=$hasFocus") + + // Cancel pending operations if window loses focus + if (!hasFocus) { + resumeHandler.removeCallbacksAndMessages(null) + } } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val deviceId = event.deviceId + val keyCode = event.keyCode + val pressed = event.action == KeyEvent.ACTION_DOWN + val source = event.source + + if (deviceId < 0 && pressed) { + when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT -> { + nativeGamepadKeyEvent(0, keyCode, true) + nativeGamepadKeyEvent(0, keyCode, false) + return true + } + KeyEvent.KEYCODE_DPAD_CENTER -> { + if (isOnTv()) { + val down = KeyEvent( + event.downTime, + event.eventTime, + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ENTER, + 0, + event.metaState, + 0, + event.scanCode, + event.flags, + event.source + ) + val up = KeyEvent( + event.downTime, + event.eventTime, + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_ENTER, + 0, + event.metaState, + 0, + event.scanCode, + event.flags, + event.source + ) + super.dispatchKeyEvent(down) + super.dispatchKeyEvent(up) + return true + } + nativeGamepadKeyEvent(0, keyCode, true) + nativeGamepadKeyEvent(0, keyCode, false) + return true + } + } + } + + // Real gamepad events (deviceId >= 0) + if (deviceId >= 0) { + val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD + val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK + val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD + if (isGamepad || isJoystick || isDpad) { + nativeGamepadKeyEvent(deviceId, keyCode, pressed) + return true + } + } + + return super.dispatchKeyEvent(event) + } + + private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean) + override fun onPause() { super.onPause() + isActivityResumed = false + // Cancel all pending operations when activity pauses + resumeHandler.removeCallbacksAndMessages(null) + openFileDeliveryScheduled = false Log.d(TAG, "Pause Amnezia activity") } override fun onResume() { super.onResume() - /* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + isActivityResumed = true + Log.d(TAG, "Resume Amnezia activity") + + if (pendingOpenFileUri != null && !openFileDeliveryScheduled) { + val uri = pendingOpenFileUri!! + openFileDeliveryScheduled = true + resumeHandler.postDelayed({ + if (!isFinishing && !isDestroyed) { + pendingOpenFileUri = null + openFileDeliveryScheduled = false + mainScope.launch { + qtInitialized.await() + QtAndroidController.onFileOpened(uri) + } + } + }, OPEN_FILE_AFTER_RESUME_DELAY_MS) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { window.decorView.apply { invalidate() - postDelayed({ - sendTouch(1f, 1f) + resumeHandler.postDelayed({ + // Check if activity is still resumed and has focus before executing + if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { + sendTouch(1f, 1f) + } }, 100) - postDelayed({ - sendTouch(2f, 2f) + resumeHandler.postDelayed({ + if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { + sendTouch(2f, 2f) + } }, 200) - postDelayed({ - requestLayout() - invalidate() + resumeHandler.postDelayed({ + if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { + requestLayout() + invalidate() + } }, 250) } - } */ - Log.d(TAG, "Resume Amnezia activity") + } } private fun configureWindowForEdgeToEdge() { @@ -362,6 +489,10 @@ class AmneziaActivity : QtActivity() { } override fun onDestroy() { + isActivityResumed = false + hasWindowFocus = false + // Cancel all pending operations when activity is destroyed + resumeHandler.removeCallbacksAndMessages(null) Log.d(TAG, "Destroy Amnezia activity") unregisterBroadcastReceiver(notificationStateReceiver) notificationStateReceiver = null @@ -687,9 +818,13 @@ class AmneziaActivity : QtActivity() { grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION) }?.toString() ?: "" Log.v(TAG, "Open file: $uri") - mainScope.launch { - qtInitialized.await() - QtAndroidController.onFileOpened(uri) + if (uri.isNotEmpty()) { + pendingOpenFileUri = uri + } else { + mainScope.launch { + qtInitialized.await() + QtAndroidController.onFileOpened(uri) + } } } )) diff --git a/client/android/src/org/amnezia/vpn/TvFilePicker.kt b/client/android/src/org/amnezia/vpn/TvFilePicker.kt index 33f4355f..a3c7ec8a 100644 --- a/client/android/src/org/amnezia/vpn/TvFilePicker.kt +++ b/client/android/src/org/amnezia/vpn/TvFilePicker.kt @@ -33,7 +33,10 @@ class TvFilePicker : ComponentActivity() { return intent } }) { - setResult(RESULT_OK, Intent().apply { data = it }) + setResult(RESULT_OK, Intent().apply { + data = it + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + }) finish() } diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 4272290d..2c3145d7 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -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( diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 8fe198ce..a2fa9a9b 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -181,7 +181,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/ipcclient.h - ${CLIENT_ROOT_DIR}/core/privileged_process.h ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h ${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h @@ -194,7 +193,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/ipcclient.cpp - ${CLIENT_ROOT_DIR}/core/privileged_process.cpp ${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 0b86e626..7a47f1ca 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -135,7 +135,7 @@ void CoreController::initControllers() new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); - m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); + m_sitesController.reset(new SitesController(m_settings, m_sitesModel)); m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel)); diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 132af459..25a40c46 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -337,6 +337,9 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS } else { baseUrls = QString(PROD_S3_ENDPOINT).split(", "); } + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(baseUrls.begin(), baseUrls.end(), generator); QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index 4586b352..d5ee3d7d 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -7,7 +7,6 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent) { m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl())); m_interface.reset(m_node.acquire()); - m_tun2socks.reset(m_node.acquire()); } IpcClient& IpcClient::Instance() @@ -33,68 +32,43 @@ QSharedPointer IpcClient::Interface() return rep; } -QSharedPointer IpcClient::InterfaceTun2Socks() +QSharedPointer IpcClient::CreatePrivilegedProcess() { - QSharedPointer rep = Instance().m_tun2socks; - if (rep.isNull()) { - qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined"; - return nullptr; - } - if (!rep->waitForSource(1000)) { - qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica"; - return nullptr; - } - if (!rep->isReplicaValid()) { - qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid"; - } - return rep; -} - -QSharedPointer IpcClient::CreatePrivilegedProcess() -{ - QSharedPointer rep = Interface(); - if (!rep) { - qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid"; - return nullptr; - } - - QRemoteObjectPendingReply pidReply = rep->createPrivilegedProcess(); - if (!pidReply.waitForFinished(5000)){ - qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call"; - return nullptr; - } - - int pid = pidReply.returnValue(); - QSharedPointer pd(new ProcessDescriptor()); - - pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data())); - - connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() { - pd->replicaNode->addClientSideConnection(pd->localSocket.data()); - - IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire(); - // TODO: rework the unsafe cast below - PrivilegedProcess *priv = static_cast(repl); - pd->ipcProcess.reset(priv); - if (!pd->ipcProcess) { - qWarning() << "Acquire PrivilegedProcess failed"; - } else { - pd->ipcProcess->waitForSource(1000); - if (!pd->ipcProcess->isReplicaValid()) { - qWarning() << "PrivilegedProcess replica is not connected!"; - } - - QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(), - [pd]() { pd->replicaNode->deleteLater(); }); + return withInterface([](QSharedPointer &iface) -> QSharedPointer { + auto createPrivilegedProcess = iface->createPrivilegedProcess(); + if (!createPrivilegedProcess.waitForFinished()) { + qCritical() << "Failed to create privileged process"; + return nullptr; } - }); - pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid)); - if (!pd->localSocket->waitForConnected()) { - qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket"; + const int pid = createPrivilegedProcess.returnValue(); + + auto* node = new QRemoteObjectNode(); + node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid)))); + + QSharedPointer rep( + node->acquire(), + [node] (IpcProcessInterfaceReplica *ptr) { + delete ptr; + node->deleteLater(); + } + ); + if (rep.isNull()) { + qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica"; + return nullptr; + } + if (!rep->waitForSource()) { + qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica"; + return nullptr; + } + if (!rep->isReplicaValid()) { + qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid"; + return nullptr; + } + + return rep; + }, + []() -> QSharedPointer { return nullptr; - } - - auto processReplica = QSharedPointer(pd->ipcProcess); - return processReplica; + }); } diff --git a/client/core/ipcclient.h b/client/core/ipcclient.h index 74f1bf29..ff691919 100644 --- a/client/core/ipcclient.h +++ b/client/core/ipcclient.h @@ -5,9 +5,7 @@ #include #include "rep_ipc_interface_replica.h" -#include "rep_ipc_process_tun2socks_replica.h" - -#include "privileged_process.h" +#include "rep_ipc_process_interface_replica.h" class IpcClient : public QObject { @@ -18,8 +16,7 @@ public: static IpcClient& Instance(); static QSharedPointer Interface(); - static QSharedPointer InterfaceTun2Socks(); - static QSharedPointer CreatePrivilegedProcess(); + static QSharedPointer CreatePrivilegedProcess(); template static auto withInterface(Func func) @@ -54,18 +51,6 @@ signals: private: QRemoteObjectNode m_node; QSharedPointer m_interface; - QSharedPointer m_tun2socks; - - struct ProcessDescriptor { - ProcessDescriptor () { - replicaNode = QSharedPointer(new QRemoteObjectNode()); - ipcProcess = QSharedPointer(); - localSocket = QSharedPointer(); - } - QSharedPointer ipcProcess; - QSharedPointer replicaNode; - QSharedPointer localSocket; - }; }; #endif // IPCCLIENT_H diff --git a/client/core/privileged_process.cpp b/client/core/privileged_process.cpp deleted file mode 100644 index 3852236f..00000000 --- a/client/core/privileged_process.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "privileged_process.h" - -PrivilegedProcess::PrivilegedProcess() : - IpcProcessInterfaceReplica() -{ -} - -PrivilegedProcess::~PrivilegedProcess() -{ - qDebug() << "PrivilegedProcess::~PrivilegedProcess()"; -} - -void PrivilegedProcess::waitForFinished(int msecs) -{ - QSharedPointer loop(new QEventLoop); - connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{ - loop->quit(); - loop.clear(); - }); - - QTimer::singleShot(msecs, this, [this, loop]() mutable { - loop->quit(); - loop.clear(); - }); - - loop->exec(); -} diff --git a/client/core/privileged_process.h b/client/core/privileged_process.h deleted file mode 100644 index 4d08c043..00000000 --- a/client/core/privileged_process.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef PRIVILEGED_PROCESS_H -#define PRIVILEGED_PROCESS_H - -#include - -#include "rep_ipc_process_interface_replica.h" -// This class is dangerous - instance of this class casted from base class, -// so it support only functions -// Do not add any members into it -// -class PrivilegedProcess : public IpcProcessInterfaceReplica -{ - Q_OBJECT -public: - PrivilegedProcess(); - ~PrivilegedProcess() override; - - void waitForFinished(int msecs); - -}; - -#endif // PRIVILEGED_PROCESS_H - - diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 2d902f1a..c90ac65b 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -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)); diff --git a/client/mozilla/networkwatcher.cpp b/client/mozilla/networkwatcher.cpp index c613c106..e6e25b5e 100644 --- a/client/mozilla/networkwatcher.cpp +++ b/client/mozilla/networkwatcher.cpp @@ -72,9 +72,9 @@ void NetworkWatcher::initialize() { connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this, &NetworkWatcher::unsecuredNetwork); connect(m_impl, &NetworkWatcherImpl::networkChanged, this, - &NetworkWatcher::networkChange); - connect(m_impl, &NetworkWatcherImpl::sleepMode, this, - &NetworkWatcher::onSleepMode); + &NetworkWatcher::networkChanged); + connect(m_impl, &NetworkWatcherImpl::wakeup, this, + &NetworkWatcher::wakeup); m_impl->initialize(); // Enable sleep/wake monitoring for VPN auto-reconnection @@ -97,12 +97,6 @@ void NetworkWatcher::settingsChanged() { logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active"; } -void NetworkWatcher::onSleepMode() -{ - logger.debug() << "Resumed from sleep mode"; - emit sleepMode(); -} - void NetworkWatcher::unsecuredNetwork(const QString& networkName, const QString& networkId) { logger.debug() << "Unsecured network:" << logger.sensitive(networkName) diff --git a/client/mozilla/networkwatcher.h b/client/mozilla/networkwatcher.h index f28e74fb..527e28e2 100644 --- a/client/mozilla/networkwatcher.h +++ b/client/mozilla/networkwatcher.h @@ -29,13 +29,11 @@ public: // false to restore. void simulateDisconnection(bool simulatedDisconnection); - void onSleepMode(); - QNetworkInformation::Reachability getReachability(); signals: - void networkChange(); - void sleepMode(); + void networkChanged(); + void wakeup(); private: void settingsChanged(); diff --git a/client/mozilla/networkwatcherimpl.h b/client/mozilla/networkwatcherimpl.h index 54bd8e87..a05c0887 100644 --- a/client/mozilla/networkwatcherimpl.h +++ b/client/mozilla/networkwatcherimpl.h @@ -41,7 +41,7 @@ signals: // TODO: Only windows-networkwatcher has this, the other plattforms should // too. void networkChanged(QString newBSSID); - void sleepMode(); + void wakeup(); private: diff --git a/client/platforms/linux/linuxnetworkwatcher.cpp b/client/platforms/linux/linuxnetworkwatcher.cpp index 22eba3c7..11b31fa5 100644 --- a/client/platforms/linux/linuxnetworkwatcher.cpp +++ b/client/platforms/linux/linuxnetworkwatcher.cpp @@ -41,8 +41,8 @@ void LinuxNetworkWatcher::initialize() { connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this, &LinuxNetworkWatcher::unsecuredNetwork); - connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this, - &NetworkWatcherImpl::sleepMode); + connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this, + &NetworkWatcherImpl::wakeup); // Let's wait a few seconds to allow the UI to be fully loaded and shown. // This is not strictly needed, but it's better for user experience because diff --git a/client/platforms/linux/linuxnetworkwatcherworker.cpp b/client/platforms/linux/linuxnetworkwatcherworker.cpp index 247a1d67..f7cce6bb 100644 --- a/client/platforms/linux/linuxnetworkwatcherworker.cpp +++ b/client/platforms/linux/linuxnetworkwatcherworker.cpp @@ -200,7 +200,7 @@ void LinuxNetworkWatcherWorker::checkDevices() { void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state) { if (state == NM_STATE_ASLEEP) { - emit sleepMode(); + emit wakeup(); } logger.debug() << "NMStateChanged " << state; diff --git a/client/platforms/linux/linuxnetworkwatcherworker.h b/client/platforms/linux/linuxnetworkwatcherworker.h index 9579fcef..7c5ae540 100644 --- a/client/platforms/linux/linuxnetworkwatcherworker.h +++ b/client/platforms/linux/linuxnetworkwatcherworker.h @@ -23,7 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject { signals: void unsecuredNetwork(const QString& networkName, const QString& networkId); - void sleepMode(); + void wakeup(); public slots: void initialize(); diff --git a/client/platforms/macos/macosnetworkwatcher.mm b/client/platforms/macos/macosnetworkwatcher.mm index 67f3a930..c2664566 100644 --- a/client/platforms/macos/macosnetworkwatcher.mm +++ b/client/platforms/macos/macosnetworkwatcher.mm @@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_ case kIOMessageSystemHasPoweredOn: /* Announces that the system and its devices have woken up. */ - logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread"; + logger.debug() << "System has powered on - emitting wakeup signal from dedicated CFRunLoop thread"; if (listener->m_watcher) { // Use QMetaObject::invokeMethod for thread-safe signal emission - QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection); + QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection); } break; diff --git a/client/platforms/windows/daemon/windowsdaemon.cpp b/client/platforms/windows/daemon/windowsdaemon.cpp index 8668b4db..a0b5e18c 100644 --- a/client/platforms/windows/daemon/windowsdaemon.cpp +++ b/client/platforms/windows/daemon/windowsdaemon.cpp @@ -62,6 +62,9 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda } void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) { + if (m_splitTunnelManager == nullptr) + return; + if (config.m_vpnDisabledApps.length() > 0) { m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex); m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps); diff --git a/client/platforms/windows/windowsnetworkwatcher.cpp b/client/platforms/windows/windowsnetworkwatcher.cpp index f72baa6e..85eb4276 100644 --- a/client/platforms/windows/windowsnetworkwatcher.cpp +++ b/client/platforms/windows/windowsnetworkwatcher.cpp @@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM switch (uMsg) { case WM_POWERBROADCAST: if (wParam == PBT_APMRESUMESUSPEND) { - emit obj->sleepMode(); + emit obj->wakeup(); } break; default: diff --git a/client/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index b518aac4..20a3416c 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -232,12 +232,6 @@ ErrorCode OpenVpnProtocol::start() return ErrorCode::AmneziaServiceConnectionFailed; } - m_openVpnProcess->waitForSource(5000); - if (!m_openVpnProcess->isInitialized()) { - qWarning() << "IpcProcess replica is not connected!"; - setLastError(ErrorCode::AmneziaServiceConnectionFailed); - return ErrorCode::AmneziaServiceConnectionFailed; - } m_openVpnProcess->setProgram(PermittedProcess::OpenVPN); QStringList arguments({ "--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort), @@ -246,13 +240,13 @@ ErrorCode OpenVpnProtocol::start() m_openVpnProcess->setArguments(arguments); qDebug() << arguments.join(" "); - connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred, + connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred, [&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; }); - connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged, + connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged, [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); - connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this, + connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this, [&]() { setConnectionState(Vpn::ConnectionState::Disconnected); }); m_openVpnProcess->start(); diff --git a/client/protocols/openvpnprotocol.h b/client/protocols/openvpnprotocol.h index b07d1268..490fff83 100644 --- a/client/protocols/openvpnprotocol.h +++ b/client/protocols/openvpnprotocol.h @@ -53,7 +53,7 @@ private: void updateRouteGateway(QString line); void updateVpnGateway(const QString &line); - QSharedPointer m_openVpnProcess; + QSharedPointer m_openVpnProcess; }; #endif // OPENVPNPROTOCOL_H diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index 869afd92..c1cd868d 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -233,7 +233,7 @@ namespace amnezia constexpr char defaultResponsePacketMagicHeader[] = "3288052141"; constexpr char defaultTransportPacketMagicHeader[] = "2528465083"; constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858"; - constexpr char defaultSpecialJunk1[] = ""; + constexpr char defaultSpecialJunk1[] = ""; constexpr char defaultSpecialJunk2[] = ""; constexpr char defaultSpecialJunk3[] = ""; constexpr char defaultSpecialJunk4[] = ""; diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 1125731d..2ae7ebcb 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -15,7 +15,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * m_impl.reset(new LocalSocketController()); connect(m_impl.get(), &ControllerImpl::connected, this, [this](const QString &pubkey, const QDateTime &connectionTimestamp) { - emit connectionStateChanged(Vpn::ConnectionState::Connected); + setConnectionState(Vpn::ConnectionState::Connected); }); connect(m_impl.get(), &ControllerImpl::statusUpdated, this, [this](const QString& serverIpv4Gateway, @@ -38,7 +38,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * }); connect(m_impl.get(), &ControllerImpl::disconnected, this, - [this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); }); + [this]() { setConnectionState(Vpn::ConnectionState::Disconnected); }); m_impl->initialize(nullptr, nullptr); } diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 575960b2..50bf829a 100755 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -1,6 +1,7 @@ #include "xrayprotocol.h" #include "core/ipcclient.h" +#include "ipc.h" #include "utilities.h" #include "core/networkUtilities.h" @@ -9,14 +10,37 @@ #include #include #include +#include +#include +#include + +#ifdef Q_OS_MACOS +static const QString tunName = "utun22"; +#else +static const QString tunName = "tun2"; +#endif XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent) { - readXrayConfiguration(configuration); - m_routeGateway = NetworkUtilities::getGatewayAndIface().first; m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr; m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr; - m_t2sProcess = IpcClient::InterfaceTun2Socks(); + m_routeGateway = NetworkUtilities::getGatewayAndIface().first; + + m_routeMode = static_cast(configuration.value(amnezia::config_key::splitTunnelType).toInt()); + m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::config_key::hostName).toString()); + + const QString primaryDns = configuration.value(amnezia::config_key::dns1).toString(); + m_dnsServers.push_back(QHostAddress(primaryDns)); + if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) { + const QString secondaryDns = configuration.value(amnezia::config_key::dns2).toString(); + m_dnsServers.push_back(QHostAddress(secondaryDns)); + } + + QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject(); + if (xrayConfiguration.isEmpty()) { + xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject(); + } + m_xrayConfig = xrayConfiguration; } XrayProtocol::~XrayProtocol() @@ -29,72 +53,16 @@ ErrorCode XrayProtocol::start() { qDebug() << "XrayProtocol::start()"; - const ErrorCode err = IpcClient::withInterface([&](QSharedPointer iface) { - iface->xrayStart(QJsonDocument(m_xrayConfig).toJson()); - return ErrorCode::NoError; + return IpcClient::withInterface([&](QSharedPointer iface) { + auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson()); + if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) { + qCritical() << "Failed to start xray"; + return ErrorCode::XrayExecutableCrashed; + } + return startTun2Socks(); }, [] () { return ErrorCode::AmneziaServiceConnectionFailed; }); - if (err != ErrorCode::NoError) - return err; - - setConnectionState(Vpn::ConnectionState::Connecting); - return startTun2Sock(); -} - -ErrorCode XrayProtocol::startTun2Sock() -{ - m_t2sProcess->start(); - - connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this, - [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); - - connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) { - qDebug() << "PrivilegedProcess setConnectionState " << vpnState; - IpcClient::withInterface([&](QSharedPointer iface) { - if (vpnState == Vpn::ConnectionState::Connected) { - setConnectionState(Vpn::ConnectionState::Connecting); - QList dnsAddr; - - dnsAddr.push_back(QHostAddress(m_primaryDNS)); - // We don't use secondary DNS if primary DNS is AmneziaDNS - if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) { - dnsAddr.push_back(QHostAddress(m_secondaryDNS)); - } - #ifdef Q_OS_WIN - QThread::msleep(8000); - #endif - #ifdef Q_OS_MACOS - QThread::msleep(5000); - iface->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr); - iface->updateResolvers("utun22", dnsAddr); - #endif - #ifdef Q_OS_LINUX - QThread::msleep(1000); - iface->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); - iface->updateResolvers("tun2", dnsAddr); - #endif - if (m_routeMode == Settings::RouteMode::VpnAllSites) { - iface->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1"); - } - iface->StopRoutingIpv6(); - #ifdef Q_OS_WIN - iface->updateResolvers("tun2", dnsAddr); - #endif - setConnectionState(Vpn::ConnectionState::Connected); - } - #if !defined(Q_OS_MACOS) - if (vpnState == Vpn::ConnectionState::Disconnected) { - setConnectionState(Vpn::ConnectionState::Disconnected); - iface->deleteTun("tun2"); - iface->StartRoutingIpv6(); - iface->clearSavedRoutes(); - } -#endif - }); - }); - - return ErrorCode::NoError; } void XrayProtocol::stop() @@ -102,43 +70,177 @@ void XrayProtocol::stop() qDebug() << "XrayProtocol::stop()"; IpcClient::withInterface([](QSharedPointer iface) { -#ifdef AMNEZIA_DESKTOP - QRemoteObjectPendingReply StartRoutingIpv6Resp = iface->StartRoutingIpv6(); - if (!StartRoutingIpv6Resp.waitForFinished(1000)) { - qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6"; - } + auto disableKillSwitch = iface->disableKillSwitch(); + if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue()) + qWarning() << "Failed to disable killswitch"; - QRemoteObjectPendingReply restoreResolvers = iface->restoreResolvers(); - if (!restoreResolvers.waitForFinished(1000)) { - qWarning() << "XrayProtocol::stop(): Failed to restore resolvers"; - } + auto StartRoutingIpv6 = iface->StartRoutingIpv6(); + if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue()) + qWarning() << "Failed to start routing ipv6"; - #if !defined(Q_OS_MACOS) - QRemoteObjectPendingReply deleteTunResp = iface->deleteTun("tun2"); - if (!deleteTunResp.waitForFinished(1000)) { - qWarning() << "XrayProtocol::stop(): Failed to delete tun"; - } - #endif -#endif - iface->xrayStop(); + auto restoreResolvers = iface->restoreResolvers(); + if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue()) + qWarning() << "Failed to restore resolvers"; + + auto deleteTun = iface->deleteTun(tunName); + if (!deleteTun.waitForFinished() || !deleteTun.returnValue()) + qWarning() << "Failed to delete tun"; + + auto xrayStop = iface->xrayStop(); + if (!xrayStop.waitForFinished() || !xrayStop.returnValue()) + qWarning() << "Failed to stop xray"; }); - if (m_t2sProcess) { - m_t2sProcess->stop(); - QThread::msleep(200); + if (m_tun2socksProcess) { + m_tun2socksProcess->blockSignals(true); + +#ifndef Q_OS_WIN + m_tun2socksProcess->terminate(); + auto waitForFinished = m_tun2socksProcess->waitForFinished(1000); + if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) { + qWarning() << "Failed to terminate tun2socks. Killing the process..."; + m_tun2socksProcess->kill(); + } +#else + // terminate does not do anything useful on Windows + // so just kill the process + m_tun2socksProcess->kill(); +#endif + + m_tun2socksProcess->close(); + m_tun2socksProcess.reset(); } setConnectionState(Vpn::ConnectionState::Disconnected); } -void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration) +ErrorCode XrayProtocol::startTun2Socks() { - QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject(); - if (xrayConfiguration.isEmpty()) { - xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject(); + m_tun2socksProcess = IpcClient::CreatePrivilegedProcess(); + if (!m_tun2socksProcess->waitForSource()) { + return ErrorCode::AmneziaServiceConnectionFailed; } - m_xrayConfig = xrayConfiguration; - m_routeMode = static_cast(configuration.value(amnezia::config_key::splitTunnelType).toInt()); - m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString(); - m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString(); + + m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks); + m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" }); + + connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() { + auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput(); + if (!readAllStandardOutput.waitForFinished()) { + qWarning() << "Failed to read output from tun2socks"; + return; + } + + const QString line = readAllStandardOutput.returnValue(); + + if (!line.contains("[TCP]") && !line.contains("[UDP]")) + qDebug() << "[tun2socks]:" << line; + + if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) { + disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr); + + if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) { + stop(); + setLastError(res); + } else { + setConnectionState(Vpn::ConnectionState::Connected); + } + } + }, Qt::QueuedConnection); + + connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + if (exitStatus == QProcess::ExitStatus::CrashExit) { + qCritical() << "Tun2socks process crashed!"; + } else { + qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode); + } + stop(); + setLastError(ErrorCode::Tun2SockExecutableCrashed); + }, Qt::QueuedConnection); + + m_tun2socksProcess->start(); + return ErrorCode::NoError; +} + +ErrorCode XrayProtocol::setupRouting() { + return IpcClient::withInterface([this](QSharedPointer iface) -> ErrorCode { +#ifdef Q_OS_WIN + const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress)); +#endif + auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr); + if (!createTun.waitForFinished() || !createTun.returnValue()) { + qCritical() << "Failed to assign IP address for TUN"; + return ErrorCode::InternalError; + } + + auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers); + if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) { + qCritical() << "Failed to set DNS resolvers for TUN"; + return ErrorCode::InternalError; + } + +#ifdef Q_OS_WIN + int vpnAdapterIndex = -1; + QList netInterfaces = QNetworkInterface::allInterfaces(); + for (auto& netInterface : netInterfaces) { + for (auto& address : netInterface.addressEntries()) { + if (m_vpnLocalAddress == address.ip().toString()) + vpnAdapterIndex = netInterface.index(); + } + } +#else + static const int vpnAdapterIndex = 0; +#endif + const bool killSwitchEnabled = QVariant(m_rawConfig.value(config_key::killSwitchOption).toString()).toBool(); + if (killSwitchEnabled) { + if (vpnAdapterIndex != -1) { + QJsonObject config = m_rawConfig; + config.insert("vpnServer", m_remoteAddress); + + auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex); + if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) { + qCritical() << "Failed to enable killswitch"; + return ErrorCode::InternalError; + } + } else + qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled"; + } + + if (m_routeMode == Settings::RouteMode::VpnAllSites) { + static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" }; + + auto routeAddList = iface->routeAddList(m_vpnGateway, subnets); + if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) { + qCritical() << "Failed to set routes for TUN"; + return ErrorCode::InternalError; + } + } + + auto StopRoutingIpv6 = iface->StopRoutingIpv6(); + if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) { + qCritical() << "Failed to disable IPv6 routing"; + return ErrorCode::InternalError; + } + +#ifdef Q_OS_WIN + if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) { + QJsonObject config = m_rawConfig; + config.insert("inetAdapterIndex", inetAdapterIndex); + config.insert("vpnAdapterIndex", vpnAdapterIndex); + config.insert("vpnGateway", m_vpnGateway); + config.insert("vpnServer", m_remoteAddress); + + auto enablePeerTraffic = iface->enablePeerTraffic(config); + if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) { + qCritical() << "Failed to enable peer traffic"; + return ErrorCode::InternalError; + } + } else + qWarning() << "Failed to get adapter indexes. Split-tunneling disabled"; +#endif + return ErrorCode::NoError; + }, + [] () { + return ErrorCode::AmneziaServiceConnectionFailed; + }); } diff --git a/client/protocols/xrayprotocol.h b/client/protocols/xrayprotocol.h index 10f81fbc..bccb844a 100644 --- a/client/protocols/xrayprotocol.h +++ b/client/protocols/xrayprotocol.h @@ -6,6 +6,7 @@ #include "core/ipcclient.h" #include "vpnprotocol.h" #include "settings.h" +#include class XrayProtocol : public VpnProtocol { @@ -14,19 +15,18 @@ public: virtual ~XrayProtocol() override; ErrorCode start() override; - ErrorCode startTun2Sock(); void stop() override; private: - void readXrayConfiguration(const QJsonObject &configuration); - + ErrorCode setupRouting(); + ErrorCode startTun2Socks(); + QJsonObject m_xrayConfig; Settings::RouteMode m_routeMode; - QString m_primaryDNS; - QString m_secondaryDNS; -#ifndef Q_OS_IOS - QSharedPointer m_t2sProcess; -#endif + QList m_dnsServers; + QString m_remoteAddress; + + QSharedPointer m_tun2socksProcess; }; #endif // XRAYPROTOCOL_H diff --git a/client/resources.qrc b/client/resources.qrc index 6a768391..d69741f9 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -131,6 +131,7 @@ ui/qml/Components/AdLabel.qml ui/qml/Components/ConnectButton.qml ui/qml/Components/ConnectionTypeSelectionDrawer.qml + ui/qml/Components/GamepadLoader.qml ui/qml/Components/HomeContainersListView.qml ui/qml/Components/HomeSplitTunnelingDrawer.qml ui/qml/Components/InstalledAppsDrawer.qml diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index 1dffc9b9..8b212264 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -35,13 +35,12 @@ SecureQSettings::SecureQSettings(const QString &organization, const QString &app } } m_settings.setValue("Conf/encrypted", true); - m_settings.sync(); } } QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const { - QMutexLocker locker(&mutex); + QMutexLocker locker(&m_mutex); if (m_cache.contains(key)) { return m_cache.value(key); @@ -85,7 +84,7 @@ QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue void SecureQSettings::setValue(const QString &key, const QVariant &value) { - QMutexLocker locker(&mutex); + QMutexLocker locker(&m_mutex); if (encryptionRequired() && encryptedKeys.contains(key)) { if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) { @@ -107,26 +106,20 @@ void SecureQSettings::setValue(const QString &key, const QVariant &value) } m_cache.insert(key, value); - sync(); } void SecureQSettings::remove(const QString &key) { - QMutexLocker locker(&mutex); + QMutexLocker locker(&m_mutex); m_settings.remove(key); m_cache.remove(key); - - sync(); -} - -void SecureQSettings::sync() -{ - m_settings.sync(); } QByteArray SecureQSettings::backupAppConfig() const { + QMutexLocker locker(&m_mutex); + QJsonObject cfg; const auto needToBackup = [this](const auto &key) { @@ -161,6 +154,8 @@ QByteArray SecureQSettings::backupAppConfig() const bool SecureQSettings::restoreAppConfig(const QByteArray &json) { + QMutexLocker locker(&m_mutex); + QJsonObject cfg = QJsonDocument::fromJson(json).object(); if (cfg.isEmpty()) return false; @@ -173,10 +168,16 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json) setValue(key, cfg.value(key).toVariant()); } - sync(); return true; } +void SecureQSettings::clearSettings() +{ + QMutexLocker locker(&m_mutex); + m_settings.clear(); + m_cache.clear(); +} + QByteArray SecureQSettings::encryptText(const QByteArray &value) const { QSimpleCrypto::QBlockCipher cipher; @@ -294,11 +295,3 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data) qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString(); } } - -void SecureQSettings::clearSettings() -{ - QMutexLocker locker(&mutex); - m_settings.clear(); - m_cache.clear(); - sync(); -} diff --git a/client/secure_qsettings.h b/client/secure_qsettings.h index 8878e1d5..e8e267d6 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -16,14 +16,16 @@ public: explicit SecureQSettings(const QString &organization, const QString &application = QString(), QObject *parent = nullptr); - Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; - Q_INVOKABLE void setValue(const QString &key, const QVariant &value); + QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; + void setValue(const QString &key, const QVariant &value); void remove(const QString &key); - void sync(); QByteArray backupAppConfig() const; bool restoreAppConfig(const QByteArray &json); + void clearSettings(); + +private: QByteArray encryptText(const QByteArray &value) const; QByteArray decryptText(const QByteArray &ba) const; @@ -35,9 +37,6 @@ public: static QByteArray getSecTag(const QString &tag); static void setSecTag(const QString &tag, const QByteArray &data); - void clearSettings(); - -private: QSettings m_settings; mutable QHash m_cache; @@ -53,7 +52,7 @@ private: const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray - mutable QMutex mutex; + mutable QRecursiveMutex m_mutex; }; #endif // SECUREQSETTINGS_H diff --git a/client/settings.cpp b/client/settings.cpp index 68155ab4..dd8a9a8a 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -21,10 +21,10 @@ Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_N { // Import old settings if (serversCount() == 0) { - QString user = value("Server/userName").toString(); - QString password = value("Server/password").toString(); - QString serverName = value("Server/serverName").toString(); - int port = value("Server/serverPort").toInt(); + QString user = m_settings.value("Server/userName").toString(); + QString password = m_settings.value("Server/password").toString(); + QString serverName = m_settings.value("Server/serverName").toString(); + int port = m_settings.value("Server/serverPort").toInt(); if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) { QJsonObject server; @@ -222,7 +222,7 @@ QString Settings::nextAvailableServerName() const void Settings::setSaveLogs(bool enabled) { - setValue("Conf/saveLogs", enabled); + m_settings.setValue("Conf/saveLogs", enabled); #ifndef Q_OS_ANDROID if (!isSaveLogs()) { Logger::deInit(); @@ -242,12 +242,12 @@ void Settings::setSaveLogs(bool enabled) QDateTime Settings::getLogEnableDate() { - return value("Conf/logEnableDate").toDateTime(); + return m_settings.value("Conf/logEnableDate").toDateTime(); } void Settings::setLogEnableDate(QDateTime date) { - setValue("Conf/logEnableDate", date); + m_settings.setValue("Conf/logEnableDate", date); } QString Settings::routeModeString(RouteMode mode) const @@ -261,17 +261,17 @@ QString Settings::routeModeString(RouteMode mode) const Settings::RouteMode Settings::routeMode() const { - return static_cast(value("Conf/routeMode", 0).toInt()); + return static_cast(m_settings.value("Conf/routeMode", 0).toInt()); } bool Settings::isSitesSplitTunnelingEnabled() const { - return value("Conf/sitesSplitTunnelingEnabled", false).toBool(); + return m_settings.value("Conf/sitesSplitTunnelingEnabled", false).toBool(); } void Settings::setSitesSplitTunnelingEnabled(bool enabled) { - setValue("Conf/sitesSplitTunnelingEnabled", enabled); + m_settings.setValue("Conf/sitesSplitTunnelingEnabled", enabled); } bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip) @@ -359,12 +359,12 @@ void Settings::removeAllVpnSites(RouteMode mode) QString Settings::primaryDns() const { - return value("Conf/primaryDns", cloudFlareNs1).toString(); + return m_settings.value("Conf/primaryDns", cloudFlareNs1).toString(); } QString Settings::secondaryDns() const { - return value("Conf/secondaryDns", cloudFlareNs2).toString(); + return m_settings.value("Conf/secondaryDns", cloudFlareNs2).toString(); } void Settings::clearSettings() @@ -386,18 +386,18 @@ QString Settings::appsRouteModeString(AppsRouteMode mode) const Settings::AppsRouteMode Settings::getAppsRouteMode() const { - return static_cast(value("Conf/appsRouteMode", 0).toInt()); + return static_cast(m_settings.value("Conf/appsRouteMode", 0).toInt()); } void Settings::setAppsRouteMode(AppsRouteMode mode) { - setValue("Conf/appsRouteMode", mode); + m_settings.setValue("Conf/appsRouteMode", mode); } QVector Settings::getVpnApps(AppsRouteMode mode) const { QVector apps; - auto appsArray = value("Conf/" + appsRouteModeString(mode)).toJsonArray(); + auto appsArray = m_settings.value("Conf/" + appsRouteModeString(mode)).toJsonArray(); for (const auto &app : appsArray) { InstalledAppInfo appInfo; appInfo.appName = app.toObject().value("appName").toString(); @@ -419,43 +419,42 @@ void Settings::setVpnApps(AppsRouteMode mode, const QVector &a appInfo.insert("appPath", app.appPath); appsArray.push_back(appInfo); } - setValue("Conf/" + appsRouteModeString(mode), appsArray); - m_settings.sync(); + m_settings.setValue("Conf/" + appsRouteModeString(mode), appsArray); } bool Settings::isAppsSplitTunnelingEnabled() const { - return value("Conf/appsSplitTunnelingEnabled", false).toBool(); + return m_settings.value("Conf/appsSplitTunnelingEnabled", false).toBool(); } void Settings::setAppsSplitTunnelingEnabled(bool enabled) { - setValue("Conf/appsSplitTunnelingEnabled", enabled); + m_settings.setValue("Conf/appsSplitTunnelingEnabled", enabled); } bool Settings::isKillSwitchEnabled() const { - return value("Conf/killSwitchEnabled", true).toBool(); + return m_settings.value("Conf/killSwitchEnabled", true).toBool(); } void Settings::setKillSwitchEnabled(bool enabled) { - setValue("Conf/killSwitchEnabled", enabled); + m_settings.setValue("Conf/killSwitchEnabled", enabled); } bool Settings::isStrictKillSwitchEnabled() const { - return value("Conf/strictKillSwitchEnabled", false).toBool(); + return m_settings.value("Conf/strictKillSwitchEnabled", false).toBool(); } void Settings::setStrictKillSwitchEnabled(bool enabled) { - setValue("Conf/strictKillSwitchEnabled", enabled); + m_settings.setValue("Conf/strictKillSwitchEnabled", enabled); } QString Settings::getInstallationUuid(const bool needCreate) { - auto uuid = value("Conf/installationUuid", "").toString(); + auto uuid = m_settings.value("Conf/installationUuid", "").toString(); if (needCreate && uuid.isEmpty()) { uuid = QUuid::createUuid().toString(); @@ -476,7 +475,7 @@ QString Settings::getInstallationUuid(const bool needCreate) void Settings::setInstallationUuid(const QString &uuid) { - setValue("Conf/installationUuid", uuid); + m_settings.setValue("Conf/installationUuid", uuid); } ServerCredentials Settings::defaultServerCredentials() const @@ -497,28 +496,6 @@ ServerCredentials Settings::serverCredentials(int index) const return credentials; } -QVariant Settings::value(const QString &key, const QVariant &defaultValue) const -{ - QVariant returnValue; - if (QThread::currentThread() == QCoreApplication::instance()->thread()) { - returnValue = m_settings.value(key, defaultValue); - } else { - QMetaObject::invokeMethod(&m_settings, "value", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, returnValue), - Q_ARG(const QString &, key), Q_ARG(const QVariant &, defaultValue)); - } - return returnValue; -} - -void Settings::setValue(const QString &key, const QVariant &value) -{ - if (QThread::currentThread() == QCoreApplication::instance()->thread()) { - m_settings.setValue(key, value); - } else { - QMetaObject::invokeMethod(&m_settings, "setValue", Qt::BlockingQueuedConnection, Q_ARG(const QString &, key), - Q_ARG(const QVariant &, value)); - } -} - void Settings::resetGatewayEndpoint() { m_gatewayEndpoint = gatewayEndpoint; @@ -541,50 +518,50 @@ QString Settings::getGatewayEndpoint(bool isTestPurchase) bool Settings::isDevGatewayEnv(bool isTestPurchase) { - return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool(); + return isTestPurchase ? true : m_settings.value("Conf/devGatewayEnv", false).toBool(); } void Settings::toggleDevGatewayEnv(bool enabled) { - setValue("Conf/devGatewayEnv", enabled); + m_settings.setValue("Conf/devGatewayEnv", enabled); } bool Settings::isHomeAdLabelVisible() { - return value("Conf/homeAdLabelVisible", true).toBool(); + return m_settings.value("Conf/homeAdLabelVisible", true).toBool(); } void Settings::disableHomeAdLabel() { - setValue("Conf/homeAdLabelVisible", false); + m_settings.setValue("Conf/homeAdLabelVisible", false); } bool Settings::isPremV1MigrationReminderActive() { - return value("Conf/premV1MigrationReminderActive", false).toBool(); + return m_settings.value("Conf/premV1MigrationReminderActive", false).toBool(); } void Settings::disablePremV1MigrationReminder() { - setValue("Conf/premV1MigrationReminderActive", false); + m_settings.setValue("Conf/premV1MigrationReminderActive", false); } QStringList Settings::allowedDnsServers() const { - return value("Conf/allowedDnsServers").toStringList(); + return m_settings.value("Conf/allowedDnsServers").toStringList(); } void Settings::setAllowedDnsServers(const QStringList &servers) { - setValue("Conf/allowedDnsServers", servers); + m_settings.setValue("Conf/allowedDnsServers", servers); } QStringList Settings::readNewsIds() const { - return value("News/readIds").toStringList(); + return m_settings.value("News/readIds").toStringList(); } void Settings::setReadNewsIds(const QStringList &ids) { - setValue("News/readIds", ids); + m_settings.setValue("News/readIds", ids); } diff --git a/client/settings.h b/client/settings.h index e8277304..45fe39e1 100644 --- a/client/settings.h +++ b/client/settings.h @@ -29,11 +29,11 @@ public: QJsonArray serversArray() const { - return QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array(); + return QJsonDocument::fromJson(m_settings.value("Servers/serversList").toByteArray()).array(); } void setServersArray(const QJsonArray &servers) { - setValue("Servers/serversList", QJsonDocument(servers).toJson()); + m_settings.setValue("Servers/serversList", QJsonDocument(servers).toJson()); } // Servers section @@ -45,11 +45,11 @@ public: int defaultServerIndex() const { - return value("Servers/defaultServerIndex", 0).toInt(); + return m_settings.value("Servers/defaultServerIndex", 0).toInt(); } void setDefaultServer(int index) { - setValue("Servers/defaultServerIndex", index); + m_settings.setValue("Servers/defaultServerIndex", index); } QJsonObject defaultServer() const { @@ -78,34 +78,34 @@ public: // App settings section bool isAutoConnect() const { - return value("Conf/autoConnect", false).toBool(); + return m_settings.value("Conf/autoConnect", false).toBool(); } void setAutoConnect(bool enabled) { - setValue("Conf/autoConnect", enabled); + m_settings.setValue("Conf/autoConnect", enabled); } bool isStartMinimized() const { - return value("Conf/startMinimized", false).toBool(); + return m_settings.value("Conf/startMinimized", false).toBool(); } void setStartMinimized(bool enabled) { - setValue("Conf/startMinimized", enabled); + m_settings.setValue("Conf/startMinimized", enabled); } bool isNewsNotifications() const { - return value("Conf/newsNotifications", true).toBool(); + return m_settings.value("Conf/newsNotifications", true).toBool(); } void setNewsNotifications(bool enabled) { - setValue("Conf/newsNotifications", enabled); + m_settings.setValue("Conf/newsNotifications", enabled); } bool isSaveLogs() const { - return value("Conf/saveLogs", false).toBool(); + return m_settings.value("Conf/saveLogs", false).toBool(); } void setSaveLogs(bool enabled); @@ -122,19 +122,18 @@ public: QString routeModeString(RouteMode mode) const; RouteMode routeMode() const; - void setRouteMode(RouteMode mode) { setValue("Conf/routeMode", mode); } + void setRouteMode(RouteMode mode) { m_settings.setValue("Conf/routeMode", mode); } bool isSitesSplitTunnelingEnabled() const; void setSitesSplitTunnelingEnabled(bool enabled); QVariantMap vpnSites(RouteMode mode) const { - return value("Conf/" + routeModeString(mode)).toMap(); + return m_settings.value("Conf/" + routeModeString(mode)).toMap(); } void setVpnSites(RouteMode mode, const QVariantMap &sites) { - setValue("Conf/" + routeModeString(mode), sites); - m_settings.sync(); + m_settings.setValue("Conf/" + routeModeString(mode), sites); } bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = ""); void addVpnSites(RouteMode mode, const QMap &sites); // map @@ -147,11 +146,11 @@ public: bool useAmneziaDns() const { - return value("Conf/useAmneziaDns", true).toBool(); + return m_settings.value("Conf/useAmneziaDns", true).toBool(); } void setUseAmneziaDns(bool enabled) { - setValue("Conf/useAmneziaDns", enabled); + m_settings.setValue("Conf/useAmneziaDns", enabled); } QString primaryDns() const; @@ -160,13 +159,13 @@ public: // QString primaryDns() const { return m_primaryDns; } void setPrimaryDns(const QString &primaryDns) { - setValue("Conf/primaryDns", primaryDns); + m_settings.setValue("Conf/primaryDns", primaryDns); } // QString secondaryDns() const { return m_secondaryDns; } void setSecondaryDns(const QString &secondaryDns) { - setValue("Conf/secondaryDns", secondaryDns); + m_settings.setValue("Conf/secondaryDns", secondaryDns); } // static constexpr char openNicNs5[] = "94.103.153.176"; @@ -188,16 +187,16 @@ public: }; void setAppLanguage(QLocale locale) { - setValue("Conf/appLanguage", locale.name()); + m_settings.setValue("Conf/appLanguage", locale.name()); }; bool isScreenshotsEnabled() const { - return value("Conf/screenshotsEnabled", true).toBool(); + return m_settings.value("Conf/screenshotsEnabled", true).toBool(); } void setScreenshotsEnabled(bool enabled) { - setValue("Conf/screenshotsEnabled", enabled); + m_settings.setValue("Conf/screenshotsEnabled", enabled); emit screenshotsEnabledChanged(enabled); } @@ -255,9 +254,6 @@ signals: void settingsCleared(); private: - QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; - void setValue(const QString &key, const QVariant &value); - void setInstallationUuid(const QString &uuid); mutable SecureQSettings m_settings; diff --git a/client/translations/defaultvpn_ru_RU.ts b/client/translations/defaultvpn_ru_RU.ts index b187cad0..eae19b3d 100644 --- a/client/translations/defaultvpn_ru_RU.ts +++ b/client/translations/defaultvpn_ru_RU.ts @@ -1,9 +1,6 @@ - - AdLabel - AllowedDnsController @@ -56,7 +53,7 @@ ApiAccountInfoModel - + Active Активна @@ -71,30 +68,34 @@ %1 из %2 - Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to 200 Mbps - Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с + Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с - Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and more. YouTube is not included in the free plan. - Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включен в бесплатный тариф. + Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включен в бесплатный тариф. ApiConfigsController - + + %1 installed successfully. %1 успешно установлен. - + + Subscription restored successfully. + Подписка успешно восстановлена. + + + API config reloaded Конфигурация API перезагружена - + Successfully changed the country of connection to %1 Страна подключения изменена на %1 @@ -102,103 +103,83 @@ ApiPremV1MigrationDrawer - Switch to the new Amnezia Premium subscription - Перейдите на новый тип подписки Amnezia Premium + Перейдите на новый тип подписки Amnezia Premium - We'll preserve all remaining days of your current subscription and give you an extra month as a thank you. - Мы сохраним все оставшиеся дни текущей подписки и подарим дополнительный месяц в благодарность за переход. + Мы сохраним все оставшиеся дни текущей подписки и подарим дополнительный месяц в благодарность за переход. - This new subscription type will be actively developed with more locations and features added regularly. Currently available: - Именно новый тип подписки будет активно развиваться и пополняться новыми локациями и функциями. Уже доступны: + Именно новый тип подписки будет активно развиваться и пополняться новыми локациями и функциями. Уже доступны: - <li>20 locations (with more coming soon)</li> - <li>20 локаций (их число будет расти)</li> + <li>20 локаций (их число будет расти)</li> - <li>Easier switching between countries in the app</li> - <li>Удобное переключение между странами в приложении</li> + <li>Удобное переключение между странами в приложении</li> - <li>Personal dashboard to manage your subscription</li> - <li>Личный кабинет для управления подпиской</li> + <li>Личный кабинет для управления подпиской</li> - Old keys will be deactivated after switching. - После перехода старые ключи перестанут работать. + После перехода старые ключи перестанут работать. - Email - Email + Email - mail@example.com - mail@example.com + mail@example.com - No old format subscriptions for a given email - Для указанного адреса электронной почты нет подписок старого типа + Для указанного адреса электронной почты нет подписок старого типа - Enter the email you used for your current subscription - Укажите адрес почты, который использовали при заказе текущей подписки + Укажите адрес почты, который использовали при заказе текущей подписки - - Continue - Продолжить + Продолжить - Remind me later - Напомнить позже + Напомнить позже - Don't remind me again - Больше не напоминать + Больше не напоминать - No more reminders? You can always switch to the new format in the server settings - Отключить напоминания? Вы всегда сможете перейти на новый тип подписки в настройках сервера + Отключить напоминания? Вы всегда сможете перейти на новый тип подписки в настройках сервера - Cancel - Отменить + Отменить ApiPremV1SubListDrawer - Choose Subscription - Выбрать подписку + Выбрать подписку - Order ID: - ID заказа: + ID заказа: - Purchase Date: - Дата покупки: + Дата покупки: @@ -224,7 +205,12 @@ Бесплатно - + + %1 $ + %1 $ + + + %1 $/month %1 $/месяц @@ -341,21 +327,25 @@ ContextMenuType + C&ut Вырезать + &Copy Копировать + &Paste Вставить + &SelectAll Выбрать всё @@ -419,17 +409,17 @@ Can't be disabled for current server ImportController - + Scanned %1 of %2. Отсканировано %1 из %2. - + This configuration contains an OpenVPN setup. OpenVPN configurations can include malicious scripts, so only add it if you fully trust the provider of this config. Эта конфигурация содержит настройки OpenVPN. Конфигурации OpenVPN могут содержать вредоносные скрипты, поэтому добавляйте их только в том случае, если полностью доверяете источнику этого файла. - + <br>In the imported configuration, potentially dangerous lines were found: <br>В импортированной конфигурации обнаружены потенциально опасные строки: @@ -461,47 +451,47 @@ Already installed containers were found on the server. All installed containers На сервере обнаружены установленные протоколы и сервисы. Все они были добавлены в приложение - + Settings updated successfully Настройки успешно обновлены - + Server '%1' was rebooted Сервер '%1' был перезагружен - + Server '%1' was removed Сервер '%1' был удален - + All containers from server '%1' have been removed Все протоколы и сервисы были удалены с сервера '%1' - + %1 has been removed from the server '%2' %1 был удален с сервера '%2' - + Api config removed Конфигурация API удалена - + %1 cached profile cleared %1 закэшированный профиль очищен - + Please login as the user Пожалуйста, войдите в систему от имени пользователя - + Server added successfully Сервер успешно добавлен @@ -545,8 +535,8 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + DefaultVPN DefaultVPN @@ -561,7 +551,7 @@ Already installed containers were found on the server. All installed containers VPN выключен - + DefaultVPN notification Уведомдение DefaultVPN @@ -574,19 +564,16 @@ Already installed containers were found on the server. All installed containers OtpCodeDrawer - OTP code was sent to your email - Одноразовый код был отправлен на ваш email + Одноразовый код был отправлен на ваш email - OTP Code - Одноразовый код + Одноразовый код - Continue - Продолжить + Продолжить @@ -653,49 +640,46 @@ Already installed containers were found on the server. All installed containers PageHome - You've successfully switched to the new Amnezia Premium subscription! - Вы успешно перешли на новый тип подписки Amnezia Premium! + Вы успешно перешли на новый тип подписки Amnezia Premium! - Old keys will no longer work. Please use your new subscription key to connect. Thank you for staying with us! - Старые ключи перестанут работать. Пожалуйста, используйте новый ключ для подключения. + Старые ключи перестанут работать. Пожалуйста, используйте новый ключ для подключения. Спасибо, что остаетесь с нами! - Continue - Продолжить + Продолжить - + Logging enabled Логирование включено - + Dev gateway enabled Dev gateway enabled - + Split tunneling enabled Раздельное туннелирование включено - + Split tunneling disabled Раздельное туннелирование выключено - + VPN protocol VPN-протокол - + Servers Серверы @@ -768,32 +752,32 @@ Thank you for staying with us! Порт - + Save Сохранить - + Save settings? Сохранить настройки? - + Only the settings for this device will be changed Будут изменены настройки только для этого устройства - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -816,37 +800,37 @@ Thank you for staying with us! I1 - Special junk 1 - + I2 - Special junk 2 I2 - Special junk 2 - + I3 - Special junk 3 I3 - Special junk 3 - + I4 - Special junk 4 I4 - Special junk 4 - + I5 - Special junk 5 I5 - Special junk 5 - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + S3 + cookie reply size (64) + S4 + transport packet size (32) Значение поля S1 + размер инициализации сообщения (148) не должно равняться S2 + размер ответа сообщения (92) + S3 + размер ответа cookie (64) + S4 + размер транспортного пакета (32) - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Save Сохранить @@ -911,27 +895,27 @@ Thank you for staying with us! H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique Значения в полях H1-H4 должны быть уникальными - + Save settings? Сохранить настройки? - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -960,32 +944,32 @@ Thank you for staying with us! Шифрование - + Save Сохранить - + Save settings? Сохранить настройки? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -1074,114 +1058,114 @@ Thank you for staying with us! SHA1 - - + + Cipher Шифрование - + AES-256-GCM AES-256-GCM - + AES-192-GCM AES-192-GCM - + AES-128-GCM AES-128-GCM - + AES-256-CBC AES-256-CBC - + AES-192-CBC AES-192-CBC - + AES-128-CBC AES-128-CBC - + ChaCha20-Poly1305 ChaCha20-Poly1305 - + ARIA-256-CBC ARIA-256-CBC - + CAMELLIA-256-CBC CAMELLIA-256-CBC - + none none - + TLS auth TLS авторизация - + Block DNS requests outside of VPN Блокировать DNS-запросы за пределами VPN - + Additional client configuration commands Дополнительные команды конфигурации клиента - - + + Commands: Команды: - + Additional server configuration commands Дополнительные команды конфигурации сервера - + Save settings? Сохранить настройки? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения - + Save Сохранить @@ -1252,32 +1236,32 @@ Thank you for staying with us! Шифрование - + Save Сохранить - + Save settings? Сохранить настройки? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -1657,7 +1641,7 @@ Thank you for staying with us! PageSettings - + Settings Настройки @@ -1688,7 +1672,7 @@ Thank you for staying with us! Резервное копирование - + About DefaultVPN Об DefaultVPN @@ -1793,7 +1777,7 @@ Thank you for staying with us! mailto:support@amnezia.org - + mailto:support@amnezia.org @@ -1845,6 +1829,11 @@ Thank you for staying with us! + Unable change server location while trying to make an active connection + Невозможно изменить локацию сервера при попытке установить активное соединение + + + Unable change server location while there is an active connection Невозможно изменить локацию во время активного соединения @@ -1918,72 +1907,72 @@ Thank you for staying with us! Windows - + Windows macOS - + macOS Android - + Android AndroidTV - + Android TV iOS - + iOS Linux - + Linux Routers - + Маршрутизаторы documentation/instructions/connect-amnezia-premium#windows - + documentation/instructions/connect-amnezia-premium#windows documentation/instructions/connect-amnezia-premium#macos - + documentation/instructions/connect-amnezia-premium#macos documentation/instructions/connect-amnezia-premium#android - + documentation/instructions/connect-amnezia-premium#android documentation/instructions/android_tv_connect/ - + documentation/instructions/android_tv_connect/ documentation/instructions/connect-amnezia-premium#ios - + documentation/instructions/connect-amnezia-premium#ios documentation/instructions/connect-amnezia-premium#linux - + documentation/instructions/connect-amnezia-premium#linux documentation/instructions/connect-amnezia-premium#routers - + documentation/instructions/connect-amnezia-premium#routers @@ -2107,14 +2096,14 @@ Thank you for staying with us! Активные соединения - - + + Use VLESS protocol Используйте протокол VLESS - - + + Cannot change protocol during active connection Невозможно изменить протокол во время активного соединения @@ -2166,16 +2155,17 @@ Thank you for staying with us! Продолжить - - - - + + + + Cancel Отменить - - + + + Cannot reload API config during active connection Невозможно перзагрузить API конфигурацию при активном соединении @@ -2210,58 +2200,84 @@ Thank you for staying with us! Удалить из приложения? - - + + + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения - + Subscription expires on Подписка истекает - + Reload API configuration Перезагрузить конфигурацию API - + + Rename server + Переименовать сервер + + + Delete Удалить - + + Server name + Имя сервера + + + + Enter server name + Введите имя сервера + + + + Save + Сохранить + + + + Server renamed successfully + Сервер успешно переименован + + + Reset API configuration? Перезагрузить конфигурацию API? - + This will reload the API configuration from the server Это перезагрузит конфигурацию API с сервера - + Reset Перезагрузить - + Are you sure you want to remove the server from the app? Вы уверены что хотите удалить сервер из приложения? - + You won't be able to connect to it Вы не сможете к нему подключиться - + Yes, delete anyway Да, удалить в любом случае - + No, keep it Нет, оставить @@ -2270,12 +2286,50 @@ Thank you for staying with us! Закрыть + + PageSettingsApiSubscriptionKey + + + Copy key + Копировать ключ + + + + Copied + Скопировано + + + + Save key as a file + Сохранить ключ в файл + + + + Save DefaultVPN config + Сохранить конфиг DefaultVPN + + + + Config files (*.vpn) + Файлы конфигурации (*.vpn) + + + + Show key text + Показать текст ключа + + + + To read the QR code in the Amnezia app, tap + in the main menu → 'QR code' + Чтобы прочитать QR-код в приложении Amnezia, нажмите + в главном меню → «QR-код» + + PageSettingsApiSupport Telegram - + Telegram @@ -2305,7 +2359,7 @@ Thank you for staying with us! Support tag - + Идентификатор поддержки @@ -2361,17 +2415,17 @@ Thank you for staying with us! Отменить - + application name название приложения - + Open executable file Открыть исполняемый файл - + Executable files (*.*) Исполняемые файлы (*.*) @@ -2414,17 +2468,17 @@ Thank you for staying with us! Запускать в свернутом виде - + Language Язык - + All settings will be reset to default. All installed DefaultVPN services will still remain on the server. Все настройки будут сброшены до значений по умолчанию. Все установленные сервисы и протоколы DefaultVPN останутся на сервере. - + Enable notifications Включить уведомления @@ -2439,42 +2493,52 @@ Thank you for staying with us! Запускает приложение свёрнутым (работает с включенной функцией автозапуска) - + + News Notification + Уведомления о новостях + + + + Show a notification icon for unread news + Показывать значок уведомления, если есть непрочитанные новости + + + Logging Логирование - + Enabled Включено - + Disabled Отключено - + Reset settings and remove all data from the application Сбросить настройки и удалить все данные из приложения - + Reset settings and remove all data from the application? Сбросить настройки и удалить все данные из приложения? - + Continue Продолжить - + Cancel Отменить - + Cannot reset settings during active connection Невозможно сбросить настройки во время активного соединения @@ -2497,7 +2561,7 @@ Thank you for staying with us! Вы можете сохранить настройки в файл резервной копии, чтобы восстановить их при следующей установке приложения. - + The backup will contain your passwords and private keys for all servers added to DefaultVPN. Keep this information in a secure place. Резервная копия будет содержать ваши пароли и закрытые ключи для всех серверов, добавленных в DefaultVPN. Храните эту информацию в надежном месте. @@ -2697,7 +2761,7 @@ Thank you for staying with us! Soft killSwitch - + Strict KillSwitch Strict KillSwitch @@ -2717,12 +2781,12 @@ Thank you for staying with us! Доступ в интернет будет заблокирован при внезапном отключении VPN - + Internet connection is blocked even when VPN is turned off manually or hasn't started Интернет-соединение блокируется, даже если VPN отключен вручную или не запущен - + If the VPN disconnects or drops while Strict KillSwitch is enabled, internet access will be blocked. To restore access, reconnect VPN or disable/change the KillSwitch. Если VPN-соединение прервётся или прервётся при включённом режиме Strict KillSwitch, доступ в интернет будет заблокирован. Чтобы восстановить доступ, переподключите VPN или отключите/измените режим KillSwitch. @@ -2839,7 +2903,7 @@ Thank you for staying with us! PageSettingsLogging - + Logging Логирование @@ -2850,22 +2914,22 @@ Thank you for staying with us! Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения. - - + + Save Сохранить - - + + Logs files (*.log) Файлы логов (*.log) - - + + Logs file saved Файл с логами сохранен @@ -2876,13 +2940,13 @@ Thank you for staying with us! Включить запись логов - + Clear logs? Очистить логи? - + Continue Продолжить @@ -2893,7 +2957,7 @@ Thank you for staying with us! Отменить - + Logs have been cleaned up Логи очищены @@ -2904,7 +2968,7 @@ Thank you for staying with us! Логи приложения - + DefaultVPN-service logs Логи DefaultVPN-service @@ -2919,17 +2983,17 @@ Thank you for staying with us! Сохранить логи - + DefaultVPN logs Логи DefaultVPN - + Service logs Логи службы - + Clear logs Очистить логи @@ -2950,6 +3014,14 @@ Thank you for staying with us! Нет, оставить + + PageSettingsNewsNotifications + + + News & Notifications + Новости и уведомления + + PageSettingsServerData @@ -2963,110 +3035,121 @@ Thank you for staying with us! Новые установленные протоколы и сервисы не обнаружены - - - - + + + + Continue Продолжить - - - - + + + + Cancel Отменить - + Check the server for previously installed Amnezia services Проверить сервер на наличие ранее установленных сервисов Amnezia - + Add them to the application if they were not displayed Добавить их в приложение, если они не отображаются - + Reboot server Перезагрузить сервер - + Do you want to reboot the server? Вы уверены, что хотите перезагрузить сервер? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? Процесс перезагрузки может занять около 30 секунд. Вы уверены, что хотите продолжить? - + Cannot reboot server during active connection Невозможно перезагрузить сервер во время активного соединения - + Do you want to remove the server from application? Вы уверены, что хотите удалить сервер из приложения? - + All installed DefaultVPN services will still remain on the server. Все установленные сервисы и протоколы DefaultVPN останутся на сервере. - + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения - - Do you want to clear server from Amnezia software? - Вы хотите очистить сервер от всех сервисов Amnezia? + + Clear server from DefaultVPN software + Удалить сервер из DefaultVPN - + + Do you want to clear server from DefaultVPN software? + Вы уверены, что хотите удалить сервер из DefaultVPN? + + + + Cannot clear server from DefaultVPN software during active connection + Невозможно удалить сервер из DefaultVPN во время активного соединения + + + Do you want to clear server from Amnezia software? + Вы хотите очистить сервер от всех сервисов Amnezia? + + + All users whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - Cannot clear server from Amnezia software during active connection - Невозможно очистить сервер от сервисов Amnezia во время активного соединения + Невозможно очистить сервер от сервисов Amnezia во время активного соединения - + Reset API config Сбросить конфигурацию API - + Do you want to reset API config? Вы хотите сбросить конфигурацию API? - + Cannot reset API config during active connection Невозможно сбросить конфигурацию API во время активного соединения - Switch to the new Amnezia Premium subscription - Перейти на новый тип подписки Amnezia Premium + Перейти на новый тип подписки Amnezia Premium - + Remove server from application Удалить сервер из приложения - Clear server from Amnezia software - Очистить сервер от протоколов и сервисов Amnezia + Очистить сервер от протоколов и сервисов Amnezia @@ -3087,65 +3170,104 @@ Thank you for staying with us! Управление - + Server settings Настройки сервера - + Name Название - Reset API Configuration - Перезагрузить конфигурацию API + Перезагрузить конфигурацию API - + Delete server Удалить сервер - + Do you want to reset API config? Вы хотите сбросить конфигурацию API? - + Continue Продолжить - + Cancel Отменить - + Cannot reset API config during active connection Невозможно сбросить конфигурацию API во время активного соединения - + + Rename server + Переименовать сервер + + + + Reset API configuration + Сбросить конфигурацию API + + + + Server name + Имя сервера + + + + Enter server name + Введите имя сервера + + + + Save + Сохранить + + + + Server renamed successfully + Сервер успешно переименован + + + + Cannot reload API config during active connection + Невозможно перезагрузить конфигурацию API при активном соединении + + + Are you sure you want to remove the server from the app? Вы уверены что хотите удалить сервер из приложения? - + You won't be able to connect to it Вы не сможете к нему подключиться - + Yes, delete anyway Да, удалить в любом случае - + No, keep it Нет, оставить + + + Cannot remove server during active connection + Невозможно удалить сервер во время активного соединения + PageSettingsServerProtocol @@ -3234,6 +3356,11 @@ Thank you for staying with us! Connect to Подключиться к + + + Unable change server location while there is an active connection + Невозможно изменить локацию во время активного соединения + PageSettingsSplitTunneling @@ -3264,13 +3391,13 @@ Thank you for staying with us! - + Continue Продолжить - + Cancel Отменить @@ -3285,70 +3412,70 @@ Thank you for staying with us! Невозможно изменить настройки раздельного туннелирования во время активного соединения - + website or IP веб-сайт или IP - + Additional options Дополнительные настройки - + Import Импорт - + Save site list Сохранить список сайтов - + Save sites Сохранить сайты - - - + + + Sites files (*.json) Файлы сайтов (*.json) - + Clear site list Очистить список сайтов - + Clear site list? Очистить список сайтов? - + All sites will be removed from list. Все сайты будут удалены из списка. - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим @@ -3356,32 +3483,47 @@ Thank you for staying with us! PageSetupWizardApiServiceInfo - + + 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. + Списание с Apple ID при подтверждении. Продление автоматическое, если автопродление не отключено минимум за 24 часа до окончания периода. Управление в настройках Apple ID. + + + + Subscribe Now + Подписаться сейчас + + + + 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> + Продолжая, вы соглашаетесь с <a href="%1" style="color: #FBB26A;">Условиями использования</a> и <a href="%2" style="color: #FBB26A;">Политикой конфиденциальности</a> + + + For the region Для региона - + Price Цена - + Work period Период работы - + Speed Скорость - + Features Особенности - + Connect Подключиться @@ -3462,7 +3604,7 @@ Thank you for staying with us! Вставьте ключ - + Insert Вставить @@ -3532,7 +3674,7 @@ Thank you for staying with us! Файлы резервных копий (*.backup) - + Open config file Открыть файл с конфигурацией @@ -3655,22 +3797,22 @@ Thank you for staying with us! Выберите тип установки - + Manual Ручная - + Choose a VPN protocol Выбрать VPN-протокол - + Skip setup Пропустить настройку - + Continue Продолжить @@ -3871,47 +4013,52 @@ Thank you for staying with us! Конфигурация отозвана - + Connection to Подключение к - + File with connection settings to Файл с настройками подключения к - + Save OpenVPN config Сохранить конфигурацию OpenVPN - + + Save DefaultVPN config + Сохранить конфиг DefaultVPN + + + Save WireGuard config Сохранить конфигурацию WireGuard - + Save AmneziaWG config Сохранить конфигурацию AmneziaWG - + Save Shadowsocks config Сохранить конфигурацию Shadowsocks - + Save Cloak config Сохранить конфигурацию Cloak - + Save XRay config Сохранить конфигурацию XRay - + For the DefaultVPN app Для приложения DefaultVPN @@ -3952,7 +4099,7 @@ Thank you for staying with us! - + Users Пользователи @@ -3962,72 +4109,72 @@ Thank you for staying with us! Имя пользователя - + Search Поиск - + Creation date: %1 Дата создания: %1 - + Latest handshake: %1 Последнее рукопожатие: %1 - + Data received: %1 Получено данных: %1 - + Data sent: %1 Отправлено данных: %1 - + Allowed IPs: %1 Разрешенные подсети: %1 - + Rename Переименовать - + Client name Имя клиента - + Save Сохранить - + Revoke Отозвать - + Revoke the config for a user - %1? Отозвать конфигурацию для пользователя - %1? - + The user will no longer be able to connect to your server. Пользователь больше не сможет подключаться к вашему серверу. - + Continue Продолжить - + Cancel Отменить @@ -4050,7 +4197,7 @@ Thank you for staying with us! - + Share Поделиться @@ -4068,9 +4215,13 @@ Thank you for staying with us! Скопировать - Save AmneziaVPN config - Сохранить конфигурацию AmneziaVPN + Сохранить конфигурацию AmneziaVPN + + + + Save DefaultVPN config + Сохранить конфиг DefaultVPN @@ -4494,75 +4645,80 @@ Thank you for staying with us! Docker error: был достигнут предел скорости вытягивания - + + Server error: Linux kernel is too old + Ошибка сервера: ядро Linux слишком старое + + + SSH request was denied SSH-запрос был отклонён - + SSH request was interrupted SSH-запрос был прерван - + SSH internal error Внутренняя ошибка SSH - + Invalid private key or invalid passphrase entered Введен неверный закрытый ключ или неверная парольная фраза - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types Выбранный формат закрытого ключа не поддерживается, используйте типы ключей openssh ED25519 или PEM - + Timeout connecting to server Тайм-аут подключения к серверу - + SCP error: Generic failure Ошибка SCP: общий сбой - + The config does not contain any containers and credentials for connecting to the server Конфигурация не содержит каких-либо контейнеров и учетных данных для подключения к серверу - + VPN Protocols is not installed. Please install VPN container at first VPN-протоколы не установлены. Пожалуйста, установите протокол - - + + Error when retrieving configuration from API Ошибка при получении конфигурации из API - + This config has already been added to the application Данная конфигурация уже была добавлена в приложение - + A migration error has occurred. Please contact our technical support Произошла ошибка миграции. Обратитесь в нашу службу техподдержки. - + Please update the application to use this feature Обновите приложение чтобы использовать эту функцию - + Your Amnezia Premium subscription has expired. Please check your email for renewal instructions. If you haven't received an email, please contact our support. @@ -4571,142 +4727,142 @@ Thank you for staying with us! Если вы не получили письмо, пожалуйста, свяжитесь с нашей службой поддержки. - + Unable to process purchase Не удалось обработать покупку - + ErrorCode: %1. Код ошибки: %1. - + OpenVPN config missing Отсутствует конфигурация OpenVPN - + OpenVPN management server error Серверная ошибка управлением OpenVPN - + OpenVPN executable missing Отсутствует исполняемый файл OpenVPN - + Shadowsocks (ss-local) executable missing Отсутствует исполняемый файл Shadowsocks (ss-local) - + Cloak (ck-client) executable missing Отсутствует исполняемый файл Cloak (ck-client) - + Amnezia helper service error Ошибка вспомогательной службы Amnezia - + OpenSSL failed Ошибка OpenSSL - + Can't connect: another VPN connection is active Невозможно подключиться: активно другое VPN-соединение - + Can't setup OpenVPN TAP network adapter Невозможно настроить сетевой адаптер OpenVPN TAP - + VPN pool error: no available addresses Ошибка пула VPN: нет доступных адресов - + Unable to open config file Не удалось открыть файл конфигурации - + VPN connection error Ошибка VPN-соединения - + In the response from the server, an empty config was received В ответе от сервера была получена пустая конфигурация - + SSL error occurred Произошла ошибка SSL - + Server response timeout on api request Тайм-аут ответа сервера на запрос API - + Missing AGW public key Отсутствует публичный ключ AGW - + Failed to decrypt response payload - + Не удалось расшифровать данные ответа - + Missing list of available services Отсутствует список доступных сервисов - + The limit of allowed configurations per subscription has been exceeded Превышен лимит разрешенных конфигураций для одной подписки - + QFile error: The file could not be opened Ошибка QFile: не удалось открыть файл - + QFile error: An error occurred when reading from the file Ошибка QFile: произошла ошибка при чтении из файла - + QFile error: The file could not be accessed Ошибка QFile: не удалось получить доступ к файлу - + QFile error: An unspecified error occurred Ошибка QFile: произошла неизвестная ошибка - + QFile error: A fatal error occurred Ошибка QFile: произошла фатальная ошибка - + QFile error: The operation was aborted Ошибка QFile: операция была прервана - + Internal error Внутренняя ошибка @@ -4716,17 +4872,17 @@ Thank you for staying with us! IPsec - + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. IKEv2/IPsec — современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. Он имеет встроенную поддержку в последних версиях Android и iOS. - + Create a file vault on your server to securely store and transfer files. Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. - + REALITY is an innovative protocol developed by the creators of XRay, designed specifically to combat high levels of internet censorship. REALITY identifies censorship systems during the TLS handshake, redirecting suspicious traffic seamlessly to legitimate websites like google.com while providing genuine TLS certificates. This allows VPN traffic to blend indistinguishably with regular web traffic without special configuration. Unlike older protocols such as VMess, VLESS, and XTLS-Vision, REALITY incorporates an advanced built-in "friend-or-foe" detection mechanism, effectively protecting against DPI and other traffic analysis methods. @@ -4751,7 +4907,7 @@ REALITY распознаёт системы блокировки во время - + DNS Service Сервис DNS @@ -4762,7 +4918,7 @@ REALITY распознаёт системы блокировки во время - + Website in Tor network Веб-сайт в сети Tor @@ -4792,17 +4948,18 @@ REALITY распознаёт системы блокировки во время WireGuard — популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. - + + AmneziaWG is a special protocol from Amnezia based on WireGuard. It provides high connection speed and ensures stable operation even in the most challenging network conditions. AmneziaWG — специальный протокол от Amnezia, основанный на WireGuard. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях. - + XRay with REALITY masks VPN traffic as web traffic and protects against active probing. It is highly resistant to detection and offers high speed. XRay с REALITY маскирует VPN-трафик под веб-трафик. Обладает высокой устойчивостью к обнаружению и обеспечивает высокую скорость соединения. - + @@ -4832,17 +4989,17 @@ REALITY распознаёт системы блокировки во время * Работает по протоколу UDP - + Deploy a WordPress site on the Tor network in two clicks. Разверните сайт на WordPress в сети Tor в два клика. - + Replace the current DNS server with your own. This will increase your privacy level. Замените текущий DNS-сервер на свой собственный. Это повысит уровень вашей конфиденциальности. - + OpenVPN is one of the most popular and reliable VPN protocols. It uses SSL/TLS encryption, supports a wide variety of devices and operating systems, and is continuously improved by the community due to its open-source nature. It provides a good balance between speed and security but is easily recognized by DPI systems, making it susceptible to blocking. Features: @@ -4859,7 +5016,7 @@ Features: * Работает по TCP и UDP - + Shadowsocks is based on the SOCKS5 protocol and encrypts connections using AEAD cipher. Although designed to be discreet, it doesn't mimic a standard HTTPS connection and can be detected by some DPI systems. Due to limited support in Amnezia, we recommend using the AmneziaWG protocol. Features: @@ -4877,7 +5034,7 @@ Features: * Работает по протоколу TCP - + This combination includes the OpenVPN protocol and the Cloak plugin, specifically designed to protect against blocking. OpenVPN securely encrypts all internet traffic between your device and the server. @@ -4908,7 +5065,7 @@ OpenVPN надёжно шифрует весь интернет-трафик м * Использует протокол TCP на порту 443 - + WireGuard is a modern, streamlined VPN protocol offering stable connectivity and excellent performance across all devices. It uses fixed encryption settings, delivering lower latency and higher data transfer speeds compared to OpenVPN. However, WireGuard is easily identifiable by DPI systems due to its distinctive packet signatures, making it susceptible to blocking. Features: @@ -4929,7 +5086,6 @@ Features: * Работает по протоколу UDP - AmneziaWG is a modern VPN protocol based on WireGuard, combining simplified architecture with high performance across all devices. It addresses WireGuard's main vulnerability (easy detection by DPI systems) through advanced obfuscation techniques, making VPN traffic indistinguishable from regular internet traffic. AmneziaWG is an excellent choice for those seeking a fast, stealthy VPN connection. @@ -4940,7 +5096,7 @@ Features: * Minimal settings required * Undetectable by traffic analysis systems (DPI) * Operates over UDP protocol - AmneziaWG — современный VPN-протокол на основе WireGuard, сочетающий простую архитектуру и высокую производительность на всех устройствах. Он устраняет основной недостаток WireGuard (лёгкое обнаружение трафика системами DPI) за счёт эффективного маскирования VPN-трафика под обычный интернет-трафик. + AmneziaWG — современный VPN-протокол на основе WireGuard, сочетающий простую архитектуру и высокую производительность на всех устройствах. Он устраняет основной недостаток WireGuard (лёгкое обнаружение трафика системами DPI) за счёт эффективного маскирования VPN-трафика под обычный интернет-трафик. Таким образом, AmneziaWG идеально подойдёт тем, кто ищет быстрое и незаметное VPN-соединение. @@ -4952,31 +5108,7 @@ Features: * Работает по протоколу UDP - - REALITY is an innovative protocol developed by the creators of XRay, designed specifically to combat high levels of internet censorship. REALITY identifies censorship systems during the TLS handshake, redirecting suspicious traffic seamlessly to legitimate websites like google.com while providing genuine TLS certificates. This allows VPN traffic to blend indistinguishably with regular web traffic without special configuration. -Unlike older protocols such as VMess, VLESS, and XTLS-Vision, REALITY incorporates an advanced built-in "friend-or-foe" detection mechanism, effectively protecting against DPI and other traffic analysis methods. - -Features: -* Resistant to active probing and DPI detection -* No special configuration required to disguise traffic -* Highly effective in heavily censored regions -* Minimal battery consumption on devices -* Operates over TCP protocol - REALITY — это инновационный протокол от разработчиков XRay, специально созданный для эффективного противодействия жесткой интернет-цензуре. - -REALITY распознаёт системы блокировки во время TLS-рукопожатия и незаметно перенаправляет подозрительные запросы на реальные сайты, такие как google.com, предъявляя подлинные TLS-сертификаты. Это позволяет маскировать VPN-трафик под обычный веб-трафик без дополнительных настроек. - -В отличие от протоколов старого поколения (VMess, VLESS и XTLS-Vision), REALITY использует встроенную технологию распознавания «свой-чужой», надёжно защищая от DPI и других методов сетевого анализа. - -Особенности: -* Устойчив к активному зондированию и DPI-системам -* Не требует специальной настройки для маскировки трафика -* Эффективен в регионах с жесткой цензурой -* Минимальное энергопотребление на устройствах -* Работает по протоколу TCP - - - + AmneziaWG is a modern VPN protocol based on WireGuard, combining simplified architecture with high performance across all devices. It addresses WireGuard's main vulnerability (easy detection by DPI systems) through advanced obfuscation techniques, making VPN traffic indistinguishable from regular internet traffic. AmneziaWG is an excellent choice for those seeking a fast, stealthy VPN connection. @@ -5000,7 +5132,7 @@ Features: - + IKEv2, combined with IPSec encryption, is a modern and reliable VPN protocol. It reconnects quickly when switching networks or devices, making it ideal for dynamic network environments. While it provides good security and speed, it's easily recognized by DPI systems and susceptible to blocking. Features: @@ -5019,7 +5151,7 @@ Features: * Работает по UDP (порты 500 и 4500) - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -5106,7 +5238,7 @@ For more detailed information, you can - + SOCKS5 proxy server Прокси-сервер SOCKS5 @@ -5253,7 +5385,6 @@ For more detailed information, you can - AmneziaWG Legacy is a outdated version of AmneziaWG protocol. To upgrade, install AmneziaWG and recreate users. AmneziaWG Legacy является устаревшей версией протокола AmneziaWG. Для обновления установите AmneziaWG и пересоздайте пользователей. @@ -5304,12 +5435,17 @@ For more detailed information, you can SettingsController - + + Can't open file: %1 + Невозможно открыть файл: %1 + + + All settings have been reset to default values Все настройки сброшены до значений по умолчанию - + Backup file is corrupted Файл резервной копии поврежден @@ -5317,87 +5453,78 @@ For more detailed information, you can ShareConnectionDrawer - Share - Поделиться + Поделиться - Copy - Скопировать + Скопировать - - Save DefaultVPN config - Сохранить конфиг DefaultVPN + Сохранить конфиг DefaultVPN - - Copied - Скопировано + Скопировано - Copy config string - Скопировать строку конфигурации + Скопировать строку конфигурации - Show connection settings - Показать настройки подключения + Показать настройки подключения - To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" - Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код" + Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код" SitesController - + Hostname not look like ip adress or domain name Имя хоста не похоже на IP-адрес или доменное имя - + New site added: %1 Добавлен новый сайт: %1 - + Site removed: %1 Сайт удален: %1 - + Site list cleared! Список сайтов очищен! - + Can't open file: %1 Невозможно открыть файл: %1 - + Failed to parse JSON data from file: %1 Не удалось разобрать JSON-данные из файла: %1 - + The JSON data is not an array in file: %1 JSON-данные не являются массивом в файле: %1 - + Import completed Импорт завершен - + Export completed Экспорт завершен @@ -5438,15 +5565,28 @@ For more detailed information, you can TextFieldWithHeaderType - + The field can't be empty Поле не может быть пустым + + TextInputPopup + + + Title + Заголовок + + + + Save + Сохранить + + VpnConnection - + Mbps Мбит/с @@ -5497,12 +5637,12 @@ For more detailed information, you can amnezia::ContainerProps - + Automatic Автоматическая - + AmneziaWG protocol will be installed. It provides high connection speed and ensures stable operation even in the most challenging network conditions. Будет установлен протокол AmneziaWG. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях. @@ -5510,12 +5650,12 @@ For more detailed information, you can main2 - + Private key passphrase Парольная фраза для закрытого ключа - + Save Сохранить diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 301e4491..96820202 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -387,6 +387,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 &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); diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 993ff7c7..547ec592 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -7,7 +7,6 @@ #include #include #include - #include "core/controllers/vpnConfigurationController.h" #include "core/qrCodeUtils.h" #include "core/serialization/serialization.h" @@ -170,8 +169,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName) m_config.append(line + "\n"); } - auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8()); - m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8()); emit exportConfigChanged(); } @@ -191,8 +189,7 @@ void ExportController::generateAwgConfig(const QString &clientName) m_config.append(line + "\n"); } - auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8()); - m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8()); emit exportConfigChanged(); } @@ -326,7 +323,7 @@ void ExportController::generateXrayConfig(const QString &clientName) vlessServer.spiderX = realitySettings.value("spiderX").toString(""); } - m_nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN"); + m_nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "DefaultVPN"); emit exportConfigChanged(); } diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 2a1332e2..91d7ec3b 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -291,6 +291,8 @@ void ImportController::processNativeWireGuardConfig() clientProtocolConfig[config_key::cookieReplyPacketJunkSize] = "0"; clientProtocolConfig[config_key::transportPacketJunkSize] = "0"; + clientProtocolConfig[config_key::specialJunk1] = protocols::awg::defaultSpecialJunk1; + clientProtocolConfig[config_key::isObfuscationEnabled] = true; serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson()); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 24c7c220..e15f5db2 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -83,8 +83,8 @@ void InstallController::install(DockerContainer container, int port, TransportPr int s1 = QRandomGenerator::global()->bounded(15, 150); int s2 = QRandomGenerator::global()->bounded(15, 150); - int s3 = QRandomGenerator::global()->bounded(0, 64); - int s4 = QRandomGenerator::global()->bounded(0, 20); + int s3 = QRandomGenerator::global()->bounded(1, 64); + int s4 = QRandomGenerator::global()->bounded(1, 20); // Ensure all values are unique and don't create equal packet sizes QSet usedValues; @@ -97,12 +97,12 @@ void InstallController::install(DockerContainer container, int port, TransportPr while (usedValues.contains(s3) || s1 + AwgConstant::messageInitiationSize == s3 + AwgConstant::messageCookieReplySize || s2 + AwgConstant::messageResponseSize == s3 + AwgConstant::messageCookieReplySize) { - s3 = QRandomGenerator::global()->bounded(0, 64); + s3 = QRandomGenerator::global()->bounded(1, 64); } usedValues.insert(s3); while (usedValues.contains(s4)) { - s4 = QRandomGenerator::global()->bounded(0, 20); + s4 = QRandomGenerator::global()->bounded(1, 20); } QString initPacketJunkSize = QString::number(s1); @@ -1070,7 +1070,7 @@ bool InstallController::isUpdateDockerContainerRequired(const DockerContainer co const QJsonObject &oldProtoConfig = oldConfig.value(ProtocolProps::protoToString(mainProto)).toObject(); const QJsonObject &newProtoConfig = newConfig.value(ProtocolProps::protoToString(mainProto)).toObject(); - if (container == DockerContainer::Awg2) { + if (ContainerProps::isAwgContainer(container)) { const AwgConfig oldConfig(oldProtoConfig); const AwgConfig newConfig(newProtoConfig); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 780efc0e..8754268b 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -178,12 +178,11 @@ void SettingsController::backupAppConfig(const QString &fileName) void SettingsController::restoreAppConfig(const QString &fileName) { - QFile file(fileName); - - file.open(QIODevice::ReadOnly); - - QByteArray data = file.readAll(); - + QByteArray data; + if (!SystemController::readFile(fileName, data)) { + emit changeSettingsErrorOccurred(tr("Can't open file: %1").arg(fileName)); + return; + } restoreAppConfigFromData(data); } diff --git a/client/ui/controllers/sitesController.cpp b/client/ui/controllers/sitesController.cpp index 08c74a93..985ed567 100644 --- a/client/ui/controllers/sitesController.cpp +++ b/client/ui/controllers/sitesController.cpp @@ -7,10 +7,8 @@ #include "systemController.h" #include "core/networkUtilities.h" -SitesController::SitesController(const std::shared_ptr &settings, - const QSharedPointer &vpnConnection, - const QSharedPointer &sitesModel, QObject *parent) - : QObject(parent), m_settings(settings), m_vpnConnection(vpnConnection), m_sitesModel(sitesModel) +SitesController::SitesController(const std::shared_ptr &settings, const QSharedPointer &sitesModel, QObject *parent) + : QObject(parent), m_settings(settings), m_sitesModel(sitesModel) { } @@ -34,32 +32,20 @@ void SitesController::addSite(QString hostname) hostname = hostname.split("/", Qt::SkipEmptyParts).first(); } - const auto &processSite = [this](const QString &hostname, const QString &ip) { - m_sitesModel->addSite(hostname, ip); - - if (!ip.isEmpty()) { - QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, - Q_ARG(QStringList, QStringList() << ip)); - } else if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) { - QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, - Q_ARG(QStringList, QStringList() << hostname)); - } - }; - - const auto &resolveCallback = [this, processSite](const QHostInfo &hostInfo) { + const auto &resolveCallback = [this](const QHostInfo &hostInfo) { const QList &addresses = hostInfo.addresses(); for (const QHostAddress &addr : hostInfo.addresses()) { if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) { - processSite(hostInfo.hostName(), addr.toString()); + m_sitesModel->addSite(hostInfo.hostName(), addr.toString()); break; } } }; if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) { - processSite(hostname, ""); + m_sitesModel->addSite(hostname, ""); } else { - processSite(hostname, ""); + m_sitesModel->addSite(hostname, ""); QHostInfo::lookupHost(hostname, this, resolveCallback); } @@ -72,9 +58,6 @@ void SitesController::removeSite(int index) auto hostname = m_sitesModel->data(modelIndex, SitesModel::Roles::UrlRole).toString(); m_sitesModel->removeSite(modelIndex); - QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteRoutes", Qt::QueuedConnection, - Q_ARG(QStringList, QStringList() << hostname)); - emit finished(tr("Site removed: %1").arg(hostname)); } @@ -128,8 +111,6 @@ void SitesController::importSites(const QString &fileName, bool replaceExisting) m_sitesModel->addSites(sites, replaceExisting); - QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, ips)); - emit finished(tr("Import completed")); } diff --git a/client/ui/controllers/sitesController.h b/client/ui/controllers/sitesController.h index 8cfe3b39..fbbe383c 100644 --- a/client/ui/controllers/sitesController.h +++ b/client/ui/controllers/sitesController.h @@ -11,9 +11,8 @@ class SitesController : public QObject { Q_OBJECT public: - explicit SitesController(const std::shared_ptr &settings, - const QSharedPointer &vpnConnection, - const QSharedPointer &sitesModel, QObject *parent = nullptr); + explicit SitesController(const std::shared_ptr &settings, const QSharedPointer &sitesModel, + QObject *parent = nullptr); public slots: void addSite(QString hostname); @@ -31,8 +30,6 @@ signals: private: std::shared_ptr m_settings; - - QSharedPointer m_vpnConnection; QSharedPointer m_sitesModel; }; diff --git a/client/ui/models/api/apiServicesModel.cpp b/client/ui/models/api/apiServicesModel.cpp index d571346b..5ed9cca1 100644 --- a/client/ui/models/api/apiServicesModel.cpp +++ b/client/ui/models/api/apiServicesModel.cpp @@ -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"); diff --git a/client/ui/models/protocols/awgConfigModel.cpp b/client/ui/models/protocols/awgConfigModel.cpp index ff535490..40de5bdd 100644 --- a/client/ui/models/protocols/awgConfigModel.cpp +++ b/client/ui/models/protocols/awgConfigModel.cpp @@ -141,10 +141,12 @@ void AwgConfigModel::updateModel(const QJsonObject &config) serverProtocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); m_serverProtocolConfig[config_key::responsePacketJunkSize] = serverProtocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); - m_serverProtocolConfig[config_key::cookieReplyPacketJunkSize] = - serverProtocolConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize); - m_serverProtocolConfig[config_key::transportPacketJunkSize] = - serverProtocolConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize); + if (protocolVersion == protocols::awg::awgV2) { + m_serverProtocolConfig[config_key::cookieReplyPacketJunkSize] = + serverProtocolConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize); + m_serverProtocolConfig[config_key::transportPacketJunkSize] = + serverProtocolConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize); + } m_serverProtocolConfig[config_key::initPacketMagicHeader] = serverProtocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); m_serverProtocolConfig[config_key::responsePacketMagicHeader] = diff --git a/client/ui/qml/Components/GamepadLoader.qml b/client/ui/qml/Components/GamepadLoader.qml new file mode 100644 index 00000000..069e1549 --- /dev/null +++ b/client/ui/qml/Components/GamepadLoader.qml @@ -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 + } + } + } +} diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 914fc267..769c3e04 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -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 } } diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index c88f9e7c..bfa31c63 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -49,10 +49,29 @@ 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 diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index b7de64c6..89758430 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -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") { diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml index c44b374c..bd0e5a45 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -330,6 +330,8 @@ PageType { AwgTextField { id: cookieReplyPacketJunkSizeTextField + visible: isAwg2 + Layout.leftMargin: 16 Layout.rightMargin: 16 @@ -342,6 +344,8 @@ PageType { AwgTextField { id: transportPacketJunkSizeTextField + visible: isAwg2 + Layout.leftMargin: 16 Layout.rightMargin: 16 diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 050aeeeb..532ab6a1 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -396,9 +396,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - if (ApiConfigsController.deactivateDevice(true)) { - InstallController.removeProcessedServer() - } + InstallController.removeProcessedServer() PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index 4445b08b..f3c63eb5 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -224,7 +224,6 @@ PageType { height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin color: AmneziaStyle.color.midnightBlack - opacity: 0.8 RowLayout { id: addAppButton diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index b1828131..45fcaa64 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -178,7 +178,7 @@ PageType { Layout.margins: 16 text: qsTr("News Notification") - descriptionText: qsTr("Show notification icon when has unread news") + descriptionText: qsTr("Show a notification icon for unread news") checked: SettingsController.isNewsNotificationsEnabled() onToggled: function() { diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 27aa5dea..ed092336 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -240,7 +240,6 @@ PageType { height: addSiteButton.implicitHeight + 48 color: AmneziaStyle.color.midnightBlack - opacity: 0.8 RowLayout { id: addSiteButton diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml index 16ac2f20..c5e581af 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -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 Terms of Use and Privacy Policy").arg(termsUrl).arg(privacyUrl) + } + + onLinkActivated: function(link) { + Qt.openUrlExternally(link) + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index cb5efa13..6613dbf2 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -505,7 +505,13 @@ PageType { exportTypeSelector.currentIndex = 0 } selectedIndex = exportTypeSelector.currentIndex - exportTypeSelector.text = selectedText + if (model.length > 0 && model[selectedIndex] && model[selectedIndex].name !== undefined) { + exportTypeSelectorListView.selectedText = model[selectedIndex].name + exportTypeSelector.text = model[selectedIndex].name + } else { + exportTypeSelectorListView.selectedText = "" + exportTypeSelector.text = "" + } } rootWidth: root.width diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index e32381ee..e731704d 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -278,7 +278,6 @@ PageType { } Keys.onPressed: function(event) { - console.debug(">>>> ", event.key, " Event is caught by StartPage") switch (event.key) { case Qt.Key_Tab: case Qt.Key_Down: @@ -304,7 +303,7 @@ PageType { anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom - + // Also adjust TabBar position when keyboard appears (Android 14+ workaround) anchors.bottomMargin: SettingsController.imeHeight diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index ae43455a..4eba5b3b 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -83,6 +83,11 @@ Window { } } + Loader { + active: Qt.platform.os === "android" + source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : "" + } + Connections { objectName: "pageControllerConnections" diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index afe47840..3997ed02 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -39,9 +39,8 @@ VpnConnection::VpnConnection(std::shared_ptr settings, QObject *parent { #if defined(Q_OS_IOS) || defined(MACOS_NE) m_checkTimer.setInterval(1000); - connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged); + connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::setConnectionState); connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged); - #endif } @@ -59,7 +58,7 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled) #ifdef AMNEZIA_DESKTOP IpcClient::withInterface([enabled](QSharedPointer iface){ QRemoteObjectPendingReply reply = iface->refreshKillSwitch(enabled); - if (reply.waitForFinished(1000) && reply.returnValue()) + if (reply.waitForFinished() && reply.returnValue()) qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed"; else qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call"; @@ -73,60 +72,57 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) auto container = m_settings->defaultContainer(m_settings->defaultServerIndex()); IpcClient::withInterface([&](QSharedPointer iface) { - if (state == Vpn::ConnectionState::Connected) { - iface->resetIpStack(); - iface->flushDns(); + switch (state) { + case Vpn::ConnectionState::Connected: { + iface->resetIpStack(); - if (!ContainerProps::isAwgContainer(container) && - container != DockerContainer::WireGuard) { - QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); - QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); + auto flushDns = iface->flushDns(); + if (flushDns.waitForFinished() && flushDns.returnValue()) + qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS"; + else + qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes"; - iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); - if (m_settings->isSitesSplitTunnelingEnabled()) { - iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); - // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - QTimer::singleShot(1000, m_vpnProtocol.data(), - [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1"); - iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1"); + if (!ContainerProps::isAwgContainer(container) && + container != DockerContainer::WireGuard) { + QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); + QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); - iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress()); - addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode()); + // TODO: add error code handling for all routeAddList (or rework the code below) + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); + + if (m_settings->isSitesSplitTunnelingEnabled()) { + iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); + // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); + if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { + QTimer::singleShot(1000, m_vpnProtocol.data(), + [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); + } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1"); + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1"); + + iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress()); + addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode()); + } } } - } + } break; + case Vpn::ConnectionState::Disconnected: + case Vpn::ConnectionState::Error: { + auto flushDns = iface->flushDns(); + if (flushDns.waitForFinished() && flushDns.returnValue()) + qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS"; + else + qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS"; - if (container != DockerContainer::Ipsec) { - if (startNetworkCheckIfReady()) { - m_pendingNetworkCheck = false; - } else { - m_pendingNetworkCheck = true; - qWarning() << "Deferring startNetworkCheck; missing gateway/local address" - << m_vpnProtocol->vpnGateway() << m_vpnProtocol->vpnLocalAddress(); - } - } else { - m_pendingNetworkCheck = false; - } - - } else if (state == Vpn::ConnectionState::Error) { - m_pendingNetworkCheck = false; - iface->flushDns(); - - if (m_settings->isSitesSplitTunnelingEnabled()) { - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - iface->clearSavedRoutes(); - } - } - } else if (state == Vpn::ConnectionState::Connecting) { - - } else if (state == Vpn::ConnectionState::Disconnected) { - m_pendingNetworkCheck = false; - auto result = iface->stopNetworkCheck(); - result.waitForFinished(3000); + auto clearSavedRoutes = iface->clearSavedRoutes(); + if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue()) + qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes"; + else + qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes"; + } break; + default: + break; } }); #endif @@ -140,7 +136,6 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) m_checkTimer.stop(); } #endif - emit connectionStateChanged(state); } const QString &VpnConnection::remoteAddress() const @@ -185,7 +180,11 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode) }); m_settings->addVpnSite(mode, site, ip); } - flushDns(); + IpcClient::withInterface([](QSharedPointer iface) { + auto reply = iface->flushDns(); + if (reply.waitForFinished() || !reply.returnValue()) + qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS"; + }); break; } } @@ -200,48 +199,6 @@ QSharedPointer VpnConnection::vpnProtocol() const return m_vpnProtocol; } -void VpnConnection::addRoutes(const QStringList &ips) -{ -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([&](QSharedPointer iface) { - if (connectionState() == Vpn::ConnectionState::Connected) { - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - iface->routeAddList(m_vpnProtocol->vpnGateway(), ips); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - iface->routeAddList(m_vpnProtocol->routeGateway(), ips); - } - } - }); -#endif -} - -void VpnConnection::deleteRoutes(const QStringList &ips) -{ -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([&](QSharedPointer iface) { - if (connectionState() == Vpn::ConnectionState::Connected) { - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - iface->routeDeleteList(vpnProtocol()->vpnGateway(), ips); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - iface->routeDeleteList(m_vpnProtocol->routeGateway(), ips); - } - } - }); -#endif -} - -void VpnConnection::flushDns() -{ -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([](QSharedPointer iface) { - auto reply = iface->flushDns(); - if (reply.waitForFinished(1000) || !reply.returnValue()) { - qWarning() << "VpnConnection::flushDns(): Failed to flush DNS"; - } - }); -#endif -} - void VpnConnection::disconnectSlots() { if (m_vpnProtocol) { @@ -265,19 +222,15 @@ ErrorCode VpnConnection::lastError() const void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration) { - qDebug() << QString("ConnectToVpn, Server index is %1, container is %2, route mode is") + qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is") .arg(serverIndex) .arg(ContainerProps::containerToString(container)) << m_settings->routeMode(); m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName); - emit connectionStateChanged(Vpn::ConnectionState::Connecting); + setConnectionState(Vpn::ConnectionState::Connecting); - m_pendingNetworkCheck = false; m_vpnConfiguration = vpnConfiguration; - m_serverIndex = serverIndex; - m_serverCredentials = credentials; - m_dockerContainer = container; #ifdef AMNEZIA_DESKTOP if (m_vpnProtocol) { @@ -293,7 +246,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration)); if (!m_vpnProtocol) { - emit connectionStateChanged(Vpn::ConnectionState::Error); + setConnectionState(Vpn::ConnectionState::Error); return; } m_vpnProtocol->prepare(); @@ -311,75 +264,23 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede createProtocolConnections(); - ErrorCode errorCode = m_vpnProtocol->start(); - if (errorCode != ErrorCode::NoError) - emit connectionStateChanged(Vpn::ConnectionState::Error); -} - -void VpnConnection::restartConnection() -{ - // Only reconnect if VPN was connected before sleep/network change - if (!m_wasConnectedBeforeSleep) { - qDebug() << "VPN was not connected before sleep/network change, skipping reconnection"; - return; + if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) { + setConnectionState(Vpn::ConnectionState::Error); + emit vpnProtocolError(err); } - - qDebug() << "VPN was connected before sleep/network change, attempting reconnection"; - this->disconnectFromVpn(); -#ifdef Q_OS_LINUX - QThread::msleep(5000); -#endif - this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration); - - // Reset the flag after reconnection attempt - m_wasConnectedBeforeSleep = false; } void VpnConnection::createProtocolConnections() { connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError); - connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this, - SLOT(onConnectionStateChanged(Vpn::ConnectionState))); + connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::setConnectionState); connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64))); #ifdef AMNEZIA_DESKTOP - if (m_connectionLoseHandle) - disconnect(m_connectionLoseHandle); - if (m_networkChangeHandle) - disconnect(m_networkChangeHandle); - m_connectionLoseHandle = QMetaObject::Connection(); - m_networkChangeHandle = QMetaObject::Connection(); - - // TODO: replace unsafe IpcClient::Interface() calls - m_connectionLoseHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::connectionLose, - this, [this]() { - qDebug() << "Connection Lose"; - auto result = IpcClient::Interface()->stopNetworkCheck(); - result.waitForFinished(3000); - // Track VPN state before connection loss - m_wasConnectedBeforeSleep = isConnected(); - qDebug() << "VPN was connected before connection loss:" << m_wasConnectedBeforeSleep; - this->restartConnection(); - }); - m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange, - this, [this]() { - qDebug() << "Network change"; - // Track VPN state before network change (including sleep/wake) - m_wasConnectedBeforeSleep = isConnected(); - qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep; - this->restartConnection(); - }); - connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated, - this, [this](const QString& gateway, const QString& localAddress) { - Q_UNUSED(gateway) - Q_UNUSED(localAddress) - if (connectionState() != Vpn::ConnectionState::Connected) { - return; - } - if (startNetworkCheckIfReady()) { - m_pendingNetworkCheck = false; - } - }); + IpcClient::withInterface([this](QSharedPointer rep) { + connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection); + connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection); + }); #endif } @@ -482,28 +383,13 @@ void VpnConnection::appendSplitTunnelingConfig() m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode); m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray); -} -bool VpnConnection::startNetworkCheckIfReady() -{ -#ifdef AMNEZIA_DESKTOP - if (!m_vpnProtocol || m_dockerContainer == DockerContainer::Ipsec) { - return false; - } - - const QString gateway = m_vpnProtocol->vpnGateway(); - const QString localAddress = m_vpnProtocol->vpnLocalAddress(); - if (gateway.isEmpty() || localAddress.isEmpty()) { - return false; - } - - return IpcClient::withInterface([&](QSharedPointer iface) { - QRemoteObjectPendingReply reply = iface->startNetworkCheck(gateway, localAddress); - return reply.waitForFinished(1000) && reply.returnValue(); - }); -#else - return false; -#endif + qDebug() << QString("Site split tunneling is %1, route mode is %2") + .arg(m_settings->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled") + .arg(routeMode); + qDebug() << QString("App split tunneling is %1, route mode is %2") + .arg(m_settings->isAppsSplitTunnelingEnabled() ? "enabled" : "disabled") + .arg(appsRouteMode); } #ifdef Q_OS_ANDROID @@ -537,6 +423,27 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes) return QString("%1 %2").arg(QString::number(mbps, 'f', 2)).arg(tr("Mbps")); // Mbit/s } +void VpnConnection::reconnectToVpn() { + if (m_vpnProtocol.isNull()) + return; + + if (m_connectionState != Vpn::ConnectionState::Connected) { + qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot") + .arg(QMetaEnum::fromType().valueToKey(m_connectionState)); + return; + } + + qDebug() << "Reconnect triggered. Reconnecting to the server"; + + setConnectionState(Vpn::ConnectionState::Reconnecting); + + m_vpnProtocol->stop(); + if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) { + setConnectionState(Vpn::ConnectionState::Error); + emit vpnProtocolError(err); + } +} + void VpnConnection::disconnectFromVpn() { #if defined(Q_OS_IOS) || defined(MACOS_NE) @@ -546,41 +453,26 @@ void VpnConnection::disconnectFromVpn() #endif if (m_vpnProtocol.isNull()) { - emit connectionStateChanged(Vpn::ConnectionState::Disconnected); + setConnectionState(Vpn::ConnectionState::Disconnected); return; } - m_vpnProtocol->stop(); - -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([](QSharedPointer iface) { - QRemoteObjectPendingReply flushReply = iface->flushDns(); - if (flushReply.waitForFinished(5000) && flushReply.returnValue()) - qDebug() << "VpnConnection::disconnectFromVpn(): Successfully flushed DNS"; - else - qWarning() << "VpnConnection::disconnectFromVpn(): Failed to flush DNS"; - - QRemoteObjectPendingReply clearSavedRoutesReply = iface->clearSavedRoutes(); - if (clearSavedRoutesReply.waitForFinished(5000) && clearSavedRoutesReply.returnValue()) - qDebug() << "VpnConnection::disconnectFromVpn(): Successfully cleared saved routes"; - else - qWarning() << "VpnConnection::disconnectFromVpn(): Failed to clear saved routes"; - }); -#endif + setConnectionState(Vpn::ConnectionState::Disconnecting); #ifdef Q_OS_ANDROID auto *const connection = new QMetaObject::Connection; *connection = connect(AndroidController::instance(), &AndroidController::vpnStateChanged, this, [this, connection](AndroidController::ConnectionState state) { if (state == AndroidController::ConnectionState::DISCONNECTED) { - onConnectionStateChanged(Vpn::ConnectionState::Disconnected); + setConnectionState(Vpn::ConnectionState::Disconnected); disconnect(*connection); delete connection; } }); - m_vpnProtocol->stop(); #endif + m_vpnProtocol->stop(); + #if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP) m_vpnProtocol->deleteLater(); #endif @@ -588,27 +480,12 @@ void VpnConnection::disconnectFromVpn() m_vpnProtocol = nullptr; } -Vpn::ConnectionState VpnConnection::connectionState() -{ - if (!m_vpnProtocol) - return Vpn::ConnectionState::Disconnected; - return m_vpnProtocol->connectionState(); -} - -bool VpnConnection::isConnected() const -{ - if (m_vpnProtocol.isNull()) { - return false; - } - - return m_vpnProtocol->isConnected(); -} - -bool VpnConnection::isDisconnected() const -{ - if (m_vpnProtocol.isNull()) { - return true; - } - - return m_vpnProtocol->isDisconnected(); +void VpnConnection::setConnectionState(Vpn::ConnectionState state) { + onConnectionStateChanged(state); + + if (state == Vpn::Disconnected && m_connectionState == Vpn::Reconnecting) + return; + + m_connectionState = state; + emit connectionStateChanged(state); } diff --git a/client/vpnconnection.h b/client/vpnconnection.h index 070ab36f..777573f2 100644 --- a/client/vpnconnection.h +++ b/client/vpnconnection.h @@ -34,10 +34,6 @@ public: ErrorCode lastError() const; - bool isConnected() const; - bool isDisconnected() const; - - Vpn::ConnectionState connectionState(); QSharedPointer vpnProtocol() const; const QString &remoteAddress() const; @@ -48,15 +44,10 @@ public: #endif public slots: - void connectToVpn(int serverIndex, - const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); - + void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); + void reconnectToVpn(); void disconnectFromVpn(); - void restartConnection(); - void addRoutes(const QStringList &ips); - void deleteRoutes(const QStringList &ips); - void flushDns(); void onKillSwitchModeChanged(bool enabled); void disconnectSlots(); @@ -71,10 +62,10 @@ protected slots: void onBytesChanged(quint64 receivedBytes, quint64 sentBytes); void onConnectionStateChanged(Vpn::ConnectionState state); + void setConnectionState(Vpn::ConnectionState state); + protected: QSharedPointer m_vpnProtocol; - QMetaObject::Connection m_connectionLoseHandle; - QMetaObject::Connection m_networkChangeHandle; private: std::shared_ptr m_settings; @@ -82,14 +73,6 @@ private: QJsonObject m_routeMode; QString m_remoteAddress; - ServerCredentials m_serverCredentials; - int m_serverIndex; - DockerContainer m_dockerContainer; - - // Track VPN state before sleep for smart reconnection - bool m_wasConnectedBeforeSleep = false; - bool m_pendingNetworkCheck = false; - // Only for iOS for now, check counters QTimer m_checkTimer; @@ -100,11 +83,12 @@ private: void createAndroidConnections(); #endif + Vpn::ConnectionState m_connectionState; + void createProtocolConnections(); void appendSplitTunnelingConfig(); void appendKillSwitchConfig(); - bool startNetworkCheckIfReady(); }; #endif // VPNCONNECTION_H diff --git a/deploy/build_windows.bat b/deploy/build_windows.bat index c951fb8c..c841b68d 100644 --- a/deploy/build_windows.bat +++ b/deploy/build_windows.bat @@ -31,6 +31,7 @@ set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=% set APP_NAME=DefaultVPN set APP_FILENAME=%APP_NAME:"=%.exe +set SERVICE_FILENAME=%APP_NAME:"=%-service.exe set APP_DOMAIN=org.defaultvpn.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 diff --git a/deploy/deploy_s3.sh b/deploy/deploy_s3.sh index 14368ae9..70cbde60 100755 --- a/deploy/deploy_s3.sh +++ b/deploy/deploy_s3.sh @@ -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,20 +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}/DefaultVPN_${VERSION}_android9+_arm64-v8a.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_armeabi-v7a.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86_64.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_linux_x64.tar -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_macos.pkg -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_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}/DefaultVPN_${VERSION}_android9+_arm64-v8a.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_armeabi-v7a.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86_64.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_linux_x64.tar +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_macos.pkg +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${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!" diff --git a/ipc/ipc.h b/ipc/ipc.h index c15b7a63..b242ad14 100644 --- a/ipc/ipc.h +++ b/ipc/ipc.h @@ -11,6 +11,7 @@ namespace amnezia { enum PermittedProcess { + Invalid, OpenVPN, Wireguard, Tun2Socks, @@ -19,16 +20,18 @@ enum PermittedProcess { inline QString permittedProcessPath(PermittedProcess pid) { - if (pid == PermittedProcess::OpenVPN) { - return Utils::openVpnExecPath(); - } else if (pid == PermittedProcess::Wireguard) { - return Utils::wireguardExecPath(); - } else if (pid == PermittedProcess::CertUtil) { - return Utils::certUtilPath(); - } else if (pid == PermittedProcess::Tun2Socks) { - return Utils::tun2socksPath(); + switch (pid) { + case PermittedProcess::OpenVPN: + return Utils::openVpnExecPath(); + case PermittedProcess::Wireguard: + return Utils::wireguardExecPath(); + case PermittedProcess::CertUtil: + return Utils::certUtilPath(); + case PermittedProcess::Tun2Socks: + return Utils::tun2socksPath(); + default: + return ""; } - return ""; } @@ -48,6 +51,51 @@ inline QString getIpcProcessUrl(int pid) { #endif } +inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &args) { + using Validator = std::function; + QMap namedArgs; + QList positionalArgs; + + switch (proc) { + case Tun2Socks: + namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); }; + namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); }; + break; + default: + //FIXME + return args; + } + + + QStringList sanitized; + + for (int i = 0, pos = 0; i < args.size(); i++) { + const auto& key = args[i]; + + if (const auto found = namedArgs.find(key); found != namedArgs.end()) { + const auto validator = found.value(); + + if (validator) { + if (i + 1 < args.size()) { + const auto& value = args[i+1]; + if (validator(value)) { + sanitized << key << value; + i++; + } + } + } else { + sanitized << key; + } + } else if (pos < positionalArgs.size()) { + if (const auto validator = positionalArgs[pos]; validator && validator(key)) { + sanitized << key; + pos++; + } + } + } + + return sanitized; +} } // namespace amnezia diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 2320c6a4..f26bd8b3 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -38,12 +38,13 @@ class IpcInterface SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); SLOT( bool restoreResolvers() ); - SLOT(void xrayStart(const QString &config)); - SLOT(void xrayStop()); + SLOT(bool xrayStart(const QString &config)); + SLOT(bool xrayStop()); SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) ); SLOT( bool stopNetworkCheck() ); SIGNAL( connectionLose() ); - SIGNAL( networkChange() ); + SIGNAL( wakeup() ); + SIGNAL( networkChanged() ); }; diff --git a/ipc/ipc_process_interface.rep b/ipc/ipc_process_interface.rep index 6b3bb654..7bf2ed22 100644 --- a/ipc/ipc_process_interface.rep +++ b/ipc/ipc_process_interface.rep @@ -4,6 +4,8 @@ class IpcProcessInterface { SLOT( start() ); + SLOT( terminate() ); + SLOT( kill() ); SLOT( close() ); SLOT( setArguments(const QStringList &arguments) ); @@ -17,6 +19,11 @@ class IpcProcessInterface SLOT( QByteArray readAllStandardError() ); SLOT( QByteArray readAllStandardOutput() ); + SLOT( bool waitForFinished() ); + SLOT( bool waitForFinished(int msecs) ); + SLOT( bool waitForStarted() ); + SLOT( bool waitForStarted(int msecs) ); + SIGNAL( errorOccurred(QProcess::ProcessError error) ); SIGNAL( finished(int exitCode, QProcess::ExitStatus exitStatus) ); diff --git a/ipc/ipc_process_tun2socks.rep b/ipc/ipc_process_tun2socks.rep deleted file mode 100644 index e355035e..00000000 --- a/ipc/ipc_process_tun2socks.rep +++ /dev/null @@ -1,11 +0,0 @@ -#include -#include - -class IpcProcessTun2Socks -{ - SLOT( start() ); - SLOT( stop() ); - - SIGNAL( setConnectionState(int state) ); - SIGNAL( stateChanged(QProcess::ProcessState newState) ); -}; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 77f3a351..4d02c1dd 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -304,7 +304,7 @@ bool IpcServer::refreshKillSwitch(bool enabled) return KillSwitch::instance()->refresh(enabled); } -void IpcServer::xrayStart(const QString& cfg) +bool IpcServer::xrayStart(const QString& cfg) { #ifdef MZ_DEBUG qDebug() << "IpcServer::xrayStart"; @@ -313,7 +313,7 @@ void IpcServer::xrayStart(const QString& cfg) return Xray::getInstance().startXray(cfg); } -void IpcServer::xrayStop() +bool IpcServer::xrayStop() { #ifdef MZ_DEBUG qDebug() << "IpcServer::xrayStop"; diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 5a63302c..e8607c5a 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -10,10 +10,8 @@ #include "ipc.h" #include "ipcserverprocess.h" -#include "ipctun2socksprocess.h" #include "rep_ipc_interface_source.h" -#include "rep_ipc_process_tun2socks_source.h" class IpcServer : public IpcInterfaceSource { @@ -44,8 +42,8 @@ public: virtual bool refreshKillSwitch( bool enabled ) override; virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; virtual bool restoreResolvers() override; - virtual void xrayStart(const QString& cfg) override; - virtual void xrayStop() override; + virtual bool xrayStart(const QString& cfg) override; + virtual bool xrayStop() override; virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override; virtual bool stopNetworkCheck() override; @@ -56,12 +54,10 @@ private: ProcessDescriptor (QObject *parent = nullptr) { serverNode = QSharedPointer(new QRemoteObjectHost(parent)); ipcProcess = QSharedPointer(new IpcServerProcess(parent)); - tun2socksProcess = QSharedPointer(new IpcProcessTun2Socks(parent)); localServer = QSharedPointer(new QLocalServer(parent)); } QSharedPointer ipcProcess; - QSharedPointer tun2socksProcess; QSharedPointer serverNode; QSharedPointer localServer; }; diff --git a/ipc/ipcserverprocess.cpp b/ipc/ipcserverprocess.cpp index 497e89d7..4890e688 100644 --- a/ipc/ipcserverprocess.cpp +++ b/ipc/ipcserverprocess.cpp @@ -40,6 +40,14 @@ void IpcServerProcess::start() m_process->waitForStarted(); } +void IpcServerProcess::terminate() { + m_process->terminate(); +} + +void IpcServerProcess::kill() { + m_process->kill(); +} + void IpcServerProcess::close() { m_process->close(); @@ -47,7 +55,7 @@ void IpcServerProcess::close() void IpcServerProcess::setArguments(const QStringList &arguments) { - m_process->setArguments(arguments); + m_process->setArguments(amnezia::sanitizeArguments(m_program, arguments)); } void IpcServerProcess::setInputChannelMode(QProcess::InputChannelMode mode) @@ -69,7 +77,9 @@ void IpcServerProcess::setProcessChannelMode(QProcess::ProcessChannelMode mode) void IpcServerProcess::setProgram(int programId) { - m_process->setProgram(amnezia::permittedProcessPath(static_cast(programId))); + m_program = static_cast(programId); + m_process->setProgram(amnezia::permittedProcessPath(m_program)); + m_process->setArguments({}); } void IpcServerProcess::setWorkingDirectory(const QString &dir) @@ -92,4 +102,20 @@ QByteArray IpcServerProcess::readAllStandardOutput() return m_process->readAllStandardOutput(); } +bool IpcServerProcess::waitForStarted() { + return m_process->waitForStarted(); +} + +bool IpcServerProcess::waitForStarted(int msecs) { + return m_process->waitForStarted(msecs); +} + +bool IpcServerProcess::waitForFinished() { + return m_process->waitForFinished(); +} + +bool IpcServerProcess::waitForFinished(int msecs) { + return m_process->waitForFinished(msecs); +} + #endif diff --git a/ipc/ipcserverprocess.h b/ipc/ipcserverprocess.h index b427d639..722b04f0 100644 --- a/ipc/ipcserverprocess.h +++ b/ipc/ipcserverprocess.h @@ -1,6 +1,7 @@ #ifndef IPCSERVERPROCESS_H #define IPCSERVERPROCESS_H +#include "ipc.h" #include #ifndef Q_OS_IOS @@ -14,6 +15,8 @@ public: virtual ~IpcServerProcess(); void start() override; + void terminate() override; + void kill() override; void close() override; void setArguments(const QStringList &arguments) override; @@ -27,9 +30,15 @@ public: QByteArray readAllStandardError() override; QByteArray readAllStandardOutput() override; + bool waitForStarted() override; + bool waitForStarted(int msecs) override; + bool waitForFinished() override; + bool waitForFinished(int msecs) override; + signals: private: + amnezia::PermittedProcess m_program = amnezia::PermittedProcess::Invalid; QSharedPointer m_process; }; diff --git a/ipc/ipctun2socksprocess.cpp b/ipc/ipctun2socksprocess.cpp deleted file mode 100644 index 65d801b0..00000000 --- a/ipc/ipctun2socksprocess.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "ipctun2socksprocess.h" -#include "ipc.h" -#include -#include - -#include "../protocols/protocols_defs.h" - -#ifndef Q_OS_IOS - -IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) : - IpcProcessTun2SocksSource(parent), - m_t2sProcess(QSharedPointer(new QProcess())) -{ - qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()"; - -} - -IpcProcessTun2Socks::~IpcProcessTun2Socks() -{ - qDebug() << "IpcProcessTun2Socks::~IpcProcessTun2Socks()"; -} - -void IpcProcessTun2Socks::start() -{ - connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged); - qDebug() << "IpcProcessTun2Socks::start()"; - m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast(amnezia::PermittedProcess::Tun2Socks))); - - QString XrayConStr = "socks5://127.0.0.1:10808"; - -#ifdef Q_OS_WIN - QStringList arguments({"-device", "tun://tun2?guid={081A8A84-8D12-4DF5-B8C4-396D5B0053E4}", "-proxy", XrayConStr, "-tun-post-up", - QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255") - .arg(amnezia::protocols::xray::defaultLocalAddr)}); -#endif -#ifdef Q_OS_LINUX - QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr}); -#endif -#ifdef Q_OS_MAC - QStringList arguments({"-device", "utun22", "-proxy", XrayConStr}); -#endif - - m_t2sProcess->setArguments(arguments); - - if (Utils::processIsRunning(Utils::executable("tun2socks", false))) { - qDebug().noquote() << "kill previos tun2socks"; - Utils::killProcessByName(Utils::executable("tun2socks", false)); - } - - m_t2sProcess->start(); - - connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() { - QString line = m_t2sProcess.data()->readAllStandardOutput(); - if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) { - emit setConnectionState(Vpn::ConnectionState::Connected); - } - }); - - connect(m_t2sProcess.data(), QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { - qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus; - emit setConnectionState(Vpn::ConnectionState::Disconnected); - if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { - emit setConnectionState(Vpn::ConnectionState::Error); - } - - }); - - m_t2sProcess->start(); - m_t2sProcess->waitForStarted(); -} - -void IpcProcessTun2Socks::stop() -{ - qDebug() << "IpcProcessTun2Socks::stop()"; - m_t2sProcess->disconnect(); - m_t2sProcess->kill(); - m_t2sProcess->waitForFinished(3000); -} -#endif diff --git a/ipc/ipctun2socksprocess.h b/ipc/ipctun2socksprocess.h deleted file mode 100644 index 8ce9be1a..00000000 --- a/ipc/ipctun2socksprocess.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef IPCTUN2SOCKSPROCESS_H -#define IPCTUN2SOCKSPROCESS_H - -#include - -#ifndef Q_OS_IOS -#include "rep_ipc_process_tun2socks_source.h" - -namespace Vpn -{ -Q_NAMESPACE - enum ConnectionState { - Unknown, - Disconnected, - Preparing, - Connecting, - Connected, - Disconnecting, - Reconnecting, - Error - }; -Q_ENUM_NS(ConnectionState) -} - - -class IpcProcessTun2Socks : public IpcProcessTun2SocksSource -{ - Q_OBJECT -public: - explicit IpcProcessTun2Socks(QObject *parent = nullptr); - virtual ~IpcProcessTun2Socks(); - - void start() override; - void stop() override; - -signals: - -private: - QSharedPointer m_t2sProcess; -}; - -#else -class IpcProcessTun2Socks : public QObject -{ - Q_OBJECT - -public: - explicit IpcProcessTun2Socks(QObject *parent = nullptr); -}; -#endif - -#endif // IPCTUN2SOCKSPROCESS_H diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index d1185bd3..05a3de37 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,7 +6,7 @@ project(${PROJECT}) 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() @@ -75,7 +75,6 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h - ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.h ${CMAKE_CURRENT_LIST_DIR}/localserver.h ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h ${CMAKE_CURRENT_LIST_DIR}/router.h @@ -97,7 +96,6 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp - ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.cpp ${CMAKE_CURRENT_LIST_DIR}/localserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp @@ -353,7 +351,7 @@ include_directories( add_executable(${PROJECT} ${SOURCES} ${HEADERS} ${RESOURCES}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus ${LIBS}) +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_$") if(CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -389,7 +387,6 @@ endif() qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep) qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep) -qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_tun2socks.rep) # copy deploy artifacts required to run the application to the debug build folder if(WIN32) diff --git a/service/server/killswitch.cpp b/service/server/killswitch.cpp index 6f77d8b3..a0e5c7e8 100644 --- a/service/server/killswitch.cpp +++ b/service/server/killswitch.cpp @@ -33,18 +33,10 @@ KillSwitch* KillSwitch::instance() bool KillSwitch::init() { -#ifdef Q_OS_LINUX - if (!LinuxFirewall::isInstalled()) { - LinuxFirewall::install(); - } - m_appSettigns = QSharedPointer(new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, nullptr)); -#endif -#ifdef Q_OS_MACOS - if (!MacOSFirewall::isInstalled()) { - MacOSFirewall::install(); - } +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) m_appSettigns = QSharedPointer(new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, nullptr)); #endif + if (isStrictKillSwitchEnabled()) { return disableAllTraffic(); } @@ -79,7 +71,6 @@ bool KillSwitch::isStrictKillSwitchEnabled() + "\\" + QString(APPLICATION_NAME), QSettings::NativeFormat); return RegHLM.value("strictKillSwitchEnabled", false).toBool(); #endif - m_appSettigns->sync(); return m_appSettigns->value("Conf/strictKillSwitchEnabled", false).toBool(); } diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 0a24d9db..26706079 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -40,7 +40,6 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), if (!m_isRemotingEnabled) { m_isRemotingEnabled = true; m_serverNode.enableRemoting(&m_ipcServer); - m_serverNode.enableRemoting(&m_tun2socks); } }); @@ -51,8 +50,8 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), } m_networkWatcher.initialize(); - connect(&m_networkWatcher, &NetworkWatcher::sleepMode, &m_ipcServer, &IpcServer::networkChange); - connect(&m_networkWatcher, &NetworkWatcher::networkChange, &m_ipcServer, &IpcServer::networkChange); + connect(&m_networkWatcher, &NetworkWatcher::networkChanged, &m_ipcServer, &IpcServer::networkChanged); + connect(&m_networkWatcher, &NetworkWatcher::wakeup, &m_ipcServer, &IpcServer::wakeup); KillSwitch::instance()->init(); #ifdef Q_OS_LINUX diff --git a/service/server/localserver.h b/service/server/localserver.h index 47a7640d..3a885904 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -38,7 +38,6 @@ public: ~LocalServer(); QSharedPointer m_server; IpcServer m_ipcServer; - IpcProcessTun2Socks m_tun2socks; QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; diff --git a/service/server/router.cpp b/service/server/router.cpp index 24e82c0e..8849d27b 100644 --- a/service/server/router.cpp +++ b/service/server/router.cpp @@ -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 diff --git a/service/server/router_win.cpp b/service/server/router_win.cpp index 7ad6505d..6cb2e38e 100644 --- a/service/server/router_win.cpp +++ b/service/server/router_win.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -308,6 +309,77 @@ void RouterWin::resetIpStack() } } +bool RouterWin::createTun(const QString &dev, const QString &subnet) +{ + NET_LUID luid; + DWORD res = ConvertInterfaceAliasToLuid(reinterpret_cast(dev.utf16()), &luid); + if (res != NO_ERROR) { + qCritical() << "Failed to convert luid: " << res; + return false; + } + + HANDLE hEvent = CreateEvent(nullptr, true, false, nullptr); + if (!hEvent) { + qCritical() << "Failed to allocate event object"; + return false; + } + auto _guardEvent = qScopeGuard([hEvent](){ CloseHandle(hEvent); }); + + struct { + HANDLE hEvent; + NET_LUID luid; + const QString &subnet; + bool found; + } ctx = { .hEvent = hEvent, .luid = luid, .subnet = subnet, .found = false }; + + auto cb = [](void *priv, MIB_UNICASTIPADDRESS_ROW *row, MIB_NOTIFICATION_TYPE NotificationType) { + auto* c = reinterpret_cast(priv); + if (row != nullptr && row->InterfaceLuid.Value == c->luid.Value && row->Address.si_family == AF_INET) { + char ip[INET_ADDRSTRLEN]; + inet_ntop(row->Address.Ipv4.sin_family, &row->Address.Ipv4.sin_addr, ip, INET_ADDRSTRLEN); + if (c->subnet == ip) { + c->found = true; + SetEvent(c->hEvent); + } + } + }; + + HANDLE hNotif; + res = NotifyUnicastIpAddressChange(AF_INET, cb, &ctx, false, &hNotif); + if (res != NO_ERROR) { + qCritical() << "Failed to subscribe to interface change"; + return false; + } + auto _guardNotif = qScopeGuard([hNotif](){ CancelMibChangeNotify2(hNotif); }); + + MIB_UNICASTIPADDRESS_ROW row; + InitializeUnicastIpAddressEntry(&row); + + row.InterfaceLuid = luid; + row.Address.si_family = AF_INET; + + inet_pton(AF_INET, subnet.toStdString().c_str(), &row.Address.Ipv4.sin_addr); + + row.OnLinkPrefixLength = 32; + row.ValidLifetime = 0xffffffff; + row.PreferredLifetime = 0xffffffff; + row.DadState = IpDadStatePreferred; + + res = CreateUnicastIpAddressEntry(&row); + if (res != NO_ERROR && res != ERROR_OBJECT_ALREADY_EXISTS) { + qDebug() << "Failed to create IP address:" << res; + return false; + } + + res = WaitForSingleObject(hEvent, 10000); + if (res == WAIT_TIMEOUT) { + qCritical() << "Timeout of waiting for IP assignment for " << dev << " device"; + return false; + } + + return ctx.found; +} + void RouterWin::suspendWcmSvc(bool suspend) { if (suspend == m_suspended) return; @@ -465,11 +537,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 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 +557,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 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; } diff --git a/service/server/router_win.h b/service/server/router_win.h index c2412cc5..9258b734 100644 --- a/service/server/router_win.h +++ b/service/server/router_win.h @@ -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& resolvers); bool restoreResolvers(); diff --git a/service/server/xray.cpp b/service/server/xray.cpp index 2d3f9a79..b0408d49 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -27,7 +27,7 @@ #include #endif -void Xray::startXray(const QString &cfg) +bool Xray::startXray(const QString &cfg) { qDebug() << "Xray::startXray()"; @@ -40,34 +40,38 @@ void Xray::startXray(const QString &cfg) if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) { qDebug() << "[xray] sockopt failed: " << err; - free(err); - return; - } - - QByteArray bytes = cfg.toUtf8(); - if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { - qDebug() << "[xray] configuration failed: " << err; - free(err); - return; + amnezia_xray_free(err); + return false; } amnezia_xray_setloghandler(ctxLogHandler, this); + QByteArray bytes = cfg.toUtf8(); + if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { + qDebug() << "[xray] configuration failed: " << err; + amnezia_xray_free(err); + return false; + } + if (auto err = amnezia_xray_start(); err != nullptr) { qDebug() << "[xray] failed to start: " << err; - free(err); - return; + amnezia_xray_free(err); + return false; } + + return true; } -void Xray::stopXray() +bool Xray::stopXray() { qDebug() << "Xray::stopXray()"; if (auto err = amnezia_xray_stop(); err != nullptr) { qDebug() << "[xray] failed to stop: " << err; - free(err); - return; + amnezia_xray_free(err); + return false; } + + return true; } void Xray::logHandler(char* str) diff --git a/service/server/xray.h b/service/server/xray.h index c199734a..f54d9902 100644 --- a/service/server/xray.h +++ b/service/server/xray.h @@ -12,8 +12,8 @@ public: return instance; } - void startXray(const QString& cfg); - void stopXray(); + bool startXray(const QString& cfg); + bool stopXray(); private: static void ctxSockCallback(uintptr_t fd, void* ctx) {