diff --git a/.github/workflows/tag-upload.yml b/.github/workflows/tag-upload.yml index bc88933d..11ad7524 100644 --- a/.github/workflows/tag-upload.yml +++ b/.github/workflows/tag-upload.yml @@ -24,7 +24,7 @@ jobs: - name: Verify git tag run: | TAG_NAME=${{ inputs.RELEASE_VERSION }} - CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/') + CMAKE_TAG=$(grep 'set(DEFAULTVPN_VERSION' CMakeLists.txt | sed -E 's/.*DEFAULTVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/') if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)." else diff --git a/.gitignore b/.gitignore index 50d648be..02a4ae08 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,6 @@ ios-ne-build.sh macos-ne-build.sh macos-signed-build.sh macos-with-sign-build.sh +DeveloperIdApplicationCertificate.p12 +DeveloperIdInstallerCertificate.p12 + diff --git a/.gitmodules b/.gitmodules index 90edb582..11845060 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ [submodule "client/3rd/QSimpleCrypto"] path = client/3rd/QSimpleCrypto url = https://github.com/amnezia-vpn/QSimpleCrypto.git +[submodule "client/3rd/qtgamepad"] + path = client/3rd/qtgamepad + url = https://github.com/amnezia-vpn/qtgamepad.git + branch = 6.6 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb8c9e0..203e205d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT DefaultVPN) -set(DEFAULTVPN_VERSION 1.0.6.2) +set(DEFAULTVPN_VERSION 1.0.6.3) project(${PROJECT} VERSION ${DEFAULTVPN_VERSION} DESCRIPTION "DefaultVPN" @@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2105) +set(APP_ANDROID_VERSION_CODE 2108) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") @@ -57,13 +57,14 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE) set(CPACK_GENERATOR "WIX") set(CPACK_WIX_VERSION 4) - set(CPACK_PACKAGE_NAME "AmneziaVPN") - set(CPACK_PACKAGE_VENDOR "AmneziaVPN") - set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION}) - set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client") - set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN") + set(CPACK_PACKAGE_NAME "DefaultVPN") + set(CPACK_PACKAGE_VENDOR "DefaultVPN") + set(CPACK_PACKAGE_VERSION ${DEFAULTVPN_VERSION}) + set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "DefaultVPN client") + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") + set(CPACK_PACKAGE_INSTALL_DIRECTORY "DefaultVPN") set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}") - set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN") + set(CPACK_PACKAGE_EXECUTABLES "DefaultVPN" "DefaultVPN") set(CPACK_WIX_UPGRADE_GUID "{2D55AC62-96D6-4692-8C05-0D85BBF95485}") set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/client/images/app.ico") diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 579673b2..b8c22928 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit 579673b2ed044fbe064e3680775dd7773db71386 +Subproject commit b8c229288da2beb3edce7e3b2f4f231213052287 diff --git a/client/3rd/qtgamepad b/client/3rd/qtgamepad new file mode 160000 index 00000000..f72b3e0c --- /dev/null +++ b/client/3rd/qtgamepad @@ -0,0 +1 @@ +Subproject commit f72b3e0c6229d1622b6bc7f6a5ec5ccff83460eb diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0d9ced79..f3cd7de4 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) @@ -228,4 +227,13 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE) endif() target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC}) -qt_finalize_target(${PROJECT}) + +# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this). +if(COMMAND qt_import_qml_plugins) + qt_import_qml_plugins(${PROJECT}) +endif() +if(COMMAND qt_finalize_executable) + qt_finalize_executable(${PROJECT}) +else() + qt_finalize_target(${PROJECT}) +endif() diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 66600dde..ef4763c5 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 @@ -88,6 +90,10 @@ 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 val vpnServiceEventHandler: Handler by lazy(NONE) { object : Handler(Looper.getMainLooper()) { @@ -260,6 +266,10 @@ class AmneziaActivity : QtActivity() { } override fun onStop() { + isActivityResumed = false + hasWindowFocus = false + // Cancel all pending operations when activity stops + resumeHandler.removeCallbacksAndMessages(null) Log.d(TAG, "Stop Amnezia activity") doUnbindService() mainScope.launch { @@ -271,35 +281,91 @@ class AmneziaActivity : QtActivity() { override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) + hasWindowFocus = hasFocus Log.d(TAG, "Window focus changed: hasFocus=$hasFocus") + + // Cancel pending operations if window loses focus + if (!hasFocus) { + resumeHandler.removeCallbacksAndMessages(null) + } } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val deviceId = event.deviceId + val keyCode = event.keyCode + val pressed = event.action == KeyEvent.ACTION_DOWN + val source = event.source + + if (deviceId < 0 && pressed) { + when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_DPAD_CENTER -> { + nativeGamepadKeyEvent(0, keyCode, true) + nativeGamepadKeyEvent(0, keyCode, false) + return true + } + } + } + + // Real gamepad events (deviceId >= 0) + if (deviceId >= 0) { + val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD + val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK + val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD + if (isGamepad || isJoystick || isDpad) { + nativeGamepadKeyEvent(deviceId, keyCode, pressed) + return true + } + } + + return super.dispatchKeyEvent(event) + } + + private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean) + override fun onPause() { super.onPause() + isActivityResumed = false + // Cancel all pending operations when activity pauses + resumeHandler.removeCallbacksAndMessages(null) Log.d(TAG, "Pause Amnezia activity") } override fun onResume() { super.onResume() - /* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + isActivityResumed = true + Log.d(TAG, "Resume Amnezia activity") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { window.decorView.apply { invalidate() - postDelayed({ - sendTouch(1f, 1f) + resumeHandler.postDelayed({ + // Check if activity is still resumed and has focus before executing + if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { + sendTouch(1f, 1f) + } }, 100) - postDelayed({ - sendTouch(2f, 2f) + resumeHandler.postDelayed({ + if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { + sendTouch(2f, 2f) + } }, 200) - postDelayed({ - requestLayout() - invalidate() + resumeHandler.postDelayed({ + if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { + requestLayout() + invalidate() + } }, 250) } - } */ - Log.d(TAG, "Resume Amnezia activity") + } } private fun configureWindowForEdgeToEdge() { @@ -362,6 +428,10 @@ class AmneziaActivity : QtActivity() { } override fun onDestroy() { + isActivityResumed = false + hasWindowFocus = false + // Cancel all pending operations when activity is destroyed + resumeHandler.removeCallbacksAndMessages(null) Log.d(TAG, "Destroy Amnezia activity") unregisterBroadcastReceiver(notificationStateReceiver) notificationStateReceiver = null diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 4272290d..2c3145d7 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -83,6 +83,26 @@ add_compile_definitions(_WINSOCKAPI_) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_WITH_QT6 ON) add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain) + +if(ANDROID) + # Use qtgamepad from amnezia-vpn/qtgamepad repository + # Only if Qt6CorePrivate is available (required by qtgamepad) + find_package(Qt6CorePrivate CONFIG QUIET) + if(Qt6CorePrivate_FOUND) + add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad) + # Link both the C++ module and QML plugin + if(TARGET GamepadLegacy) + target_link_libraries(${PROJECT} PRIVATE GamepadLegacy) + endif() + if(TARGET GamepadLegacyQuickPrivate) + target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate) + endif() + message(STATUS "Gamepad support enabled for Android") + else() + message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.") + endif() +endif() + set(LIBS ${LIBS} qt6keychain) include_directories( diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 8fe198ce..a2fa9a9b 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -181,7 +181,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/ipcclient.h - ${CLIENT_ROOT_DIR}/core/privileged_process.h ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h ${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h @@ -194,7 +193,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/ipcclient.cpp - ${CLIENT_ROOT_DIR}/core/privileged_process.cpp ${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 132af459..25a40c46 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -337,6 +337,9 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS } else { baseUrls = QString(PROD_S3_ENDPOINT).split(", "); } + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(baseUrls.begin(), baseUrls.end(), generator); QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index 4586b352..d5ee3d7d 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -7,7 +7,6 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent) { m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl())); m_interface.reset(m_node.acquire()); - m_tun2socks.reset(m_node.acquire()); } IpcClient& IpcClient::Instance() @@ -33,68 +32,43 @@ QSharedPointer IpcClient::Interface() return rep; } -QSharedPointer IpcClient::InterfaceTun2Socks() +QSharedPointer IpcClient::CreatePrivilegedProcess() { - QSharedPointer rep = Instance().m_tun2socks; - if (rep.isNull()) { - qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined"; - return nullptr; - } - if (!rep->waitForSource(1000)) { - qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica"; - return nullptr; - } - if (!rep->isReplicaValid()) { - qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid"; - } - return rep; -} - -QSharedPointer IpcClient::CreatePrivilegedProcess() -{ - QSharedPointer rep = Interface(); - if (!rep) { - qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid"; - return nullptr; - } - - QRemoteObjectPendingReply pidReply = rep->createPrivilegedProcess(); - if (!pidReply.waitForFinished(5000)){ - qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call"; - return nullptr; - } - - int pid = pidReply.returnValue(); - QSharedPointer pd(new ProcessDescriptor()); - - pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data())); - - connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() { - pd->replicaNode->addClientSideConnection(pd->localSocket.data()); - - IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire(); - // TODO: rework the unsafe cast below - PrivilegedProcess *priv = static_cast(repl); - pd->ipcProcess.reset(priv); - if (!pd->ipcProcess) { - qWarning() << "Acquire PrivilegedProcess failed"; - } else { - pd->ipcProcess->waitForSource(1000); - if (!pd->ipcProcess->isReplicaValid()) { - qWarning() << "PrivilegedProcess replica is not connected!"; - } - - QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(), - [pd]() { pd->replicaNode->deleteLater(); }); + return withInterface([](QSharedPointer &iface) -> QSharedPointer { + auto createPrivilegedProcess = iface->createPrivilegedProcess(); + if (!createPrivilegedProcess.waitForFinished()) { + qCritical() << "Failed to create privileged process"; + return nullptr; } - }); - pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid)); - if (!pd->localSocket->waitForConnected()) { - qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket"; + const int pid = createPrivilegedProcess.returnValue(); + + auto* node = new QRemoteObjectNode(); + node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid)))); + + QSharedPointer rep( + node->acquire(), + [node] (IpcProcessInterfaceReplica *ptr) { + delete ptr; + node->deleteLater(); + } + ); + if (rep.isNull()) { + qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica"; + return nullptr; + } + if (!rep->waitForSource()) { + qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica"; + return nullptr; + } + if (!rep->isReplicaValid()) { + qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid"; + return nullptr; + } + + return rep; + }, + []() -> QSharedPointer { return nullptr; - } - - auto processReplica = QSharedPointer(pd->ipcProcess); - return processReplica; + }); } diff --git a/client/core/ipcclient.h b/client/core/ipcclient.h index 74f1bf29..ff691919 100644 --- a/client/core/ipcclient.h +++ b/client/core/ipcclient.h @@ -5,9 +5,7 @@ #include #include "rep_ipc_interface_replica.h" -#include "rep_ipc_process_tun2socks_replica.h" - -#include "privileged_process.h" +#include "rep_ipc_process_interface_replica.h" class IpcClient : public QObject { @@ -18,8 +16,7 @@ public: static IpcClient& Instance(); static QSharedPointer Interface(); - static QSharedPointer InterfaceTun2Socks(); - static QSharedPointer CreatePrivilegedProcess(); + static QSharedPointer CreatePrivilegedProcess(); template static auto withInterface(Func func) @@ -54,18 +51,6 @@ signals: private: QRemoteObjectNode m_node; QSharedPointer m_interface; - QSharedPointer m_tun2socks; - - struct ProcessDescriptor { - ProcessDescriptor () { - replicaNode = QSharedPointer(new QRemoteObjectNode()); - ipcProcess = QSharedPointer(); - localSocket = QSharedPointer(); - } - QSharedPointer ipcProcess; - QSharedPointer replicaNode; - QSharedPointer localSocket; - }; }; #endif // IPCCLIENT_H diff --git a/client/core/privileged_process.cpp b/client/core/privileged_process.cpp deleted file mode 100644 index 3852236f..00000000 --- a/client/core/privileged_process.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "privileged_process.h" - -PrivilegedProcess::PrivilegedProcess() : - IpcProcessInterfaceReplica() -{ -} - -PrivilegedProcess::~PrivilegedProcess() -{ - qDebug() << "PrivilegedProcess::~PrivilegedProcess()"; -} - -void PrivilegedProcess::waitForFinished(int msecs) -{ - QSharedPointer loop(new QEventLoop); - connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{ - loop->quit(); - loop.clear(); - }); - - QTimer::singleShot(msecs, this, [this, loop]() mutable { - loop->quit(); - loop.clear(); - }); - - loop->exec(); -} diff --git a/client/core/privileged_process.h b/client/core/privileged_process.h deleted file mode 100644 index 4d08c043..00000000 --- a/client/core/privileged_process.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef PRIVILEGED_PROCESS_H -#define PRIVILEGED_PROCESS_H - -#include - -#include "rep_ipc_process_interface_replica.h" -// This class is dangerous - instance of this class casted from base class, -// so it support only functions -// Do not add any members into it -// -class PrivilegedProcess : public IpcProcessInterfaceReplica -{ - Q_OBJECT -public: - PrivilegedProcess(); - ~PrivilegedProcess() override; - - void waitForFinished(int msecs); - -}; - -#endif // PRIVILEGED_PROCESS_H - - diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 2d902f1a..c90ac65b 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -270,12 +270,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { && !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined() && !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined() - && !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined() - && !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined() - && !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined() - && !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined() - && !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined() - && !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()) { + && !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) { json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount)); json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize)); json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize)); diff --git a/client/mozilla/networkwatcher.cpp b/client/mozilla/networkwatcher.cpp index c613c106..e6e25b5e 100644 --- a/client/mozilla/networkwatcher.cpp +++ b/client/mozilla/networkwatcher.cpp @@ -72,9 +72,9 @@ void NetworkWatcher::initialize() { connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this, &NetworkWatcher::unsecuredNetwork); connect(m_impl, &NetworkWatcherImpl::networkChanged, this, - &NetworkWatcher::networkChange); - connect(m_impl, &NetworkWatcherImpl::sleepMode, this, - &NetworkWatcher::onSleepMode); + &NetworkWatcher::networkChanged); + connect(m_impl, &NetworkWatcherImpl::wakeup, this, + &NetworkWatcher::wakeup); m_impl->initialize(); // Enable sleep/wake monitoring for VPN auto-reconnection @@ -97,12 +97,6 @@ void NetworkWatcher::settingsChanged() { logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active"; } -void NetworkWatcher::onSleepMode() -{ - logger.debug() << "Resumed from sleep mode"; - emit sleepMode(); -} - void NetworkWatcher::unsecuredNetwork(const QString& networkName, const QString& networkId) { logger.debug() << "Unsecured network:" << logger.sensitive(networkName) diff --git a/client/mozilla/networkwatcher.h b/client/mozilla/networkwatcher.h index f28e74fb..527e28e2 100644 --- a/client/mozilla/networkwatcher.h +++ b/client/mozilla/networkwatcher.h @@ -29,13 +29,11 @@ public: // false to restore. void simulateDisconnection(bool simulatedDisconnection); - void onSleepMode(); - QNetworkInformation::Reachability getReachability(); signals: - void networkChange(); - void sleepMode(); + void networkChanged(); + void wakeup(); private: void settingsChanged(); diff --git a/client/mozilla/networkwatcherimpl.h b/client/mozilla/networkwatcherimpl.h index 54bd8e87..a05c0887 100644 --- a/client/mozilla/networkwatcherimpl.h +++ b/client/mozilla/networkwatcherimpl.h @@ -41,7 +41,7 @@ signals: // TODO: Only windows-networkwatcher has this, the other plattforms should // too. void networkChanged(QString newBSSID); - void sleepMode(); + void wakeup(); private: diff --git a/client/platforms/linux/linuxnetworkwatcher.cpp b/client/platforms/linux/linuxnetworkwatcher.cpp index 22eba3c7..11b31fa5 100644 --- a/client/platforms/linux/linuxnetworkwatcher.cpp +++ b/client/platforms/linux/linuxnetworkwatcher.cpp @@ -41,8 +41,8 @@ void LinuxNetworkWatcher::initialize() { connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this, &LinuxNetworkWatcher::unsecuredNetwork); - connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this, - &NetworkWatcherImpl::sleepMode); + connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this, + &NetworkWatcherImpl::wakeup); // Let's wait a few seconds to allow the UI to be fully loaded and shown. // This is not strictly needed, but it's better for user experience because diff --git a/client/platforms/linux/linuxnetworkwatcherworker.cpp b/client/platforms/linux/linuxnetworkwatcherworker.cpp index 247a1d67..f7cce6bb 100644 --- a/client/platforms/linux/linuxnetworkwatcherworker.cpp +++ b/client/platforms/linux/linuxnetworkwatcherworker.cpp @@ -200,7 +200,7 @@ void LinuxNetworkWatcherWorker::checkDevices() { void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state) { if (state == NM_STATE_ASLEEP) { - emit sleepMode(); + emit wakeup(); } logger.debug() << "NMStateChanged " << state; diff --git a/client/platforms/linux/linuxnetworkwatcherworker.h b/client/platforms/linux/linuxnetworkwatcherworker.h index 9579fcef..7c5ae540 100644 --- a/client/platforms/linux/linuxnetworkwatcherworker.h +++ b/client/platforms/linux/linuxnetworkwatcherworker.h @@ -23,7 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject { signals: void unsecuredNetwork(const QString& networkName, const QString& networkId); - void sleepMode(); + void wakeup(); public slots: void initialize(); diff --git a/client/platforms/macos/macosnetworkwatcher.mm b/client/platforms/macos/macosnetworkwatcher.mm index 67f3a930..c2664566 100644 --- a/client/platforms/macos/macosnetworkwatcher.mm +++ b/client/platforms/macos/macosnetworkwatcher.mm @@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_ case kIOMessageSystemHasPoweredOn: /* Announces that the system and its devices have woken up. */ - logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread"; + logger.debug() << "System has powered on - emitting wakeup signal from dedicated CFRunLoop thread"; if (listener->m_watcher) { // Use QMetaObject::invokeMethod for thread-safe signal emission - QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection); + QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection); } break; diff --git a/client/platforms/windows/daemon/windowsdaemon.cpp b/client/platforms/windows/daemon/windowsdaemon.cpp index 8668b4db..a0b5e18c 100644 --- a/client/platforms/windows/daemon/windowsdaemon.cpp +++ b/client/platforms/windows/daemon/windowsdaemon.cpp @@ -62,6 +62,9 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda } void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) { + if (m_splitTunnelManager == nullptr) + return; + if (config.m_vpnDisabledApps.length() > 0) { m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex); m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps); diff --git a/client/platforms/windows/windowsnetworkwatcher.cpp b/client/platforms/windows/windowsnetworkwatcher.cpp index f72baa6e..85eb4276 100644 --- a/client/platforms/windows/windowsnetworkwatcher.cpp +++ b/client/platforms/windows/windowsnetworkwatcher.cpp @@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM switch (uMsg) { case WM_POWERBROADCAST: if (wParam == PBT_APMRESUMESUSPEND) { - emit obj->sleepMode(); + emit obj->wakeup(); } break; default: diff --git a/client/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index b518aac4..20a3416c 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -232,12 +232,6 @@ ErrorCode OpenVpnProtocol::start() return ErrorCode::AmneziaServiceConnectionFailed; } - m_openVpnProcess->waitForSource(5000); - if (!m_openVpnProcess->isInitialized()) { - qWarning() << "IpcProcess replica is not connected!"; - setLastError(ErrorCode::AmneziaServiceConnectionFailed); - return ErrorCode::AmneziaServiceConnectionFailed; - } m_openVpnProcess->setProgram(PermittedProcess::OpenVPN); QStringList arguments({ "--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort), @@ -246,13 +240,13 @@ ErrorCode OpenVpnProtocol::start() m_openVpnProcess->setArguments(arguments); qDebug() << arguments.join(" "); - connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred, + connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred, [&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; }); - connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged, + connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged, [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); - connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this, + connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this, [&]() { setConnectionState(Vpn::ConnectionState::Disconnected); }); m_openVpnProcess->start(); diff --git a/client/protocols/openvpnprotocol.h b/client/protocols/openvpnprotocol.h index b07d1268..490fff83 100644 --- a/client/protocols/openvpnprotocol.h +++ b/client/protocols/openvpnprotocol.h @@ -53,7 +53,7 @@ private: void updateRouteGateway(QString line); void updateVpnGateway(const QString &line); - QSharedPointer m_openVpnProcess; + QSharedPointer m_openVpnProcess; }; #endif // OPENVPNPROTOCOL_H diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index 869afd92..c1cd868d 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -233,7 +233,7 @@ namespace amnezia constexpr char defaultResponsePacketMagicHeader[] = "3288052141"; constexpr char defaultTransportPacketMagicHeader[] = "2528465083"; constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858"; - constexpr char defaultSpecialJunk1[] = ""; + constexpr char defaultSpecialJunk1[] = ""; constexpr char defaultSpecialJunk2[] = ""; constexpr char defaultSpecialJunk3[] = ""; constexpr char defaultSpecialJunk4[] = ""; diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 1125731d..2ae7ebcb 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -15,7 +15,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * m_impl.reset(new LocalSocketController()); connect(m_impl.get(), &ControllerImpl::connected, this, [this](const QString &pubkey, const QDateTime &connectionTimestamp) { - emit connectionStateChanged(Vpn::ConnectionState::Connected); + setConnectionState(Vpn::ConnectionState::Connected); }); connect(m_impl.get(), &ControllerImpl::statusUpdated, this, [this](const QString& serverIpv4Gateway, @@ -38,7 +38,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * }); connect(m_impl.get(), &ControllerImpl::disconnected, this, - [this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); }); + [this]() { setConnectionState(Vpn::ConnectionState::Disconnected); }); m_impl->initialize(nullptr, nullptr); } diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 575960b2..50bf829a 100755 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -1,6 +1,7 @@ #include "xrayprotocol.h" #include "core/ipcclient.h" +#include "ipc.h" #include "utilities.h" #include "core/networkUtilities.h" @@ -9,14 +10,37 @@ #include #include #include +#include +#include +#include + +#ifdef Q_OS_MACOS +static const QString tunName = "utun22"; +#else +static const QString tunName = "tun2"; +#endif XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent) { - readXrayConfiguration(configuration); - m_routeGateway = NetworkUtilities::getGatewayAndIface().first; m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr; m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr; - m_t2sProcess = IpcClient::InterfaceTun2Socks(); + m_routeGateway = NetworkUtilities::getGatewayAndIface().first; + + m_routeMode = static_cast(configuration.value(amnezia::config_key::splitTunnelType).toInt()); + m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::config_key::hostName).toString()); + + const QString primaryDns = configuration.value(amnezia::config_key::dns1).toString(); + m_dnsServers.push_back(QHostAddress(primaryDns)); + if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) { + const QString secondaryDns = configuration.value(amnezia::config_key::dns2).toString(); + m_dnsServers.push_back(QHostAddress(secondaryDns)); + } + + QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject(); + if (xrayConfiguration.isEmpty()) { + xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject(); + } + m_xrayConfig = xrayConfiguration; } XrayProtocol::~XrayProtocol() @@ -29,72 +53,16 @@ ErrorCode XrayProtocol::start() { qDebug() << "XrayProtocol::start()"; - const ErrorCode err = IpcClient::withInterface([&](QSharedPointer iface) { - iface->xrayStart(QJsonDocument(m_xrayConfig).toJson()); - return ErrorCode::NoError; + return IpcClient::withInterface([&](QSharedPointer iface) { + auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson()); + if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) { + qCritical() << "Failed to start xray"; + return ErrorCode::XrayExecutableCrashed; + } + return startTun2Socks(); }, [] () { return ErrorCode::AmneziaServiceConnectionFailed; }); - if (err != ErrorCode::NoError) - return err; - - setConnectionState(Vpn::ConnectionState::Connecting); - return startTun2Sock(); -} - -ErrorCode XrayProtocol::startTun2Sock() -{ - m_t2sProcess->start(); - - connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this, - [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); - - connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) { - qDebug() << "PrivilegedProcess setConnectionState " << vpnState; - IpcClient::withInterface([&](QSharedPointer iface) { - if (vpnState == Vpn::ConnectionState::Connected) { - setConnectionState(Vpn::ConnectionState::Connecting); - QList dnsAddr; - - dnsAddr.push_back(QHostAddress(m_primaryDNS)); - // We don't use secondary DNS if primary DNS is AmneziaDNS - if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) { - dnsAddr.push_back(QHostAddress(m_secondaryDNS)); - } - #ifdef Q_OS_WIN - QThread::msleep(8000); - #endif - #ifdef Q_OS_MACOS - QThread::msleep(5000); - iface->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr); - iface->updateResolvers("utun22", dnsAddr); - #endif - #ifdef Q_OS_LINUX - QThread::msleep(1000); - iface->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); - iface->updateResolvers("tun2", dnsAddr); - #endif - if (m_routeMode == Settings::RouteMode::VpnAllSites) { - iface->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1"); - } - iface->StopRoutingIpv6(); - #ifdef Q_OS_WIN - iface->updateResolvers("tun2", dnsAddr); - #endif - setConnectionState(Vpn::ConnectionState::Connected); - } - #if !defined(Q_OS_MACOS) - if (vpnState == Vpn::ConnectionState::Disconnected) { - setConnectionState(Vpn::ConnectionState::Disconnected); - iface->deleteTun("tun2"); - iface->StartRoutingIpv6(); - iface->clearSavedRoutes(); - } -#endif - }); - }); - - return ErrorCode::NoError; } void XrayProtocol::stop() @@ -102,43 +70,177 @@ void XrayProtocol::stop() qDebug() << "XrayProtocol::stop()"; IpcClient::withInterface([](QSharedPointer iface) { -#ifdef AMNEZIA_DESKTOP - QRemoteObjectPendingReply StartRoutingIpv6Resp = iface->StartRoutingIpv6(); - if (!StartRoutingIpv6Resp.waitForFinished(1000)) { - qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6"; - } + auto disableKillSwitch = iface->disableKillSwitch(); + if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue()) + qWarning() << "Failed to disable killswitch"; - QRemoteObjectPendingReply restoreResolvers = iface->restoreResolvers(); - if (!restoreResolvers.waitForFinished(1000)) { - qWarning() << "XrayProtocol::stop(): Failed to restore resolvers"; - } + auto StartRoutingIpv6 = iface->StartRoutingIpv6(); + if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue()) + qWarning() << "Failed to start routing ipv6"; - #if !defined(Q_OS_MACOS) - QRemoteObjectPendingReply deleteTunResp = iface->deleteTun("tun2"); - if (!deleteTunResp.waitForFinished(1000)) { - qWarning() << "XrayProtocol::stop(): Failed to delete tun"; - } - #endif -#endif - iface->xrayStop(); + auto restoreResolvers = iface->restoreResolvers(); + if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue()) + qWarning() << "Failed to restore resolvers"; + + auto deleteTun = iface->deleteTun(tunName); + if (!deleteTun.waitForFinished() || !deleteTun.returnValue()) + qWarning() << "Failed to delete tun"; + + auto xrayStop = iface->xrayStop(); + if (!xrayStop.waitForFinished() || !xrayStop.returnValue()) + qWarning() << "Failed to stop xray"; }); - if (m_t2sProcess) { - m_t2sProcess->stop(); - QThread::msleep(200); + if (m_tun2socksProcess) { + m_tun2socksProcess->blockSignals(true); + +#ifndef Q_OS_WIN + m_tun2socksProcess->terminate(); + auto waitForFinished = m_tun2socksProcess->waitForFinished(1000); + if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) { + qWarning() << "Failed to terminate tun2socks. Killing the process..."; + m_tun2socksProcess->kill(); + } +#else + // terminate does not do anything useful on Windows + // so just kill the process + m_tun2socksProcess->kill(); +#endif + + m_tun2socksProcess->close(); + m_tun2socksProcess.reset(); } setConnectionState(Vpn::ConnectionState::Disconnected); } -void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration) +ErrorCode XrayProtocol::startTun2Socks() { - QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject(); - if (xrayConfiguration.isEmpty()) { - xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject(); + m_tun2socksProcess = IpcClient::CreatePrivilegedProcess(); + if (!m_tun2socksProcess->waitForSource()) { + return ErrorCode::AmneziaServiceConnectionFailed; } - m_xrayConfig = xrayConfiguration; - m_routeMode = static_cast(configuration.value(amnezia::config_key::splitTunnelType).toInt()); - m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString(); - m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString(); + + m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks); + m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" }); + + connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() { + auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput(); + if (!readAllStandardOutput.waitForFinished()) { + qWarning() << "Failed to read output from tun2socks"; + return; + } + + const QString line = readAllStandardOutput.returnValue(); + + if (!line.contains("[TCP]") && !line.contains("[UDP]")) + qDebug() << "[tun2socks]:" << line; + + if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) { + disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr); + + if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) { + stop(); + setLastError(res); + } else { + setConnectionState(Vpn::ConnectionState::Connected); + } + } + }, Qt::QueuedConnection); + + connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + if (exitStatus == QProcess::ExitStatus::CrashExit) { + qCritical() << "Tun2socks process crashed!"; + } else { + qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode); + } + stop(); + setLastError(ErrorCode::Tun2SockExecutableCrashed); + }, Qt::QueuedConnection); + + m_tun2socksProcess->start(); + return ErrorCode::NoError; +} + +ErrorCode XrayProtocol::setupRouting() { + return IpcClient::withInterface([this](QSharedPointer iface) -> ErrorCode { +#ifdef Q_OS_WIN + const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress)); +#endif + auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr); + if (!createTun.waitForFinished() || !createTun.returnValue()) { + qCritical() << "Failed to assign IP address for TUN"; + return ErrorCode::InternalError; + } + + auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers); + if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) { + qCritical() << "Failed to set DNS resolvers for TUN"; + return ErrorCode::InternalError; + } + +#ifdef Q_OS_WIN + int vpnAdapterIndex = -1; + QList netInterfaces = QNetworkInterface::allInterfaces(); + for (auto& netInterface : netInterfaces) { + for (auto& address : netInterface.addressEntries()) { + if (m_vpnLocalAddress == address.ip().toString()) + vpnAdapterIndex = netInterface.index(); + } + } +#else + static const int vpnAdapterIndex = 0; +#endif + const bool killSwitchEnabled = QVariant(m_rawConfig.value(config_key::killSwitchOption).toString()).toBool(); + if (killSwitchEnabled) { + if (vpnAdapterIndex != -1) { + QJsonObject config = m_rawConfig; + config.insert("vpnServer", m_remoteAddress); + + auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex); + if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) { + qCritical() << "Failed to enable killswitch"; + return ErrorCode::InternalError; + } + } else + qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled"; + } + + if (m_routeMode == Settings::RouteMode::VpnAllSites) { + static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" }; + + auto routeAddList = iface->routeAddList(m_vpnGateway, subnets); + if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) { + qCritical() << "Failed to set routes for TUN"; + return ErrorCode::InternalError; + } + } + + auto StopRoutingIpv6 = iface->StopRoutingIpv6(); + if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) { + qCritical() << "Failed to disable IPv6 routing"; + return ErrorCode::InternalError; + } + +#ifdef Q_OS_WIN + if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) { + QJsonObject config = m_rawConfig; + config.insert("inetAdapterIndex", inetAdapterIndex); + config.insert("vpnAdapterIndex", vpnAdapterIndex); + config.insert("vpnGateway", m_vpnGateway); + config.insert("vpnServer", m_remoteAddress); + + auto enablePeerTraffic = iface->enablePeerTraffic(config); + if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) { + qCritical() << "Failed to enable peer traffic"; + return ErrorCode::InternalError; + } + } else + qWarning() << "Failed to get adapter indexes. Split-tunneling disabled"; +#endif + return ErrorCode::NoError; + }, + [] () { + return ErrorCode::AmneziaServiceConnectionFailed; + }); } diff --git a/client/protocols/xrayprotocol.h b/client/protocols/xrayprotocol.h index 10f81fbc..bccb844a 100644 --- a/client/protocols/xrayprotocol.h +++ b/client/protocols/xrayprotocol.h @@ -6,6 +6,7 @@ #include "core/ipcclient.h" #include "vpnprotocol.h" #include "settings.h" +#include class XrayProtocol : public VpnProtocol { @@ -14,19 +15,18 @@ public: virtual ~XrayProtocol() override; ErrorCode start() override; - ErrorCode startTun2Sock(); void stop() override; private: - void readXrayConfiguration(const QJsonObject &configuration); - + ErrorCode setupRouting(); + ErrorCode startTun2Socks(); + QJsonObject m_xrayConfig; Settings::RouteMode m_routeMode; - QString m_primaryDNS; - QString m_secondaryDNS; -#ifndef Q_OS_IOS - QSharedPointer m_t2sProcess; -#endif + QList m_dnsServers; + QString m_remoteAddress; + + QSharedPointer m_tun2socksProcess; }; #endif // XRAYPROTOCOL_H diff --git a/client/resources.qrc b/client/resources.qrc index 6a768391..d69741f9 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -131,6 +131,7 @@ ui/qml/Components/AdLabel.qml ui/qml/Components/ConnectButton.qml ui/qml/Components/ConnectionTypeSelectionDrawer.qml + ui/qml/Components/GamepadLoader.qml ui/qml/Components/HomeContainersListView.qml ui/qml/Components/HomeSplitTunnelingDrawer.qml ui/qml/Components/InstalledAppsDrawer.qml diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 301e4491..96820202 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -387,6 +387,51 @@ bool ApiConfigsController::fillAvailableServices() } QJsonObject data = QJsonDocument::fromJson(responseBody).object(); + +#if defined(Q_OS_IOS) || defined(MACOS_NE) + QEventLoop waitProducts; + bool productsFetched = false; + QString productPrice; + QString productCurrency; + + IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"), + [&](const QList &products, + const QStringList &invalidIds, + const QString &errorString) { + if (!errorString.isEmpty() || products.isEmpty()) { + qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString; + } else { + const auto &product = products.first(); + productPrice = product.value("price").toString(); + productCurrency = product.value("currencyCode").toString(); + productsFetched = true; + qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency; + } + waitProducts.quit(); + }); + waitProducts.exec(); + + if (productsFetched && !productPrice.isEmpty()) { + QJsonArray services = data.value("services").toArray(); + for (int i = 0; i < services.size(); ++i) { + QJsonObject service = services[i].toObject(); + if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) { + QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); + QString formattedPrice = productPrice; + if (!productCurrency.isEmpty()) { + formattedPrice += " " + productCurrency; + } + serviceInfo["price"] = formattedPrice; + service[configKey::serviceInfo] = serviceInfo; + services[i] = service; + data["services"] = services; + qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice; + break; + } + } + } +#endif + m_apiServicesModel->updateModel(data); if (m_apiServicesModel->rowCount() > 0) { m_apiServicesModel->setServiceIndex(0); diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 993ff7c7..0371b1b9 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -326,7 +326,7 @@ void ExportController::generateXrayConfig(const QString &clientName) vlessServer.spiderX = realitySettings.value("spiderX").toString(""); } - m_nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN"); + m_nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "DefaultVPN"); emit exportConfigChanged(); } diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 2a1332e2..91d7ec3b 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -291,6 +291,8 @@ void ImportController::processNativeWireGuardConfig() clientProtocolConfig[config_key::cookieReplyPacketJunkSize] = "0"; clientProtocolConfig[config_key::transportPacketJunkSize] = "0"; + clientProtocolConfig[config_key::specialJunk1] = protocols::awg::defaultSpecialJunk1; + clientProtocolConfig[config_key::isObfuscationEnabled] = true; serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson()); diff --git a/client/ui/models/api/apiServicesModel.cpp b/client/ui/models/api/apiServicesModel.cpp index d571346b..5ed9cca1 100644 --- a/client/ui/models/api/apiServicesModel.cpp +++ b/client/ui/models/api/apiServicesModel.cpp @@ -112,7 +112,11 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (price == "free") { return tr("Free"); } +#if defined(Q_OS_IOS) || defined(MACOS_NE) + return tr("%1 $").arg(price); +#else return tr("%1 $/month").arg(price); +#endif } case EndDateRole: { return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); diff --git a/client/ui/qml/Components/GamepadLoader.qml b/client/ui/qml/Components/GamepadLoader.qml new file mode 100644 index 00000000..069e1549 --- /dev/null +++ b/client/ui/qml/Components/GamepadLoader.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtGamepadLegacy + +Item { + id: root + + property alias gamepad: gamepad + property alias gamepadKeyNav: gamepadKeyNav + + Gamepad { + id: gamepad + deviceId: GamepadManager.connectedGamepads.length > 0 ? GamepadManager.connectedGamepads[0] : -1 + + onButtonStartChanged: { + if (buttonStart) { + ServersModel.setProcessedServerIndex(ServersModel.defaultIndex) + ConnectionController.connectButtonClicked() + } + } + } + + GamepadKeyNavigation { + id: gamepadKeyNav + gamepad: gamepad + active: true + } + + Connections { + target: GamepadManager + function onConnectedGamepadsChanged() { + if (GamepadManager.connectedGamepads.length > 0) { + gamepad.deviceId = GamepadManager.connectedGamepads[0] + } else { + gamepad.deviceId = -1 + } + } + } +} diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 914fc267..769c3e04 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -111,11 +111,11 @@ Button { color: { if (root.enabled) { if (root.pressed) { - return pressedColor + return root.pressedColor } - return root.hovered ? hoveredColor : defaultColor + return root.hovered ? root.hoveredColor : root.defaultColor } else { - return disabledColor + return root.disabledColor } } diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index c88f9e7c..bfa31c63 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -49,10 +49,29 @@ Item { return drawerContent.state === stateName } + function isDrawerType2(obj) { + return obj && typeof obj.drawerExpandedStateName !== "undefined" && + typeof obj.drawerCollapsedStateName !== "undefined" + } + + function isDescendantOfDrawer(obj) { + var current = obj + while (current && current !== root.parent) { + if (isDrawerType2(current)) { + return true + } + current = current.parent + } + return false + } + function findComponent(obj, typeCtor) { if (!obj) return null + if (isDrawerType2(obj) || isDescendantOfDrawer(obj)) + return null + if (obj instanceof typeCtor) return obj diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index b7de64c6..89758430 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -19,9 +19,6 @@ Item { property string buttonText property string buttonImageSource - property string buttonImageColor: AmneziaStyle.color.midnightBlack - property string buttonBackgroundColor: AmneziaStyle.color.paleGray - property string buttonHoveredColor: AmneziaStyle.color.lightGray property var clickedFunc property alias textField: textField @@ -70,7 +67,7 @@ Item { border.width: 1 Behavior on border.color { - PropertyAnimation { duration: 100 } + PropertyAnimation { duration: 200 } } RowLayout { @@ -124,7 +121,7 @@ Item { background: Rectangle { anchors.fill: parent - color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor + color: root.backgroundDisabledColor } onTextChanged: { @@ -189,14 +186,6 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText leftImageSource: root.buttonImageSource - leftImageColor: root.buttonImageColor - - defaultColor: root.buttonBackgroundColor - hoveredColor: root.buttonHoveredColor - pressedColor: root.buttonHoveredColor - disabledColor: AmneziaStyle.color.transparent - - borderWidth: 0 anchors.top: content.top anchors.bottom: content.bottom @@ -204,7 +193,7 @@ Item { height: content.implicitHeight width: content.implicitHeight - squareLeftSide: false + squareLeftSide: true clickedFunc: function() { if (root.clickedFunc && typeof root.clickedFunc === "function") { diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 050aeeeb..532ab6a1 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -396,9 +396,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - if (ApiConfigsController.deactivateDevice(true)) { - InstallController.removeProcessedServer() - } + InstallController.removeProcessedServer() PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index 4445b08b..f3c63eb5 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -224,7 +224,6 @@ PageType { height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin color: AmneziaStyle.color.midnightBlack - opacity: 0.8 RowLayout { id: addAppButton diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 27aa5dea..ed092336 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -240,7 +240,6 @@ PageType { height: addSiteButton.implicitHeight + 48 color: AmneziaStyle.color.midnightBlack - opacity: 0.8 RowLayout { id: addSiteButton diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml index 16ac2f20..c5e581af 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -97,16 +97,32 @@ PageType { } } + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium" + + horizontalAlignment: Text.AlignHCenter + textFormat: Text.PlainText + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + + text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.") + } + BasicButtonType { id: continueButton Layout.fillWidth: true Layout.topMargin: 32 - Layout.bottomMargin: 32 + Layout.bottomMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("Connect") + text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect") clickedFunc: function() { PageController.showBusyIndicator(true) @@ -121,6 +137,37 @@ PageType { } } } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + + visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium" + + horizontalAlignment: Text.AlignHCenter + textFormat: Text.RichText + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + + text: { + var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" + var privacyUrl = LanguageModel.getCurrentSiteUrl("policy") + return qsTr("By continuing, you agree to the Terms of Use and Privacy Policy").arg(termsUrl).arg(privacyUrl) + } + + onLinkActivated: function(link) { + Qt.openUrlExternally(link) + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } } } diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index ae43455a..4eba5b3b 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -83,6 +83,11 @@ Window { } } + Loader { + active: Qt.platform.os === "android" + source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : "" + } + Connections { objectName: "pageControllerConnections" diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index afe47840..d249fae6 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -41,7 +41,6 @@ VpnConnection::VpnConnection(std::shared_ptr settings, QObject *parent m_checkTimer.setInterval(1000); connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged); connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged); - #endif } @@ -59,7 +58,7 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled) #ifdef AMNEZIA_DESKTOP IpcClient::withInterface([enabled](QSharedPointer iface){ QRemoteObjectPendingReply reply = iface->refreshKillSwitch(enabled); - if (reply.waitForFinished(1000) && reply.returnValue()) + if (reply.waitForFinished() && reply.returnValue()) qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed"; else qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call"; @@ -73,60 +72,57 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) auto container = m_settings->defaultContainer(m_settings->defaultServerIndex()); IpcClient::withInterface([&](QSharedPointer iface) { - if (state == Vpn::ConnectionState::Connected) { - iface->resetIpStack(); - iface->flushDns(); + switch (state) { + case Vpn::ConnectionState::Connected: { + iface->resetIpStack(); - if (!ContainerProps::isAwgContainer(container) && - container != DockerContainer::WireGuard) { - QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); - QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); + auto flushDns = iface->flushDns(); + if (flushDns.waitForFinished() && flushDns.returnValue()) + qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS"; + else + qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes"; - iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); - if (m_settings->isSitesSplitTunnelingEnabled()) { - iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); - // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - QTimer::singleShot(1000, m_vpnProtocol.data(), - [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1"); - iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1"); + if (!ContainerProps::isAwgContainer(container) && + container != DockerContainer::WireGuard) { + QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); + QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); - iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress()); - addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode()); + // TODO: add error code handling for all routeAddList (or rework the code below) + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); + + if (m_settings->isSitesSplitTunnelingEnabled()) { + iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); + // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); + if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { + QTimer::singleShot(1000, m_vpnProtocol.data(), + [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); + } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1"); + iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1"); + + iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress()); + addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode()); + } } } - } + } break; + case Vpn::ConnectionState::Disconnected: + case Vpn::ConnectionState::Error: { + auto flushDns = iface->flushDns(); + if (flushDns.waitForFinished() && flushDns.returnValue()) + qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS"; + else + qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS"; - if (container != DockerContainer::Ipsec) { - if (startNetworkCheckIfReady()) { - m_pendingNetworkCheck = false; - } else { - m_pendingNetworkCheck = true; - qWarning() << "Deferring startNetworkCheck; missing gateway/local address" - << m_vpnProtocol->vpnGateway() << m_vpnProtocol->vpnLocalAddress(); - } - } else { - m_pendingNetworkCheck = false; - } - - } else if (state == Vpn::ConnectionState::Error) { - m_pendingNetworkCheck = false; - iface->flushDns(); - - if (m_settings->isSitesSplitTunnelingEnabled()) { - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - iface->clearSavedRoutes(); - } - } - } else if (state == Vpn::ConnectionState::Connecting) { - - } else if (state == Vpn::ConnectionState::Disconnected) { - m_pendingNetworkCheck = false; - auto result = iface->stopNetworkCheck(); - result.waitForFinished(3000); + auto clearSavedRoutes = iface->clearSavedRoutes(); + if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue()) + qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes"; + else + qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes"; + } break; + default: + break; } }); #endif @@ -140,7 +136,6 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) m_checkTimer.stop(); } #endif - emit connectionStateChanged(state); } const QString &VpnConnection::remoteAddress() const @@ -185,7 +180,11 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode) }); m_settings->addVpnSite(mode, site, ip); } - flushDns(); + IpcClient::withInterface([](QSharedPointer iface) { + auto reply = iface->flushDns(); + if (reply.waitForFinished() || !reply.returnValue()) + qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS"; + }); break; } } @@ -200,48 +199,6 @@ QSharedPointer VpnConnection::vpnProtocol() const return m_vpnProtocol; } -void VpnConnection::addRoutes(const QStringList &ips) -{ -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([&](QSharedPointer iface) { - if (connectionState() == Vpn::ConnectionState::Connected) { - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - iface->routeAddList(m_vpnProtocol->vpnGateway(), ips); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - iface->routeAddList(m_vpnProtocol->routeGateway(), ips); - } - } - }); -#endif -} - -void VpnConnection::deleteRoutes(const QStringList &ips) -{ -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([&](QSharedPointer iface) { - if (connectionState() == Vpn::ConnectionState::Connected) { - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - iface->routeDeleteList(vpnProtocol()->vpnGateway(), ips); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - iface->routeDeleteList(m_vpnProtocol->routeGateway(), ips); - } - } - }); -#endif -} - -void VpnConnection::flushDns() -{ -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([](QSharedPointer iface) { - auto reply = iface->flushDns(); - if (reply.waitForFinished(1000) || !reply.returnValue()) { - qWarning() << "VpnConnection::flushDns(): Failed to flush DNS"; - } - }); -#endif -} - void VpnConnection::disconnectSlots() { if (m_vpnProtocol) { @@ -265,19 +222,15 @@ ErrorCode VpnConnection::lastError() const void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration) { - qDebug() << QString("ConnectToVpn, Server index is %1, container is %2, route mode is") + qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is") .arg(serverIndex) .arg(ContainerProps::containerToString(container)) << m_settings->routeMode(); m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName); - emit connectionStateChanged(Vpn::ConnectionState::Connecting); + setConnectionState(Vpn::ConnectionState::Connecting); - m_pendingNetworkCheck = false; m_vpnConfiguration = vpnConfiguration; - m_serverIndex = serverIndex; - m_serverCredentials = credentials; - m_dockerContainer = container; #ifdef AMNEZIA_DESKTOP if (m_vpnProtocol) { @@ -293,7 +246,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration)); if (!m_vpnProtocol) { - emit connectionStateChanged(Vpn::ConnectionState::Error); + setConnectionState(Vpn::ConnectionState::Error); return; } m_vpnProtocol->prepare(); @@ -311,75 +264,23 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede createProtocolConnections(); - ErrorCode errorCode = m_vpnProtocol->start(); - if (errorCode != ErrorCode::NoError) - emit connectionStateChanged(Vpn::ConnectionState::Error); -} - -void VpnConnection::restartConnection() -{ - // Only reconnect if VPN was connected before sleep/network change - if (!m_wasConnectedBeforeSleep) { - qDebug() << "VPN was not connected before sleep/network change, skipping reconnection"; - return; + if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) { + setConnectionState(Vpn::ConnectionState::Error); + emit vpnProtocolError(err); } - - qDebug() << "VPN was connected before sleep/network change, attempting reconnection"; - this->disconnectFromVpn(); -#ifdef Q_OS_LINUX - QThread::msleep(5000); -#endif - this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration); - - // Reset the flag after reconnection attempt - m_wasConnectedBeforeSleep = false; } void VpnConnection::createProtocolConnections() { connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError); - connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this, - SLOT(onConnectionStateChanged(Vpn::ConnectionState))); + connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::setConnectionState); connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64))); #ifdef AMNEZIA_DESKTOP - if (m_connectionLoseHandle) - disconnect(m_connectionLoseHandle); - if (m_networkChangeHandle) - disconnect(m_networkChangeHandle); - m_connectionLoseHandle = QMetaObject::Connection(); - m_networkChangeHandle = QMetaObject::Connection(); - - // TODO: replace unsafe IpcClient::Interface() calls - m_connectionLoseHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::connectionLose, - this, [this]() { - qDebug() << "Connection Lose"; - auto result = IpcClient::Interface()->stopNetworkCheck(); - result.waitForFinished(3000); - // Track VPN state before connection loss - m_wasConnectedBeforeSleep = isConnected(); - qDebug() << "VPN was connected before connection loss:" << m_wasConnectedBeforeSleep; - this->restartConnection(); - }); - m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange, - this, [this]() { - qDebug() << "Network change"; - // Track VPN state before network change (including sleep/wake) - m_wasConnectedBeforeSleep = isConnected(); - qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep; - this->restartConnection(); - }); - connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated, - this, [this](const QString& gateway, const QString& localAddress) { - Q_UNUSED(gateway) - Q_UNUSED(localAddress) - if (connectionState() != Vpn::ConnectionState::Connected) { - return; - } - if (startNetworkCheckIfReady()) { - m_pendingNetworkCheck = false; - } - }); + IpcClient::withInterface([this](QSharedPointer rep) { + connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection); + connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection); + }); #endif } @@ -482,28 +383,13 @@ void VpnConnection::appendSplitTunnelingConfig() m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode); m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray); -} -bool VpnConnection::startNetworkCheckIfReady() -{ -#ifdef AMNEZIA_DESKTOP - if (!m_vpnProtocol || m_dockerContainer == DockerContainer::Ipsec) { - return false; - } - - const QString gateway = m_vpnProtocol->vpnGateway(); - const QString localAddress = m_vpnProtocol->vpnLocalAddress(); - if (gateway.isEmpty() || localAddress.isEmpty()) { - return false; - } - - return IpcClient::withInterface([&](QSharedPointer iface) { - QRemoteObjectPendingReply reply = iface->startNetworkCheck(gateway, localAddress); - return reply.waitForFinished(1000) && reply.returnValue(); - }); -#else - return false; -#endif + qDebug() << QString("Site split tunneling is %1, route mode is %2") + .arg(m_settings->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled") + .arg(routeMode); + qDebug() << QString("App split tunneling is %1, route mode is %2") + .arg(m_settings->isAppsSplitTunnelingEnabled() ? "enabled" : "disabled") + .arg(appsRouteMode); } #ifdef Q_OS_ANDROID @@ -537,6 +423,27 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes) return QString("%1 %2").arg(QString::number(mbps, 'f', 2)).arg(tr("Mbps")); // Mbit/s } +void VpnConnection::reconnectToVpn() { + if (m_vpnProtocol.isNull()) + return; + + if (m_connectionState != Vpn::ConnectionState::Connected) { + qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot") + .arg(QMetaEnum::fromType().valueToKey(m_connectionState)); + return; + } + + qDebug() << "Reconnect triggered. Reconnecting to the server"; + + setConnectionState(Vpn::ConnectionState::Reconnecting); + + m_vpnProtocol->stop(); + if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) { + setConnectionState(Vpn::ConnectionState::Error); + emit vpnProtocolError(err); + } +} + void VpnConnection::disconnectFromVpn() { #if defined(Q_OS_IOS) || defined(MACOS_NE) @@ -546,41 +453,26 @@ void VpnConnection::disconnectFromVpn() #endif if (m_vpnProtocol.isNull()) { - emit connectionStateChanged(Vpn::ConnectionState::Disconnected); + setConnectionState(Vpn::ConnectionState::Disconnected); return; } - m_vpnProtocol->stop(); - -#ifdef AMNEZIA_DESKTOP - IpcClient::withInterface([](QSharedPointer iface) { - QRemoteObjectPendingReply flushReply = iface->flushDns(); - if (flushReply.waitForFinished(5000) && flushReply.returnValue()) - qDebug() << "VpnConnection::disconnectFromVpn(): Successfully flushed DNS"; - else - qWarning() << "VpnConnection::disconnectFromVpn(): Failed to flush DNS"; - - QRemoteObjectPendingReply clearSavedRoutesReply = iface->clearSavedRoutes(); - if (clearSavedRoutesReply.waitForFinished(5000) && clearSavedRoutesReply.returnValue()) - qDebug() << "VpnConnection::disconnectFromVpn(): Successfully cleared saved routes"; - else - qWarning() << "VpnConnection::disconnectFromVpn(): Failed to clear saved routes"; - }); -#endif + setConnectionState(Vpn::ConnectionState::Disconnecting); #ifdef Q_OS_ANDROID auto *const connection = new QMetaObject::Connection; *connection = connect(AndroidController::instance(), &AndroidController::vpnStateChanged, this, [this, connection](AndroidController::ConnectionState state) { if (state == AndroidController::ConnectionState::DISCONNECTED) { - onConnectionStateChanged(Vpn::ConnectionState::Disconnected); + setConnectionState(Vpn::ConnectionState::Disconnected); disconnect(*connection); delete connection; } }); - m_vpnProtocol->stop(); #endif + m_vpnProtocol->stop(); + #if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP) m_vpnProtocol->deleteLater(); #endif @@ -588,27 +480,12 @@ void VpnConnection::disconnectFromVpn() m_vpnProtocol = nullptr; } -Vpn::ConnectionState VpnConnection::connectionState() -{ - if (!m_vpnProtocol) - return Vpn::ConnectionState::Disconnected; - return m_vpnProtocol->connectionState(); -} - -bool VpnConnection::isConnected() const -{ - if (m_vpnProtocol.isNull()) { - return false; - } - - return m_vpnProtocol->isConnected(); -} - -bool VpnConnection::isDisconnected() const -{ - if (m_vpnProtocol.isNull()) { - return true; - } - - return m_vpnProtocol->isDisconnected(); +void VpnConnection::setConnectionState(Vpn::ConnectionState state) { + onConnectionStateChanged(state); + + if (state == Vpn::Disconnected && m_connectionState == Vpn::Reconnecting) + return; + + m_connectionState = state; + emit connectionStateChanged(state); } diff --git a/client/vpnconnection.h b/client/vpnconnection.h index 070ab36f..777573f2 100644 --- a/client/vpnconnection.h +++ b/client/vpnconnection.h @@ -34,10 +34,6 @@ public: ErrorCode lastError() const; - bool isConnected() const; - bool isDisconnected() const; - - Vpn::ConnectionState connectionState(); QSharedPointer vpnProtocol() const; const QString &remoteAddress() const; @@ -48,15 +44,10 @@ public: #endif public slots: - void connectToVpn(int serverIndex, - const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); - + void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); + void reconnectToVpn(); void disconnectFromVpn(); - void restartConnection(); - void addRoutes(const QStringList &ips); - void deleteRoutes(const QStringList &ips); - void flushDns(); void onKillSwitchModeChanged(bool enabled); void disconnectSlots(); @@ -71,10 +62,10 @@ protected slots: void onBytesChanged(quint64 receivedBytes, quint64 sentBytes); void onConnectionStateChanged(Vpn::ConnectionState state); + void setConnectionState(Vpn::ConnectionState state); + protected: QSharedPointer m_vpnProtocol; - QMetaObject::Connection m_connectionLoseHandle; - QMetaObject::Connection m_networkChangeHandle; private: std::shared_ptr m_settings; @@ -82,14 +73,6 @@ private: QJsonObject m_routeMode; QString m_remoteAddress; - ServerCredentials m_serverCredentials; - int m_serverIndex; - DockerContainer m_dockerContainer; - - // Track VPN state before sleep for smart reconnection - bool m_wasConnectedBeforeSleep = false; - bool m_pendingNetworkCheck = false; - // Only for iOS for now, check counters QTimer m_checkTimer; @@ -100,11 +83,12 @@ private: void createAndroidConnections(); #endif + Vpn::ConnectionState m_connectionState; + void createProtocolConnections(); void appendSplitTunnelingConfig(); void appendKillSwitchConfig(); - bool startNetworkCheckIfReady(); }; #endif // VPNCONNECTION_H diff --git a/deploy/build_windows.bat b/deploy/build_windows.bat index c951fb8c..c841b68d 100644 --- a/deploy/build_windows.bat +++ b/deploy/build_windows.bat @@ -31,6 +31,7 @@ set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=% set APP_NAME=DefaultVPN set APP_FILENAME=%APP_NAME:"=%.exe +set SERVICE_FILENAME=%APP_NAME:"=%-service.exe set APP_DOMAIN=org.defaultvpn.package set OUT_APP_DIR=%WORK_DIR:"=%\client\release set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=% @@ -43,6 +44,7 @@ set STAGE_DIR=%WORK_DIR:"=%\stage echo "Environment:" echo "WORK_DIR: %WORK_DIR%" echo "APP_FILENAME: %APP_FILENAME%" +echo "SERVICE_FILENAME: %SERVICE_FILENAME%" echo "PROJECT_DIR: %PROJECT_DIR%" echo "SCRIPT_DIR: %SCRIPT_DIR%" echo "OUT_APP_DIR: %OUT_APP_DIR%" @@ -74,7 +76,7 @@ if %errorlevel% neq 0 exit /b %errorlevel% echo "Deploying..." mkdir "%OUT_APP_DIR%" -copy "%WORK_DIR%\service\server\release\%APP_NAME%-service.exe" "%OUT_APP_DIR%" +copy "%WORK_DIR%\service\server\release\%SERVICE_FILENAME%" "%OUT_APP_DIR%" rem copy "%WORK_DIR%\client\%APP_FILENAME%" "%OUT_APP_DIR%" copy /Y "%PROJECT_DIR%\client\images\app.ico" "%OUT_APP_DIR%\AmneziaVPN.ico" >nul @@ -83,7 +85,8 @@ echo "Signing exe" cd %OUT_APP_DIR% signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe -"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%" +"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations --force-openssl "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%" +"%QT_BIN_DIR:"=%\windeployqt" --release "%OUT_APP_DIR:"=%\%SERVICE_FILENAME:"=%" signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll diff --git a/deploy/data/windows/x32/post_uninstall.cmd b/deploy/data/windows/x32/post_uninstall.cmd index 79e8e5b8..803a802d 100644 --- a/deploy/data/windows/x32/post_uninstall.cmd +++ b/deploy/data/windows/x32/post_uninstall.cmd @@ -14,6 +14,8 @@ sc stop DefaultVPN-service sc delete DefaultVPN-service sc stop AmneziaWGTunnel$DefaultVPN sc delete AmneziaWGTunnel$DefaultVPN +sc stop DefaultVPNSplitTunnel +sc delete DefaultVPNSplitTunnel taskkill /IM "DefaultVPN-service.exe" /F taskkill /IM "DefaultVPN.exe" /F diff --git a/deploy/data/windows/x64/post_uninstall.cmd b/deploy/data/windows/x64/post_uninstall.cmd index 79e8e5b8..803a802d 100644 --- a/deploy/data/windows/x64/post_uninstall.cmd +++ b/deploy/data/windows/x64/post_uninstall.cmd @@ -14,6 +14,8 @@ sc stop DefaultVPN-service sc delete DefaultVPN-service sc stop AmneziaWGTunnel$DefaultVPN sc delete AmneziaWGTunnel$DefaultVPN +sc stop DefaultVPNSplitTunnel +sc delete DefaultVPNSplitTunnel taskkill /IM "DefaultVPN-service.exe" /F taskkill /IM "DefaultVPN.exe" /F diff --git a/deploy/deploy_s3.sh b/deploy/deploy_s3.sh index 14368ae9..70cbde60 100755 --- a/deploy/deploy_s3.sh +++ b/deploy/deploy_s3.sh @@ -1,9 +1,9 @@ -#!/bin/sh +#!/bin/bash set -e VERSION=$1 -if [[ $VERSION = '' ]]; then +if [[ -z "$VERSION" ]]; then echo '::error::VERSION does not set. Exiting with error...' exit 1 fi @@ -14,20 +14,39 @@ cd dist echo $VERSION >> VERSION curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG +curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE if [[ $(cat CHANGELOG) = null ]]; then echo '::error::Release does not exists. Exiting with error...' exit 1 fi -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_arm64-v8a.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_armeabi-v7a.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86_64.apk -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_linux_x64.tar -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_macos.pkg -wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_x64.exe +# Download files with error handling +download_file() { + local url=$1 + local filename=$(basename "$url") + echo "Downloading $filename..." + if ! wget -q "$url"; then + echo "::error::Failed to download $filename from $url" + exit 8 + fi + echo "Successfully downloaded $filename" +} + +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_arm64-v8a.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_armeabi-v7a.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_android9+_x86_64.apk +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_linux_x64.tar +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_macos.pkg +download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/DefaultVPN_${VERSION}_x64.exe cd ../ -rclone sync ./dist/ r2:/updates/ +echo "Syncing to R2..." +if ! rclone sync ./dist/ r2:/updates/; then + echo "::error::Failed to sync files to R2" + exit 8 +fi + +echo "Deployment completed successfully!" diff --git a/ipc/ipc.h b/ipc/ipc.h index c15b7a63..b242ad14 100644 --- a/ipc/ipc.h +++ b/ipc/ipc.h @@ -11,6 +11,7 @@ namespace amnezia { enum PermittedProcess { + Invalid, OpenVPN, Wireguard, Tun2Socks, @@ -19,16 +20,18 @@ enum PermittedProcess { inline QString permittedProcessPath(PermittedProcess pid) { - if (pid == PermittedProcess::OpenVPN) { - return Utils::openVpnExecPath(); - } else if (pid == PermittedProcess::Wireguard) { - return Utils::wireguardExecPath(); - } else if (pid == PermittedProcess::CertUtil) { - return Utils::certUtilPath(); - } else if (pid == PermittedProcess::Tun2Socks) { - return Utils::tun2socksPath(); + switch (pid) { + case PermittedProcess::OpenVPN: + return Utils::openVpnExecPath(); + case PermittedProcess::Wireguard: + return Utils::wireguardExecPath(); + case PermittedProcess::CertUtil: + return Utils::certUtilPath(); + case PermittedProcess::Tun2Socks: + return Utils::tun2socksPath(); + default: + return ""; } - return ""; } @@ -48,6 +51,51 @@ inline QString getIpcProcessUrl(int pid) { #endif } +inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &args) { + using Validator = std::function; + QMap namedArgs; + QList positionalArgs; + + switch (proc) { + case Tun2Socks: + namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); }; + namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); }; + break; + default: + //FIXME + return args; + } + + + QStringList sanitized; + + for (int i = 0, pos = 0; i < args.size(); i++) { + const auto& key = args[i]; + + if (const auto found = namedArgs.find(key); found != namedArgs.end()) { + const auto validator = found.value(); + + if (validator) { + if (i + 1 < args.size()) { + const auto& value = args[i+1]; + if (validator(value)) { + sanitized << key << value; + i++; + } + } + } else { + sanitized << key; + } + } else if (pos < positionalArgs.size()) { + if (const auto validator = positionalArgs[pos]; validator && validator(key)) { + sanitized << key; + pos++; + } + } + } + + return sanitized; +} } // namespace amnezia diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 2320c6a4..f26bd8b3 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -38,12 +38,13 @@ class IpcInterface SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); SLOT( bool restoreResolvers() ); - SLOT(void xrayStart(const QString &config)); - SLOT(void xrayStop()); + SLOT(bool xrayStart(const QString &config)); + SLOT(bool xrayStop()); SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) ); SLOT( bool stopNetworkCheck() ); SIGNAL( connectionLose() ); - SIGNAL( networkChange() ); + SIGNAL( wakeup() ); + SIGNAL( networkChanged() ); }; diff --git a/ipc/ipc_process_interface.rep b/ipc/ipc_process_interface.rep index 6b3bb654..7bf2ed22 100644 --- a/ipc/ipc_process_interface.rep +++ b/ipc/ipc_process_interface.rep @@ -4,6 +4,8 @@ class IpcProcessInterface { SLOT( start() ); + SLOT( terminate() ); + SLOT( kill() ); SLOT( close() ); SLOT( setArguments(const QStringList &arguments) ); @@ -17,6 +19,11 @@ class IpcProcessInterface SLOT( QByteArray readAllStandardError() ); SLOT( QByteArray readAllStandardOutput() ); + SLOT( bool waitForFinished() ); + SLOT( bool waitForFinished(int msecs) ); + SLOT( bool waitForStarted() ); + SLOT( bool waitForStarted(int msecs) ); + SIGNAL( errorOccurred(QProcess::ProcessError error) ); SIGNAL( finished(int exitCode, QProcess::ExitStatus exitStatus) ); diff --git a/ipc/ipc_process_tun2socks.rep b/ipc/ipc_process_tun2socks.rep deleted file mode 100644 index e355035e..00000000 --- a/ipc/ipc_process_tun2socks.rep +++ /dev/null @@ -1,11 +0,0 @@ -#include -#include - -class IpcProcessTun2Socks -{ - SLOT( start() ); - SLOT( stop() ); - - SIGNAL( setConnectionState(int state) ); - SIGNAL( stateChanged(QProcess::ProcessState newState) ); -}; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 77f3a351..4d02c1dd 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -304,7 +304,7 @@ bool IpcServer::refreshKillSwitch(bool enabled) return KillSwitch::instance()->refresh(enabled); } -void IpcServer::xrayStart(const QString& cfg) +bool IpcServer::xrayStart(const QString& cfg) { #ifdef MZ_DEBUG qDebug() << "IpcServer::xrayStart"; @@ -313,7 +313,7 @@ void IpcServer::xrayStart(const QString& cfg) return Xray::getInstance().startXray(cfg); } -void IpcServer::xrayStop() +bool IpcServer::xrayStop() { #ifdef MZ_DEBUG qDebug() << "IpcServer::xrayStop"; diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 5a63302c..e8607c5a 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -10,10 +10,8 @@ #include "ipc.h" #include "ipcserverprocess.h" -#include "ipctun2socksprocess.h" #include "rep_ipc_interface_source.h" -#include "rep_ipc_process_tun2socks_source.h" class IpcServer : public IpcInterfaceSource { @@ -44,8 +42,8 @@ public: virtual bool refreshKillSwitch( bool enabled ) override; virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; virtual bool restoreResolvers() override; - virtual void xrayStart(const QString& cfg) override; - virtual void xrayStop() override; + virtual bool xrayStart(const QString& cfg) override; + virtual bool xrayStop() override; virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override; virtual bool stopNetworkCheck() override; @@ -56,12 +54,10 @@ private: ProcessDescriptor (QObject *parent = nullptr) { serverNode = QSharedPointer(new QRemoteObjectHost(parent)); ipcProcess = QSharedPointer(new IpcServerProcess(parent)); - tun2socksProcess = QSharedPointer(new IpcProcessTun2Socks(parent)); localServer = QSharedPointer(new QLocalServer(parent)); } QSharedPointer ipcProcess; - QSharedPointer tun2socksProcess; QSharedPointer serverNode; QSharedPointer localServer; }; diff --git a/ipc/ipcserverprocess.cpp b/ipc/ipcserverprocess.cpp index 497e89d7..4890e688 100644 --- a/ipc/ipcserverprocess.cpp +++ b/ipc/ipcserverprocess.cpp @@ -40,6 +40,14 @@ void IpcServerProcess::start() m_process->waitForStarted(); } +void IpcServerProcess::terminate() { + m_process->terminate(); +} + +void IpcServerProcess::kill() { + m_process->kill(); +} + void IpcServerProcess::close() { m_process->close(); @@ -47,7 +55,7 @@ void IpcServerProcess::close() void IpcServerProcess::setArguments(const QStringList &arguments) { - m_process->setArguments(arguments); + m_process->setArguments(amnezia::sanitizeArguments(m_program, arguments)); } void IpcServerProcess::setInputChannelMode(QProcess::InputChannelMode mode) @@ -69,7 +77,9 @@ void IpcServerProcess::setProcessChannelMode(QProcess::ProcessChannelMode mode) void IpcServerProcess::setProgram(int programId) { - m_process->setProgram(amnezia::permittedProcessPath(static_cast(programId))); + m_program = static_cast(programId); + m_process->setProgram(amnezia::permittedProcessPath(m_program)); + m_process->setArguments({}); } void IpcServerProcess::setWorkingDirectory(const QString &dir) @@ -92,4 +102,20 @@ QByteArray IpcServerProcess::readAllStandardOutput() return m_process->readAllStandardOutput(); } +bool IpcServerProcess::waitForStarted() { + return m_process->waitForStarted(); +} + +bool IpcServerProcess::waitForStarted(int msecs) { + return m_process->waitForStarted(msecs); +} + +bool IpcServerProcess::waitForFinished() { + return m_process->waitForFinished(); +} + +bool IpcServerProcess::waitForFinished(int msecs) { + return m_process->waitForFinished(msecs); +} + #endif diff --git a/ipc/ipcserverprocess.h b/ipc/ipcserverprocess.h index b427d639..722b04f0 100644 --- a/ipc/ipcserverprocess.h +++ b/ipc/ipcserverprocess.h @@ -1,6 +1,7 @@ #ifndef IPCSERVERPROCESS_H #define IPCSERVERPROCESS_H +#include "ipc.h" #include #ifndef Q_OS_IOS @@ -14,6 +15,8 @@ public: virtual ~IpcServerProcess(); void start() override; + void terminate() override; + void kill() override; void close() override; void setArguments(const QStringList &arguments) override; @@ -27,9 +30,15 @@ public: QByteArray readAllStandardError() override; QByteArray readAllStandardOutput() override; + bool waitForStarted() override; + bool waitForStarted(int msecs) override; + bool waitForFinished() override; + bool waitForFinished(int msecs) override; + signals: private: + amnezia::PermittedProcess m_program = amnezia::PermittedProcess::Invalid; QSharedPointer m_process; }; diff --git a/ipc/ipctun2socksprocess.cpp b/ipc/ipctun2socksprocess.cpp deleted file mode 100644 index 65d801b0..00000000 --- a/ipc/ipctun2socksprocess.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "ipctun2socksprocess.h" -#include "ipc.h" -#include -#include - -#include "../protocols/protocols_defs.h" - -#ifndef Q_OS_IOS - -IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) : - IpcProcessTun2SocksSource(parent), - m_t2sProcess(QSharedPointer(new QProcess())) -{ - qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()"; - -} - -IpcProcessTun2Socks::~IpcProcessTun2Socks() -{ - qDebug() << "IpcProcessTun2Socks::~IpcProcessTun2Socks()"; -} - -void IpcProcessTun2Socks::start() -{ - connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged); - qDebug() << "IpcProcessTun2Socks::start()"; - m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast(amnezia::PermittedProcess::Tun2Socks))); - - QString XrayConStr = "socks5://127.0.0.1:10808"; - -#ifdef Q_OS_WIN - QStringList arguments({"-device", "tun://tun2?guid={081A8A84-8D12-4DF5-B8C4-396D5B0053E4}", "-proxy", XrayConStr, "-tun-post-up", - QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255") - .arg(amnezia::protocols::xray::defaultLocalAddr)}); -#endif -#ifdef Q_OS_LINUX - QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr}); -#endif -#ifdef Q_OS_MAC - QStringList arguments({"-device", "utun22", "-proxy", XrayConStr}); -#endif - - m_t2sProcess->setArguments(arguments); - - if (Utils::processIsRunning(Utils::executable("tun2socks", false))) { - qDebug().noquote() << "kill previos tun2socks"; - Utils::killProcessByName(Utils::executable("tun2socks", false)); - } - - m_t2sProcess->start(); - - connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() { - QString line = m_t2sProcess.data()->readAllStandardOutput(); - if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) { - emit setConnectionState(Vpn::ConnectionState::Connected); - } - }); - - connect(m_t2sProcess.data(), QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { - qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus; - emit setConnectionState(Vpn::ConnectionState::Disconnected); - if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { - emit setConnectionState(Vpn::ConnectionState::Error); - } - - }); - - m_t2sProcess->start(); - m_t2sProcess->waitForStarted(); -} - -void IpcProcessTun2Socks::stop() -{ - qDebug() << "IpcProcessTun2Socks::stop()"; - m_t2sProcess->disconnect(); - m_t2sProcess->kill(); - m_t2sProcess->waitForFinished(3000); -} -#endif diff --git a/ipc/ipctun2socksprocess.h b/ipc/ipctun2socksprocess.h deleted file mode 100644 index 8ce9be1a..00000000 --- a/ipc/ipctun2socksprocess.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef IPCTUN2SOCKSPROCESS_H -#define IPCTUN2SOCKSPROCESS_H - -#include - -#ifndef Q_OS_IOS -#include "rep_ipc_process_tun2socks_source.h" - -namespace Vpn -{ -Q_NAMESPACE - enum ConnectionState { - Unknown, - Disconnected, - Preparing, - Connecting, - Connected, - Disconnecting, - Reconnecting, - Error - }; -Q_ENUM_NS(ConnectionState) -} - - -class IpcProcessTun2Socks : public IpcProcessTun2SocksSource -{ - Q_OBJECT -public: - explicit IpcProcessTun2Socks(QObject *parent = nullptr); - virtual ~IpcProcessTun2Socks(); - - void start() override; - void stop() override; - -signals: - -private: - QSharedPointer m_t2sProcess; -}; - -#else -class IpcProcessTun2Socks : public QObject -{ - Q_OBJECT - -public: - explicit IpcProcessTun2Socks(QObject *parent = nullptr); -}; -#endif - -#endif // IPCTUN2SOCKSPROCESS_H diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index d1185bd3..05a3de37 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,7 +6,7 @@ project(${PROJECT}) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat Concurrent) qt_standard_project_setup() @@ -75,7 +75,6 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h - ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.h ${CMAKE_CURRENT_LIST_DIR}/localserver.h ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h ${CMAKE_CURRENT_LIST_DIR}/router.h @@ -97,7 +96,6 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp - ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.cpp ${CMAKE_CURRENT_LIST_DIR}/localserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp @@ -353,7 +351,7 @@ include_directories( add_executable(${PROJECT} ${SOURCES} ${HEADERS} ${RESOURCES}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus ${LIBS}) +target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus Qt6::Concurrent ${LIBS}) target_compile_definitions(${PROJECT} PRIVATE "MZ_$") if(CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -389,7 +387,6 @@ endif() qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep) qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep) -qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_tun2socks.rep) # copy deploy artifacts required to run the application to the debug build folder if(WIN32) diff --git a/service/server/killswitch.cpp b/service/server/killswitch.cpp index 6f77d8b3..49031b41 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(); } diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 0a24d9db..26706079 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -40,7 +40,6 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), if (!m_isRemotingEnabled) { m_isRemotingEnabled = true; m_serverNode.enableRemoting(&m_ipcServer); - m_serverNode.enableRemoting(&m_tun2socks); } }); @@ -51,8 +50,8 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), } m_networkWatcher.initialize(); - connect(&m_networkWatcher, &NetworkWatcher::sleepMode, &m_ipcServer, &IpcServer::networkChange); - connect(&m_networkWatcher, &NetworkWatcher::networkChange, &m_ipcServer, &IpcServer::networkChange); + connect(&m_networkWatcher, &NetworkWatcher::networkChanged, &m_ipcServer, &IpcServer::networkChanged); + connect(&m_networkWatcher, &NetworkWatcher::wakeup, &m_ipcServer, &IpcServer::wakeup); KillSwitch::instance()->init(); #ifdef Q_OS_LINUX diff --git a/service/server/localserver.h b/service/server/localserver.h index 47a7640d..3a885904 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -38,7 +38,6 @@ public: ~LocalServer(); QSharedPointer m_server; IpcServer m_ipcServer; - IpcProcessTun2Socks m_tun2socks; QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; diff --git a/service/server/router.cpp b/service/server/router.cpp index 24e82c0e..8849d27b 100644 --- a/service/server/router.cpp +++ b/service/server/router.cpp @@ -66,6 +66,9 @@ void Router::resetIpStack() bool Router::createTun(const QString &dev, const QString &subnet) { +#ifdef Q_OS_WIN + return RouterWin::Instance().createTun(dev, subnet); +#endif #ifdef Q_OS_LINUX return RouterLinux::Instance().createTun(dev, subnet); #endif diff --git a/service/server/router_win.cpp b/service/server/router_win.cpp index 7ad6505d..6cb2e38e 100644 --- a/service/server/router_win.cpp +++ b/service/server/router_win.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -308,6 +309,77 @@ void RouterWin::resetIpStack() } } +bool RouterWin::createTun(const QString &dev, const QString &subnet) +{ + NET_LUID luid; + DWORD res = ConvertInterfaceAliasToLuid(reinterpret_cast(dev.utf16()), &luid); + if (res != NO_ERROR) { + qCritical() << "Failed to convert luid: " << res; + return false; + } + + HANDLE hEvent = CreateEvent(nullptr, true, false, nullptr); + if (!hEvent) { + qCritical() << "Failed to allocate event object"; + return false; + } + auto _guardEvent = qScopeGuard([hEvent](){ CloseHandle(hEvent); }); + + struct { + HANDLE hEvent; + NET_LUID luid; + const QString &subnet; + bool found; + } ctx = { .hEvent = hEvent, .luid = luid, .subnet = subnet, .found = false }; + + auto cb = [](void *priv, MIB_UNICASTIPADDRESS_ROW *row, MIB_NOTIFICATION_TYPE NotificationType) { + auto* c = reinterpret_cast(priv); + if (row != nullptr && row->InterfaceLuid.Value == c->luid.Value && row->Address.si_family == AF_INET) { + char ip[INET_ADDRSTRLEN]; + inet_ntop(row->Address.Ipv4.sin_family, &row->Address.Ipv4.sin_addr, ip, INET_ADDRSTRLEN); + if (c->subnet == ip) { + c->found = true; + SetEvent(c->hEvent); + } + } + }; + + HANDLE hNotif; + res = NotifyUnicastIpAddressChange(AF_INET, cb, &ctx, false, &hNotif); + if (res != NO_ERROR) { + qCritical() << "Failed to subscribe to interface change"; + return false; + } + auto _guardNotif = qScopeGuard([hNotif](){ CancelMibChangeNotify2(hNotif); }); + + MIB_UNICASTIPADDRESS_ROW row; + InitializeUnicastIpAddressEntry(&row); + + row.InterfaceLuid = luid; + row.Address.si_family = AF_INET; + + inet_pton(AF_INET, subnet.toStdString().c_str(), &row.Address.Ipv4.sin_addr); + + row.OnLinkPrefixLength = 32; + row.ValidLifetime = 0xffffffff; + row.PreferredLifetime = 0xffffffff; + row.DadState = IpDadStatePreferred; + + res = CreateUnicastIpAddressEntry(&row); + if (res != NO_ERROR && res != ERROR_OBJECT_ALREADY_EXISTS) { + qDebug() << "Failed to create IP address:" << res; + return false; + } + + res = WaitForSingleObject(hEvent, 10000); + if (res == WAIT_TIMEOUT) { + qCritical() << "Timeout of waiting for IP assignment for " << dev << " device"; + return false; + } + + return ctx.found; +} + void RouterWin::suspendWcmSvc(bool suspend) { if (suspend == m_suspended) return; @@ -465,11 +537,19 @@ bool RouterWin::StopRoutingIpv6() qDebug() << "RouterWin::StopRoutingIpv6"; if (auto loopback = findLoopbackIface(); loopback.isValid()) { - for (auto subnet : kIpv6Subnets) { - QProcess{}.execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" }); - } + QFuture res = QtConcurrent::mappedReduced(kIpv6Subnets, [loopback](const QString &subnet) -> bool { + int res = QProcess::execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" }); + return res == 0; + }, + [](bool &result, bool success) { + result = result && success; + }, true); + + res.waitForFinished(); + return res.result(); } - return true; + + return false; } bool RouterWin::StartRoutingIpv6() @@ -477,9 +557,14 @@ bool RouterWin::StartRoutingIpv6() qDebug() << "RouterWin::StartRoutingIpv6"; if (auto loopback = findLoopbackIface(); loopback.isValid()) { - for (auto subnet : kIpv6Subnets) { - QProcess{}.execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) }); - } + QFuture res = QtConcurrent::mappedReduced(kIpv6Subnets, [loopback](const QString &subnet) -> bool { + int res = QProcess::execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) }); + return res == 0; + }, + [](bool &result, bool success) { + result = result && success; + }, true); } - return true; + + return false; } diff --git a/service/server/router_win.h b/service/server/router_win.h index c2412cc5..9258b734 100644 --- a/service/server/router_win.h +++ b/service/server/router_win.h @@ -45,6 +45,7 @@ public: bool StartRoutingIpv6(); bool StopRoutingIpv6(); + bool createTun(const QString &dev, const QString &subnet); void suspendWcmSvc(bool suspend); bool updateResolvers(const QString& ifname, const QList& resolvers); bool restoreResolvers(); diff --git a/service/server/xray.cpp b/service/server/xray.cpp index 2d3f9a79..b0408d49 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -27,7 +27,7 @@ #include #endif -void Xray::startXray(const QString &cfg) +bool Xray::startXray(const QString &cfg) { qDebug() << "Xray::startXray()"; @@ -40,34 +40,38 @@ void Xray::startXray(const QString &cfg) if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) { qDebug() << "[xray] sockopt failed: " << err; - free(err); - return; - } - - QByteArray bytes = cfg.toUtf8(); - if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { - qDebug() << "[xray] configuration failed: " << err; - free(err); - return; + amnezia_xray_free(err); + return false; } amnezia_xray_setloghandler(ctxLogHandler, this); + QByteArray bytes = cfg.toUtf8(); + if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { + qDebug() << "[xray] configuration failed: " << err; + amnezia_xray_free(err); + return false; + } + if (auto err = amnezia_xray_start(); err != nullptr) { qDebug() << "[xray] failed to start: " << err; - free(err); - return; + amnezia_xray_free(err); + return false; } + + return true; } -void Xray::stopXray() +bool Xray::stopXray() { qDebug() << "Xray::stopXray()"; if (auto err = amnezia_xray_stop(); err != nullptr) { qDebug() << "[xray] failed to stop: " << err; - free(err); - return; + amnezia_xray_free(err); + return false; } + + return true; } void Xray::logHandler(char* str) diff --git a/service/server/xray.h b/service/server/xray.h index c199734a..f54d9902 100644 --- a/service/server/xray.h +++ b/service/server/xray.h @@ -12,8 +12,8 @@ public: return instance; } - void startXray(const QString& cfg); - void stopXray(); + bool startXray(const QString& cfg); + bool stopXray(); private: static void ctxSockCallback(uintptr_t fd, void* ctx) {