diff --git a/.gitignore b/.gitignore index e05974b0c..a48886bfc 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 90edb5822..11845060a 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 9df417f23..ca4ab5f83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -set(AMNEZIAVPN_VERSION 4.8.12.9) +set(AMNEZIAVPN_VERSION 4.8.14.5) project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} DESCRIPTION "AmneziaVPN" @@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2105) +set(APP_ANDROID_VERSION_CODE 2118) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") @@ -61,6 +61,9 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE) set(CPACK_PACKAGE_VENDOR "AmneziaVPN") set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION}) set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client") + set(AMNEZIA_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt") + configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${AMNEZIA_LICENSE_TXT}" COPYONLY) + set(CPACK_RESOURCE_FILE_LICENSE "${AMNEZIA_LICENSE_TXT}") set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN") set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}") set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN") diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 579673b2e..568b8d720 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 000000000..f72b3e0c6 --- /dev/null +++ b/client/3rd/qtgamepad @@ -0,0 +1 @@ +Subproject commit f72b3e0c6229d1622b6bc7f6a5ec5ccff83460eb diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 3a9ffb27a..0f3ae7a0f 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(AMNEZIAVPN_TS_FILES ) file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui) +list(FILTER AMNEZIAVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples") qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_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/amnezia_application.cpp b/client/amnezia_application.cpp index c53059302..c6c8d672e 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -109,6 +109,16 @@ void AmneziaApplication::init() // install filter on main window if (auto win = qobject_cast(obj)) { win->installEventFilter(this); +#ifdef Q_OS_ANDROID + QObject::connect(win, &QQuickWindow::sceneGraphError, + [](QQuickWindow::SceneGraphError, const QString &msg) { + qWarning() << "Scene graph error (suppressed):" << msg; + }); + // Keep graphics context alive across hide/show cycles to avoid + // eglSwapBuffers/makeCurrent being called on a context Android has reclaimed. + win->setPersistentSceneGraph(true); + win->setPersistentGraphics(true); +#endif win->show(); } }, diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index ca61ae798..daedfda3f 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() { @@ -89,6 +93,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()) { override fun handleMessage(msg: Message) { @@ -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,103 @@ 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 keyCode = event.keyCode + val pressed = event.action == KeyEvent.ACTION_DOWN + + 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, pressed) + return true + } + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT -> { + val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode + val synthetic = KeyEvent( + event.downTime, event.eventTime, event.action, syntheticKeyCode, + event.repeatCount, event.metaState, -1, event.scanCode, + event.flags, InputDevice.SOURCE_KEYBOARD + ) + return super.dispatchKeyEvent(synthetic) + } + } + + 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() { @@ -337,31 +427,35 @@ class AmneziaActivity : QtActivity() { ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets -> val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime()) - + val imeHeight = if (imeVisible) imeInsets.bottom else 0 val density = resources.displayMetrics.density val imeHeightDp = (imeHeight / density).toInt() - + // Also track system bars (navigation bar, status bar) changes val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val navBarHeight = systemBarsInsets.bottom val navBarHeightDp = (navBarHeight / density).toInt() val statusBarHeight = systemBarsInsets.top val statusBarHeightDp = (statusBarHeight / density).toInt() - + mainScope.launch { qtInitialized.await() QtAndroidController.onImeInsetsChanged(imeHeightDp) QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp) } - + // Return windowInsets instead of CONSUMED to allow proper handling windowInsets } } 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 +781,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 33f4355f4..a3c7ec8af 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 4272290d7..2c3145d73 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/macos_ne.cmake b/client/cmake/macos_ne.cmake index 749053757..02dfb4122 100644 --- a/client/cmake/macos_ne.cmake +++ b/client/cmake/macos_ne.cmake @@ -163,7 +163,7 @@ add_custom_command(TARGET ${PROJECT} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory $/Contents/Frameworks COMMAND /usr/bin/find "$/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete - COMMAND /usr/bin/codesign --force --sign "Apple Distribution" + COMMAND /usr/bin/codesign --force --sign "Apple Distribution: Privacy Technologies OU" "$/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter" COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $ -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Signing OpenVPNAdapter framework" diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index d592b646b..cc6532894 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 26267219a..c8ea65e5d 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)); @@ -368,7 +368,11 @@ void CoreController::initPrepareConfigHandler() return; } - if (!m_installController->isConfigValid()) { + m_installController->validateConfig(); + }); + + connect(m_installController.get(), &InstallController::configValidated, this, [this](bool isValid) { + if (!isValid) { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); return; } diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 132af459f..25a40c460 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 4586b3522..d5ee3d7d3 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 74f1bf29d..ff6919195 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 3852236f6..000000000 --- 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 4d08c0436..000000000 --- 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 e88c55177..17bf5a5e9 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 c613c1067..e6e25b5ef 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 f28e74fb7..527e28e28 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 54bd8e87d..a05c08870 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 22eba3c7b..11b31fa5b 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 247a1d67f..f7cce6bb6 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 9579fcefb..7c5ae5407 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 67f3a9301..c26645664 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 8668b4db0..a0b5e18c3 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 f72baa6e2..85eb4276f 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 b518aac42..20a3416c1 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 b07d1268e..490fff832 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 869afd92f..c1cd868d8 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 1125731d1..2ae7ebcba 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 575960b2d..50bf829a7 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 10f81fbc8..bccb844a2 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 069d2a1e0..c0c20a4dc 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 8df24e742..4104e1e00 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 eef057d98..69fc8496b 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 c11295ef6..2f7b24cbe 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", true).toBool(); + return m_settings.value("Conf/premV1MigrationReminderActive", true).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 7540d0d03..b0b292195 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,20 +78,20 @@ 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 isFileEncryption() const @@ -123,16 +123,16 @@ public: 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); @@ -149,19 +149,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 @@ -174,11 +173,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; @@ -187,13 +186,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"; @@ -215,16 +214,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); } @@ -282,9 +281,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/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 6f355d327..13f8249bc 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1,9 +1,6 @@ - - AdLabel - AllowedDnsController @@ -74,26 +71,23 @@ ApiConfigsController - - - + + %1 installed successfully. %1 успешно установлен. - - + Subscription restored successfully. Подписка успешно восстановлена. - + API config reloaded Конфигурация API перезагружена - - + Successfully changed the country of connection to %1 Страна подключения изменена на %1 @@ -101,103 +95,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: - Дата покупки: + Дата покупки: @@ -223,7 +197,12 @@ Бесплатно - + + %1 $ + %1 $ + + + %1 $/month %1 $/месяц @@ -326,7 +305,7 @@ ContextMenuType - + C&ut Вырезать @@ -336,12 +315,12 @@ Копировать - + &Paste Вставить - + &SelectAll Выбрать всё @@ -413,17 +392,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>В импортированной конфигурации обнаружены потенциально опасные строки: @@ -455,47 +434,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 Сервер успешно добавлен @@ -568,19 +547,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 - Продолжить + Продолжить @@ -612,49 +588,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 Серверы @@ -707,32 +680,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 Невозможно изменить настройки во время активного соединения @@ -755,37 +728,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 Сохранить @@ -850,27 +823,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 Невозможно изменить настройки во время активного соединения @@ -899,32 +872,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 Невозможно изменить настройки во время активного соединения @@ -1013,114 +986,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 Сохранить @@ -1191,32 +1164,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 Невозможно изменить настройки во время активного соединения @@ -1686,7 +1659,7 @@ Thank you for staying with us! mailto:support@amnezia.org - + mailto:support@amnezia.org @@ -1810,72 +1783,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 @@ -2151,7 +2124,7 @@ Thank you for staying with us! Telegram - + Telegram @@ -2181,7 +2154,7 @@ Thank you for staying with us! Support tag - + Идентификатор поддержки @@ -2411,17 +2384,17 @@ Encrypted files can only be opened with the password used to encrypt themОтменить - + application name название приложения - + Open executable file Открыть исполняемый файл - + Executable files (*.*) Исполняемые файлы (*.*) @@ -2464,7 +2437,7 @@ Encrypted files can only be opened with the password used to encrypt themЗапускать в свернутом виде - + Language Язык @@ -2484,57 +2457,57 @@ Encrypted files can only be opened with the password used to encrypt themЗапускает приложение свёрнутым (работает с включенной функцией автозапуска) - - Password & Encryption - Пароль и шифрование + + News Notification + Уведомления о новостях - - Password protection for backups and configuration files - Защита паролем резервных файлов и файлов конфигурации + + 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? Сбросить настройки и удалить все данные из приложения? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. Все настройки будут сброшены до значений по умолчанию. Все установленные сервисы AmneziaVPN останутся на сервере. - + Continue Продолжить - + Cancel Отменить - + Cannot reset settings during active connection Невозможно сбросить настройки во время активного соединения @@ -3013,108 +2986,107 @@ Encrypted files can only be opened with the password used to encrypt themНовые установленные протоколы и сервисы не обнаружены - - - - + + + + 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? Вы уверены, что хотите удалить сервер из приложения? - + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения - + 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 во время активного соединения - + 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 Удалить сервер из приложения - + All installed AmneziaVPN services will still remain on the server. Все установленные сервисы и протоколы Amnezia останутся на сервере. - + Clear server from Amnezia software Очистить сервер от протоколов и сервисов Amnezia @@ -3249,13 +3221,13 @@ Encrypted files can only be opened with the password used to encrypt them - + Continue Продолжить - + Cancel Отменить @@ -3270,70 +3242,70 @@ Encrypted files can only be opened with the password used to encrypt themНевозможно изменить настройки раздельного туннелирования во время активного соединения - + 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 Добавить импортированные сайты к существующим @@ -3341,32 +3313,47 @@ Encrypted files can only be opened with the password used to encrypt them 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 Подключиться @@ -3617,22 +3604,22 @@ Encrypted files can only be opened with the password used to encrypt themВыберите тип установки - + Manual Ручная - + Choose a VPN protocol Выбрать VPN-протокол - + Skip setup Пропустить настройку - + Continue Продолжить @@ -3924,7 +3911,7 @@ Encrypted files can only be opened with the password used to encrypt them - + Users Пользователи @@ -3934,72 +3921,72 @@ Encrypted files can only be opened with the password used to encrypt themИмя пользователя - + 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 Отменить @@ -4022,7 +4009,7 @@ Encrypted files can only be opened with the password used to encrypt them - + Share Поделиться @@ -4495,75 +4482,80 @@ Encrypted files can only be opened with the password used to encrypt themDocker 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. @@ -4572,142 +4564,142 @@ Encrypted files can only be opened with the password used to encrypt them - + 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 Внутренняя ошибка @@ -4717,17 +4709,17 @@ Encrypted files can only be opened with the password used to encrypt themIPsec - + 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. Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. - + DNS Service Сервис DNS @@ -4738,7 +4730,7 @@ Encrypted files can only be opened with the password used to encrypt them - + Website in Tor network Веб-сайт в сети Tor @@ -4768,22 +4760,23 @@ Encrypted files can only be opened with the password used to encrypt themWireGuard — популярный 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-трафик под веб-трафик. Обладает высокой устойчивостью к обнаружению и обеспечивает высокую скорость соединения. - + - + 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: @@ -4800,7 +4793,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: @@ -4818,7 +4811,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. @@ -4849,7 +4842,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: @@ -4870,7 +4863,7 @@ 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. @@ -4893,7 +4886,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. @@ -4917,7 +4910,7 @@ REALITY распознаёт системы блокировки во время * Работает по протоколу TCP - + 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: @@ -4936,7 +4929,7 @@ Features: * Работает по UDP (порты 500 и 4500) - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -4982,12 +4975,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир - + 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-сервер на свой собственный. Это повысит уровень вашей конфиденциальности. @@ -5060,7 +5053,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир - + SOCKS5 proxy server Прокси-сервер SOCKS5 @@ -5207,7 +5200,6 @@ FileZilla или другие SFTP-клиенты, а также смонтир - AmneziaWG Legacy is a outdated version of AmneziaWG protocol. To upgrade, install AmneziaWG and recreate users. AmneziaWG Legacy является устаревшей версией протокола AmneziaWG. Для обновления установите AmneziaWG и пересоздайте пользователей. @@ -5271,47 +5263,47 @@ FileZilla или другие SFTP-клиенты, а также смонтир 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 Экспорт завершен @@ -5352,7 +5344,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир TextFieldWithHeaderType - + The field can't be empty Поле не может быть пустым @@ -5360,7 +5352,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир VpnConnection - + Mbps Мбит/с @@ -5411,12 +5403,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир 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. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях. @@ -5424,12 +5416,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир main2 - + Private key passphrase Парольная фраза для закрытого ключа - + Save Сохранить diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index c1f7a8b48..57d0223d9 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -384,6 +384,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 476626f3e..780673763 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(); } diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 2a1332e20..91d7ec3b5 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 24c7c220b..8e1e198f2 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); @@ -987,79 +987,94 @@ void InstallController::addEmptyServer() emit installServerFinished(tr("Server added successfully")); } -bool InstallController::isConfigValid() +void InstallController::validateConfig() { int serverIndex = m_serversModel->getDefaultServerIndex(); QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex); if (apiUtils::isServerFromApi(serverConfigObject)) { - return true; + emit configValidated(true); + return; } if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit noInstalledContainers(); - return false; + emit configValidated(false); + return; } DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); if (container == DockerContainer::None) { emit installationErrorOccurred(ErrorCode::NoInstalledContainersError); - return false; + emit configValidated(false); + return; } - QSharedPointer serverController(new ServerController(m_settings)); - VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - QJsonObject containerConfig = m_containersModel->getContainerConfig(container); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + QSharedPointer serverController(new ServerController(m_settings)); - QFutureWatcher watcher; + auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) { + for (Proto protocol : ContainerProps::protocolsForContainer(container)) { + QString protocolConfig = + containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString(); - QFuture future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() { - ErrorCode errorCode = ErrorCode::NoError; - - auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) { - for (Proto protocol : ContainerProps::protocolsForContainer(container)) { - QString protocolConfig = - containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString(); - - if (protocolConfig.isEmpty()) { - return false; - } - } - return true; - }; - - if (!isProtocolConfigExists(containerConfig, container)) { - VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - m_serversModel->updateContainerConfig(container, containerConfig); - - errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, - QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController); - if (errorCode != ErrorCode::NoError) { - return errorCode; + if (protocolConfig.isEmpty()) { + return false; } } - return errorCode; - }); + return true; + }; - QEventLoop wait; - connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); - watcher.setFuture(future); - wait.exec(); - - ErrorCode errorCode = watcher.result(); - - if (errorCode != ErrorCode::NoError) { - emit installationErrorOccurred(errorCode); - return false; + if (isProtocolConfigExists(containerConfig, container)) { + emit configValidated(true); + return; } - return true; + + struct ValidationResult { + ErrorCode errorCode = ErrorCode::NoError; + QJsonObject containerConfig; + }; + + QFuture future = + QtConcurrent::run([settings = m_settings, serverController, credentials, containerConfig, container]() mutable { + ValidationResult result; + result.containerConfig = containerConfig; + + VpnConfigurationsController vpnConfigurationController(settings, serverController); + result.errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, + result.containerConfig); + return result; + }); + + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, + [this, watcher, container, credentials, serverController]() { + auto result = watcher->result(); + watcher->deleteLater(); + + if (result.errorCode != ErrorCode::NoError) { + emit installationErrorOccurred(result.errorCode); + emit configValidated(false); + return; + } + + m_serversModel->updateContainerConfig(container, result.containerConfig); + + ErrorCode appendError = m_clientManagementModel->appendClient( + container, credentials, result.containerConfig, + QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController); + + if (appendError != ErrorCode::NoError) { + emit installationErrorOccurred(appendError); + emit configValidated(false); + return; + } + + emit configValidated(true); + }); + watcher->setFuture(future); } bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig, @@ -1070,7 +1085,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/installController.h b/client/ui/controllers/installController.h index d18ba946d..034aa8494 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -50,9 +50,10 @@ public slots: void addEmptyServer(); - bool isConfigValid(); + void validateConfig(); signals: + void configValidated(bool isValid); void installContainerFinished(const QString &finishMessage, bool isServiceInstall); void installServerFinished(const QString &finishMessage); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index ecc363d35..722175baf 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -180,12 +180,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 08c74a934..985ed5673 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 8cfe3b397..fbbe383cd 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 d571346b7..5ed9cca16 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 ff535490f..40de5bdde 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 000000000..069e15490 --- /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 914fc2670..769c3e046 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 c88f9e7c8..bfa31c63f 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/PageType.qml b/client/ui/qml/Controls2/PageType.qml index d7f3317f3..b8f3698e1 100644 --- a/client/ui/qml/Controls2/PageType.qml +++ b/client/ui/qml/Controls2/PageType.qml @@ -8,9 +8,10 @@ Item { id: root property StackView stackView: StackView.view + property bool enableTimer: true onVisibleChanged: { - if (visible) { + if (visible && enableTimer) { timer.start() } } @@ -24,6 +25,6 @@ Item { FocusController.setFocusOnDefaultItem() } repeat: false // Stop the timer after one trigger - running: true // Start the timer + running: enableTimer // Start the timer } } diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index b7de64c63..897584303 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 c44b374ca..bd0e5a450 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 050aeeeb6..532ab6a10 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 4445b08bf..f3c63eb59 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 0ad835e05..4e6b6c504 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 27aa5dea6..ed0923367 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 16ac2f204..c5e581af8 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/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index 82ff3b7c1..adacd6c08 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -13,6 +13,7 @@ import "../Components" PageType { id: root + enableTimer: (SettingsController.isOnTv()) ? false : true ColumnLayout { id: content @@ -45,4 +46,22 @@ PageType { } } } + + Timer { + interval: 250 + running: SettingsController.isOnTv() + repeat: true + onTriggered: { + startButton.forceActiveFocus() + if (startButton.activeFocus) { + running = false + } + } + } + + onVisibleChanged: { + if (visible && SettingsController.isOnTv()) { + startButton.forceActiveFocus() + } + } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 6d32828c6..5e378d794 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -515,7 +515,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 e32381ee8..e731704df 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 a137559ad..a95044d91 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -21,10 +21,14 @@ Window { function onStateChanged() { if (Qt.platform.os === "android") { if (Qt.application.state === Qt.ApplicationActive) { + root.visible = true refreshTimer.restart() - } else if (Qt.application.state === Qt.ApplicationSuspended || - Qt.application.state === Qt.ApplicationInactive) { - console.log("QML: Application going to background, state:", Qt.application.state) + } else if (Qt.application.state === Qt.ApplicationSuspended) { + // Hide window to stop the Qt render loop and prevent + // eglSwapBuffers from being called on a lost EGL context. + // NOTE: Do NOT hide on ApplicationInactive — that fires on any + // focus change (IME, notifications) and would blank the screen. + root.visible = false } } } @@ -56,6 +60,11 @@ Window { PageController.closeWindow() } + onSceneGraphError: function(error, message) { + // Prevent qFatal crash on Android when EGL context is lost + console.warn("Scene graph error:", error, message) + } + title: "AmneziaVPN" Item { // This item is needed for focus handling @@ -83,6 +92,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 afe478405..3997ed02c 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 070ab36fa..777573f26 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 5d56dcb59..e2b2dc04d 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=AmneziaVPN set APP_FILENAME=%APP_NAME:"=%.exe +set SERVICE_FILENAME=%APP_NAME:"=%-service.exe set APP_DOMAIN=org.amneziavpn.package set OUT_APP_DIR=%WORK_DIR:"=%\client\release set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=% @@ -43,6 +44,7 @@ set STAGE_DIR=%WORK_DIR:"=%\stage echo "Environment:" echo "WORK_DIR: %WORK_DIR%" echo "APP_FILENAME: %APP_FILENAME%" +echo "SERVICE_FILENAME: %SERVICE_FILENAME%" echo "PROJECT_DIR: %PROJECT_DIR%" echo "SCRIPT_DIR: %SCRIPT_DIR%" echo "OUT_APP_DIR: %OUT_APP_DIR%" @@ -74,7 +76,7 @@ if %errorlevel% neq 0 exit /b %errorlevel% echo "Deploying..." mkdir "%OUT_APP_DIR%" -copy "%WORK_DIR%\service\server\release\%APP_NAME%-service.exe" "%OUT_APP_DIR%" +copy "%WORK_DIR%\service\server\release\%SERVICE_FILENAME%" "%OUT_APP_DIR%" rem copy "%WORK_DIR%\client\%APP_FILENAME%" "%OUT_APP_DIR%" copy /Y "%PROJECT_DIR%\client\images\app.ico" "%OUT_APP_DIR%\AmneziaVPN.ico" >nul @@ -83,7 +85,8 @@ echo "Signing exe" cd %OUT_APP_DIR% signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe -"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%" +"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations --force-openssl "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%" +"%QT_BIN_DIR:"=%\windeployqt" --release "%OUT_APP_DIR:"=%\%SERVICE_FILENAME:"=%" signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll diff --git a/deploy/deploy_s3.sh b/deploy/deploy_s3.sh index 893a78ef0..193d26079 100755 --- a/deploy/deploy_s3.sh +++ b/deploy/deploy_s3.sh @@ -14,6 +14,7 @@ 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...' diff --git a/ipc/ipc.h b/ipc/ipc.h index b13296577..8fca44369 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 2320c6a4a..f26bd8b35 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 6b3bb6547..7bf2ed22e 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 e355035e0..000000000 --- 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 77f3a3519..4d02c1dd1 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 5a63302cb..e8607c5ad 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 497e89d79..4890e6884 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 b427d639d..722b04f0d 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 65d801b03..000000000 --- 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 8ce9be1ac..000000000 --- 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 3330e9c56..63a3ec137 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,7 +6,7 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat Concurrent) qt_standard_project_setup() @@ -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 6f77d8b39..a0e5c7e8a 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 0a24d9dbd..26706079a 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 47a7640d6..3a8859044 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 24e82c0e5..8849d27b7 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 7ad6505df..6cb2e38ee 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 c2412cc55..9258b7341 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 2d3f9a797..b0408d49a 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 c199734aa..f54d9902f 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) {