diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 905f34d1..92bf3dff 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -127,7 +127,6 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.h ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h - ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h ${CMAKE_CURRENT_LIST_DIR}/ui/pages.h ${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h @@ -156,6 +155,12 @@ if(NOT IOS) ) endif() +if(NOT ANDROID) + set(HEADERS ${HEADERS} + ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h + ) +endif() + set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/migrations.cpp ${CMAKE_CURRENT_LIST_DIR}/amnezia_application.cpp @@ -167,7 +172,6 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp ${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.cpp ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp @@ -192,6 +196,12 @@ if(NOT IOS) ) endif() +if(NOT ANDROID) + set(SOURCES ${SOURCES} + ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp + ) +endif() + file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.h) file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.cpp) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 804a6e03..06d2f9ac 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -143,6 +143,7 @@ void AmneziaApplication::init() connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); }); #endif +#ifndef Q_OS_ANDROID m_notificationHandler.reset(NotificationHandler::create(nullptr)); connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), @@ -154,6 +155,7 @@ void AmneziaApplication::init() connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), &ConnectionController::closeConnection); connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); +#endif m_engine->load(url); m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 73fb31b0..5561d7c7 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -27,7 +27,9 @@ #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" #include "ui/models/protocols/cloakConfigModel.h" -#include "ui/notificationhandler.h" +#ifndef Q_OS_ANDROID + #include "ui/notificationhandler.h" +#endif #ifdef Q_OS_WINDOWS #include "ui/models/protocols/ikev2ConfigModel.h" #endif @@ -115,7 +117,9 @@ private: QSharedPointer m_vpnConnection; QThread m_vpnConnectionThread; +#ifndef Q_OS_ANDROID QScopedPointer m_notificationHandler; +#endif QScopedPointer m_connectionController; QScopedPointer m_pageController; diff --git a/client/android/cloak/src/main/kotlin/Cloak.kt b/client/android/cloak/src/main/kotlin/Cloak.kt index 651e353b..5a549130 100644 --- a/client/android/cloak/src/main/kotlin/Cloak.kt +++ b/client/android/cloak/src/main/kotlin/Cloak.kt @@ -3,9 +3,6 @@ package org.amnezia.vpn.protocol.cloak import android.util.Base64 import net.openvpn.ovpn3.ClientAPI_Config import org.amnezia.vpn.protocol.openvpn.OpenVpn -import org.amnezia.vpn.protocol.openvpn.OpenVpnConfig -import org.amnezia.vpn.util.net.InetNetwork -import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject /** @@ -54,13 +51,6 @@ class Cloak : OpenVpn() { return openVpnConfig } - override fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) { - // exclude remote server ip from vpn routes - val remoteServer = config.getString("hostName") - val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer)) - configBuilder.excludeRoute(remoteServerAddress) - } - private fun checkCloakJson(cloakConfigJson: JSONObject): JSONObject { cloakConfigJson.put("NumConn", 1) cloakConfigJson.put("ProxyMethod", "openvpn") diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index 1cf763c8..e36fdefc 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -13,7 +13,9 @@ import org.amnezia.vpn.protocol.ProtocolState import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.VpnStartException +import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.getLocalNetworks +import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject /** @@ -77,6 +79,12 @@ open class OpenVpn : Protocol() { if (evalConfig.error) { throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}") } + + // exclude remote server ip from vpn routes + val remoteServer = config.getString("hostName") + val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer)) + configBuilder.excludeRoute(remoteServerAddress) + configPluggableTransport(configBuilder, config) configBuilder.configSplitTunneling(config) configBuilder.configAppSplitTunneling(config) diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index ce4a1ed6..e51d0fc1 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -105,15 +105,17 @@ abstract class Protocol { vpnBuilder.addSearchDomain(it) } - for (addr in config.routes) { - Log.d(TAG, "addRoute: $addr") - vpnBuilder.addRoute(addr) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - for (addr in config.excludedRoutes) { - Log.d(TAG, "excludeRoute: $addr") - vpnBuilder.excludeRoute(addr) + for ((inetNetwork, include) in config.routes) { + if (include) { + Log.d(TAG, "addRoute: $inetNetwork") + vpnBuilder.addRoute(inetNetwork) + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Log.d(TAG, "excludeRoute: $inetNetwork") + vpnBuilder.excludeRoute(inetNetwork) + } else { + Log.e(TAG, "Trying to exclude route $inetNetwork on old Android") + } } } diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt index 5731de6c..bebcea4c 100644 --- a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt +++ b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt @@ -12,8 +12,7 @@ open class ProtocolConfig protected constructor( val addresses: Set, val dnsServers: Set, val searchDomain: String?, - val routes: Set, - val excludedRoutes: Set, + val routes: Set, val includedAddresses: Set, val excludedAddresses: Set, val includedApplications: Set, @@ -29,7 +28,6 @@ open class ProtocolConfig protected constructor( builder.dnsServers, builder.searchDomain, builder.routes, - builder.excludedRoutes, builder.includedAddresses, builder.excludedAddresses, builder.includedApplications, @@ -43,8 +41,7 @@ open class ProtocolConfig protected constructor( open class Builder(blockingMode: Boolean) { internal val addresses: MutableSet = hashSetOf() internal val dnsServers: MutableSet = hashSetOf() - internal val routes: MutableSet = hashSetOf() - internal val excludedRoutes: MutableSet = hashSetOf() + internal val routes: MutableSet = mutableSetOf() internal val includedAddresses: MutableSet = hashSetOf() internal val excludedAddresses: MutableSet = hashSetOf() internal val includedApplications: MutableSet = hashSetOf() @@ -77,13 +74,21 @@ open class ProtocolConfig protected constructor( fun setSearchDomain(domain: String) = apply { this.searchDomain = domain } - fun addRoute(route: InetNetwork) = apply { this.routes += route } - fun addRoutes(routes: Collection) = apply { this.routes += routes } - fun removeRoute(route: InetNetwork) = apply { this.routes.remove(route) } + fun addRoute(route: InetNetwork) = apply { this.routes += Route(route, true) } + fun addRoutes(routes: Collection) = apply { this.routes += routes.map { Route(it, true) } } + + fun excludeRoute(route: InetNetwork) = apply { this.routes += Route(route, false) } + fun excludeRoutes(routes: Collection) = apply { this.routes += routes.map { Route(it, false) } } + + fun removeRoute(route: InetNetwork) = apply { this.routes.removeIf { it.inetNetwork == route } } fun clearRoutes() = apply { this.routes.clear() } - fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route } - fun excludeRoutes(routes: Collection) = apply { this.excludedRoutes += routes } + fun prependRoutes(block: Builder.() -> Unit) = apply { + val savedRoutes = mutableListOf().apply { addAll(routes) } + routes.clear() + block() + routes.addAll(savedRoutes) + } fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr } fun includeAddresses(addresses: Collection) = apply { this.includedAddresses += addresses } @@ -117,37 +122,46 @@ open class ProtocolConfig protected constructor( // remove default routes, if any removeRoute(InetNetwork("0.0.0.0", 0)) removeRoute(InetNetwork("::", 0)) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - // for older versions of Android, add the default route to the excluded routes - // to correctly build the excluded subnets list later - excludeRoute(InetNetwork("0.0.0.0", 0)) + removeRoute(InetNetwork("2000::", 3)) + prependRoutes { + addRoutes(includedAddresses) } - addRoutes(includedAddresses) } else if (excludedAddresses.isNotEmpty()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // default routes are required for split tunneling in newer versions of Android + prependRoutes { addRoute(InetNetwork("0.0.0.0", 0)) - addRoute(InetNetwork("::", 0)) + addRoute(InetNetwork("2000::", 3)) + excludeRoutes(excludedAddresses) } - excludeRoutes(excludedAddresses) } } - private fun processExcludedRoutes() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && excludedRoutes.isNotEmpty()) { - // todo: rewrite, taking into account the current routes - // for older versions of Android, build a list of subnets without excluded routes - // and add them to routes - val ipRangeSet = IpRangeSet() - ipRangeSet.remove(IpRange("127.0.0.0", 8)) - excludedRoutes.forEach { - ipRangeSet.remove(IpRange(it)) + private fun processRoutes() { + // replace ::/0 as it may cause LAN connection issues + val ipv6DefaultRoute = InetNetwork("::", 0) + if (routes.removeIf { it.include && it.inetNetwork == ipv6DefaultRoute }) { + prependRoutes { + addRoute(InetNetwork("2000::", 3)) } - // remove default routes, if any - removeRoute(InetNetwork("0.0.0.0", 0)) - removeRoute(InetNetwork("::", 0)) + } + // for older versions of Android, build a list of subnets without excluded routes + // and add them to routes + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && routes.any { !it.include }) { + val ipRangeSet = IpRangeSet() + routes.forEach { + if (it.include) ipRangeSet.add(IpRange(it.inetNetwork)) + else ipRangeSet.remove(IpRange(it.inetNetwork)) + } + ipRangeSet.remove(IpRange("127.0.0.0", 8)) + ipRangeSet.remove(IpRange("::1", 128)) + routes.clear() ipRangeSet.subnets().forEach(::addRoute) - addRoute(InetNetwork("2000::", 3)) + } + // filter ipv4 and ipv6 loopback addresses + val ipv6Loopback = InetNetwork("::1", 128) + routes.removeIf { + it.include && + if (it.inetNetwork.isIpv4) it.inetNetwork.address.address[0] == 127.toByte() + else it.inetNetwork == ipv6Loopback } } @@ -165,7 +179,7 @@ open class ProtocolConfig protected constructor( protected fun configBuild() { processSplitTunneling() - processExcludedRoutes() + processRoutes() validate() } @@ -177,3 +191,5 @@ open class ProtocolConfig protected constructor( Builder(blockingMode).apply(block).build() } } + +data class Route(val inetNetwork: InetNetwork, val include: Boolean) diff --git a/client/android/res/values-ru/strings.xml b/client/android/res/values-ru/strings.xml index 53cb9c45..8bdabfc0 100644 --- a/client/android/res/values-ru/strings.xml +++ b/client/android/res/values-ru/strings.xml @@ -1,12 +1,26 @@ - Подключение - Отключение - Отмена + Не подключено + Подключено + Подключение… + Отключение… + Переподключение… + Подключиться + Отключиться ОК + Отмена + Да + Нет + VPN-подключение разрешено - VPN-подключение запрещено Ошибка настройки VPN Чтобы подключиться к AmneziaVPN необходимо:\n\n- Разрешить приложению подключаться к сети VPN\n- Отключить функцию \"Постоянная VPN\" для всех остальных VPN-приложений в системных настройках VPN Открыть настройки VPN + + Уведомления сервиса AmneziaVPN + Сервис AmneziaVPN + Показывать статус VPN в строке состояния? + Настройки уведомлений + Для показа уведомлений необходимо включить уведомления в системных настройках + Открыть настройки уведомлений \ No newline at end of file diff --git a/client/android/res/values/strings.xml b/client/android/res/values/strings.xml index 9172d14b..5251403b 100644 --- a/client/android/res/values/strings.xml +++ b/client/android/res/values/strings.xml @@ -1,12 +1,26 @@ - Connecting - Disconnecting - Cancel + Not connected + Connected + Connecting… + Disconnecting… + Reconnecting… + Connect + Disconnect OK + Cancel + Yes + No + VPN permission granted - VPN permission denied VPN setup error To connect to AmneziaVPN, please do the following:\n\n- Allow the app to set up a VPN connection\n- Disable Always-on VPN for any other VPN app in the VPN system settings Open VPN settings + + AmneziaVPN service notification + AmneziaVPN service + Show the VPN state in the status bar? + Notification settings + To show notifications, you must enable notifications in the system settings + Open notification settings \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 8e71f136..c9063f22 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -1,6 +1,9 @@ package org.amnezia.vpn +import android.Manifest import android.app.AlertDialog +import android.app.NotificationManager +import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Intent import android.content.Intent.EXTRA_MIME_TYPES @@ -8,8 +11,8 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY import android.content.ServiceConnection import android.content.pm.PackageManager import android.graphics.Bitmap -import android.net.Uri import android.net.VpnService +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.IBinder @@ -21,6 +24,7 @@ import android.view.WindowManager.LayoutParams import android.webkit.MimeTypeMap import android.widget.Toast import androidx.annotation.MainThread +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import java.io.IOException import kotlin.LazyThreadSafetyMode.NONE @@ -38,6 +42,7 @@ import org.amnezia.vpn.protocol.getStatistics import org.amnezia.vpn.protocol.getStatus import org.amnezia.vpn.qt.QtAndroidController import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.Prefs import org.qtproject.qt.android.bindings.QtActivity private const val TAG = "AmneziaActivity" @@ -46,6 +51,9 @@ const val ACTIVITY_MESSENGER_NAME = "Activity" private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1 private const val CREATE_FILE_ACTION_CODE = 2 private const val OPEN_FILE_ACTION_CODE = 3 +private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4 + +private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED" class AmneziaActivity : QtActivity() { @@ -54,8 +62,11 @@ class AmneziaActivity : QtActivity() { private var isWaitingStatus = true private var isServiceConnected = false private var isInBoundState = false + private var notificationStateReceiver: BroadcastReceiver? = null private lateinit var vpnServiceMessenger: IpcMessenger - private var tmpFileContentToSave: String = "" + + private val actionResultHandlers = mutableMapOf() + private val permissionRequestHandlers = mutableMapOf() private val vpnServiceEventHandler: Handler by lazy(NONE) { object : Handler(Looper.getMainLooper()) { @@ -135,10 +146,6 @@ class AmneziaActivity : QtActivity() { } } - private data class CheckVpnPermissionCallbacks(val onSuccess: () -> Unit, val onFail: () -> Unit) - - private var checkVpnPermissionCallbacks: CheckVpnPermissionCallbacks? = null - /** * Activity overloaded methods */ @@ -153,9 +160,30 @@ class AmneziaActivity : QtActivity() { doBindService() } ) + registerBroadcastReceivers() intent?.let(::processIntent) } + private fun registerBroadcastReceivers() { + notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + registerBroadcastReceiver( + arrayOf( + NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED, + NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED + ) + ) { + Log.d( + TAG, "Notification state changed: ${it?.action}, blocked = " + + "${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}" + ) + mainScope.launch { + qtInitialized.await() + QtAndroidController.onNotificationStateChanged() + } + } + } else null + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) Log.d(TAG, "onNewIntent: $intent") @@ -193,50 +221,46 @@ class AmneziaActivity : QtActivity() { override fun onDestroy() { Log.d(TAG, "Destroy Amnezia activity") + unregisterBroadcastReceiver(notificationStateReceiver) + notificationStateReceiver = null mainScope.cancel() super.onDestroy() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - CREATE_FILE_ACTION_CODE -> { - when (resultCode) { - RESULT_OK -> { - data?.data?.let { uri -> - alterDocument(uri) - } - } - } + Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " + + "resultCode: $resultCode, data: $data") + actionResultHandlers[requestCode]?.let { handler -> + when (resultCode) { + RESULT_OK -> handler.onSuccess(data) + else -> handler.onFail(data) } + handler.onAny(data) + actionResultHandlers.remove(requestCode) + } ?: super.onActivityResult(requestCode, resultCode, data) + } - OPEN_FILE_ACTION_CODE -> { - when (resultCode) { - RESULT_OK -> data?.data?.toString() ?: "" - else -> "" - }.let { uri -> - QtAndroidController.onFileOpened(uri) - } + private fun startActivityForResult(intent: Intent, requestCode: Int, handler: ActivityResultHandler) { + actionResultHandlers[requestCode] = handler + startActivityForResult(intent, requestCode) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + Log.d(TAG, "Process permission result, code: ${actionCodeToString(requestCode)}, " + + "permissions: ${permissions.contentToString()}, results: ${grantResults.contentToString()}") + permissionRequestHandlers[requestCode]?.let { handler -> + if (grantResults.isNotEmpty()) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) handler.onSuccess() + else handler.onFail() } + handler.onAny() + permissionRequestHandlers.remove(requestCode) + } ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } - CHECK_VPN_PERMISSION_ACTION_CODE -> { - when (resultCode) { - RESULT_OK -> { - Log.d(TAG, "Vpn permission granted") - Toast.makeText(this, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show() - checkVpnPermissionCallbacks?.run { onSuccess() } - } - - else -> { - Log.w(TAG, "Vpn permission denied, resultCode: $resultCode") - showOnVpnPermissionRejectDialog() - checkVpnPermissionCallbacks?.run { onFail() } - } - } - checkVpnPermissionCallbacks = null - } - - else -> super.onActivityResult(requestCode, resultCode, data) - } + private fun requestPermission(permission: String, requestCode: Int, handler: PermissionRequestHandler) { + permissionRequestHandlers[requestCode] = handler + requestPermissions(arrayOf(permission), requestCode) } /** @@ -268,22 +292,26 @@ class AmneziaActivity : QtActivity() { /** * Methods of starting and stopping VpnService */ - private fun checkVpnPermissionAndStart(vpnConfig: String) { - checkVpnPermission( - onSuccess = { startVpn(vpnConfig) }, - onFail = QtAndroidController::onVpnPermissionRejected - ) - } - @MainThread - private fun checkVpnPermission(onSuccess: () -> Unit, onFail: () -> Unit) { + private fun checkVpnPermission(onPermissionGranted: () -> Unit) { Log.d(TAG, "Check VPN permission") - VpnService.prepare(applicationContext)?.let { - checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail) - startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE) - return - } - onSuccess() + VpnService.prepare(applicationContext)?.let { intent -> + startActivityForResult(intent, CHECK_VPN_PERMISSION_ACTION_CODE, ActivityResultHandler( + onSuccess = { + Log.d(TAG, "Vpn permission granted") + Toast.makeText(this@AmneziaActivity, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show() + onPermissionGranted() + }, + onFail = { + Log.w(TAG, "Vpn permission denied") + showOnVpnPermissionRejectDialog() + mainScope.launch { + qtInitialized.await() + QtAndroidController.onVpnPermissionRejected() + } + } + )) + } ?: onPermissionGranted() } private fun showOnVpnPermissionRejectDialog() { @@ -297,6 +325,44 @@ class AmneziaActivity : QtActivity() { .show() } + private fun checkNotificationPermission(onChecked: () -> Unit) { + Log.d(TAG, "Check notification permission") + if ( + !isNotificationPermissionGranted() && + !Prefs.load(PREFS_NOTIFICATION_PERMISSION_ASKED) + ) { + showNotificationPermissionDialog(onChecked) + } else { + onChecked() + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun showNotificationPermissionDialog(onChecked: () -> Unit) { + AlertDialog.Builder(this) + .setTitle(R.string.notificationDialogTitle) + .setMessage(R.string.notificationDialogMessage) + .setNegativeButton(R.string.no) { _, _ -> + Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true) + onChecked() + } + .setPositiveButton(R.string.yes) { _, _ -> + val saveAsked: () -> Unit = { + Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true) + } + requestPermission( + Manifest.permission.POST_NOTIFICATIONS, + CHECK_NOTIFICATION_PERMISSION_ACTION_CODE, + PermissionRequestHandler( + onSuccess = saveAsked, + onFail = saveAsked, + onAny = onChecked + ) + ) + } + .show() + } + @MainThread private fun startVpn(vpnConfig: String) { if (isServiceConnected) { @@ -322,28 +388,21 @@ class AmneziaActivity : QtActivity() { Intent(this, AmneziaVpnService::class.java).apply { putExtra(MSG_VPN_CONFIG, vpnConfig) }.also { - ContextCompat.startForegroundService(this, it) + try { + ContextCompat.startForegroundService(this, it) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to start AmneziaVpnService: $e") + QtAndroidController.onServiceError() + } } } + @MainThread private fun disconnectFromVpn() { Log.d(TAG, "Disconnect from VPN") vpnServiceMessenger.send(Action.DISCONNECT) } - // saving file - private fun alterDocument(uri: Uri) { - try { - contentResolver.openOutputStream(uri)?.use { os -> - os.bufferedWriter().use { it.write(tmpFileContentToSave) } - } - } catch (e: IOException) { - e.printStackTrace() - } - - tmpFileContentToSave = "" - } - /** * Methods called by Qt */ @@ -357,7 +416,11 @@ class AmneziaActivity : QtActivity() { fun start(vpnConfig: String) { Log.v(TAG, "Start VPN") mainScope.launch { - checkVpnPermissionAndStart(vpnConfig) + checkVpnPermission { + checkNotificationPermission { + startVpn(vpnConfig) + } + } } } @@ -389,14 +452,26 @@ class AmneziaActivity : QtActivity() { fun saveFile(fileName: String, data: String) { Log.d(TAG, "Save file $fileName") mainScope.launch { - tmpFileContentToSave = data - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "text/*" putExtra(Intent.EXTRA_TITLE, fileName) }.also { - startActivityForResult(it, CREATE_FILE_ACTION_CODE) + startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( + onSuccess = { + it?.data?.let { uri -> + Log.d(TAG, "Save file to $uri") + try { + contentResolver.openOutputStream(uri)?.use { os -> + os.bufferedWriter().use { it.write(data) } + } + } catch (e: IOException) { + Log.e(TAG, "Failed to save file $uri: $e") + // todo: send error to Qt + } + } + } + )) } } } @@ -404,42 +479,47 @@ class AmneziaActivity : QtActivity() { @Suppress("unused") fun openFile(filter: String?) { Log.v(TAG, "Open file with filter: $filter") + mainScope.launch { + val mimeTypes = if (!filter.isNullOrEmpty()) { + val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) + val mime = MimeTypeMap.getSingleton() + extensionRegex.findAll(filter).map { + it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*" + }.toSet() + } else emptySet() - val mimeTypes = if (!filter.isNullOrEmpty()) { - val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) - val mime = MimeTypeMap.getSingleton() - extensionRegex.findAll(filter).map { - it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*" - }.toSet() - } else emptySet() + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + Log.v(TAG, "File mimyType filter: $mimeTypes") + if ("*/*" in mimeTypes) { + type = "*/*" + } else { + when (mimeTypes.size) { + 1 -> type = mimeTypes.first() - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - Log.v(TAG, "File mimyType filter: $mimeTypes") - if ("*/*" in mimeTypes) { - type = "*/*" - } else { - when (mimeTypes.size) { - 1 -> type = mimeTypes.first() + in 2..Int.MAX_VALUE -> { + type = "*/*" + putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + } - in 2..Int.MAX_VALUE -> { - type = "*/*" - putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + else -> type = "*/*" } - - else -> type = "*/*" } + }.also { + startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( + onSuccess = { + val uri = it?.data?.toString() ?: "" + Log.d(TAG, "Open file: $uri") + mainScope.launch { + qtInitialized.await() + QtAndroidController.onFileOpened(uri) + } + } + )) } - }.also { - startActivityForResult(it, OPEN_FILE_ACTION_CODE) } } - @Suppress("unused") - fun setNotificationText(title: String, message: String, timerSec: Int) { - Log.v(TAG, "Set notification text") - } - @Suppress("unused") fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) @@ -514,4 +594,76 @@ class AmneziaActivity : QtActivity() { Log.v(TAG, "Get app icon") return AppListProvider.getAppIcon(packageManager, packageName, width, height) } + + @Suppress("unused") + fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted() + + @Suppress("unused") + fun requestNotificationPermission() { + val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) + requestPermission( + Manifest.permission.POST_NOTIFICATIONS, + CHECK_NOTIFICATION_PERMISSION_ACTION_CODE, + PermissionRequestHandler( + onSuccess = { + mainScope.launch { + Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true) + vpnServiceMessenger.send(Action.NOTIFICATION_PERMISSION_GRANTED) + qtInitialized.await() + QtAndroidController.onNotificationStateChanged() + } + }, + onFail = { + if (!Prefs.load(PREFS_NOTIFICATION_PERMISSION_ASKED)) { + Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true) + } else { + val shouldShowPostRequest = + shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) + if (!shouldShowPreRequest && !shouldShowPostRequest) { + showNotificationSettingsDialog() + } + } + } + ) + ) + } + + private fun showNotificationSettingsDialog() { + AlertDialog.Builder(this) + .setTitle(R.string.notificationSettingsDialogTitle) + .setMessage(R.string.notificationSettingsDialogMessage) + .setNegativeButton(R.string.cancel) { _, _ -> } + .setPositiveButton(R.string.openNotificationSettings) { _, _ -> + startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + }) + } + .show() + } + + /** + * Utils methods + */ + companion object { + private fun actionCodeToString(actionCode: Int): String = + when (actionCode) { + CHECK_VPN_PERMISSION_ACTION_CODE -> "CHECK_VPN_PERMISSION" + CREATE_FILE_ACTION_CODE -> "CREATE_FILE" + OPEN_FILE_ACTION_CODE -> "OPEN_FILE" + CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION" + else -> actionCode.toString() + } + } } + +private class ActivityResultHandler( + val onSuccess: (data: Intent?) -> Unit = {}, + val onFail: (data: Intent?) -> Unit = {}, + val onAny: (data: Intent?) -> Unit = {} +) + +private class PermissionRequestHandler( + val onSuccess: () -> Unit = {}, + val onFail: () -> Unit = {}, + val onAny: () -> Unit = {} +) diff --git a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt index d8c87bd6..8b066056 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt @@ -3,14 +3,11 @@ package org.amnezia.vpn import androidx.camera.camera2.Camera2Config import androidx.camera.core.CameraSelector import androidx.camera.core.CameraXConfig -import androidx.core.app.NotificationChannelCompat.Builder -import androidx.core.app.NotificationManagerCompat import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Prefs import org.qtproject.qt.android.bindings.QtApplication private const val TAG = "AmneziaApplication" -const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification" class AmneziaApplication : QtApplication(), CameraXConfig.Provider { @@ -20,7 +17,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider { Log.init(this) VpnStateStore.init(this) Log.d(TAG, "Create Amnezia application") - createNotificationChannel() + ServiceNotification.createNotificationChannel(this) } override fun getCameraXConfig(): CameraXConfig = CameraXConfig.Builder @@ -28,14 +25,4 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider { .setMinimumLoggingLevel(android.util.Log.ERROR) .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA) .build() - - private fun createNotificationChannel() { - NotificationManagerCompat.from(this).createNotificationChannel( - Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) - .setName("AmneziaVPN") - .setDescription("AmneziaVPN service notification") - .setShowBadge(false) - .build() - ) - } } diff --git a/client/android/src/org/amnezia/vpn/AmneziaContext.kt b/client/android/src/org/amnezia/vpn/AmneziaContext.kt new file mode 100644 index 00000000..3f2104b5 --- /dev/null +++ b/client/android/src/org/amnezia/vpn/AmneziaContext.kt @@ -0,0 +1,56 @@ +package org.amnezia.vpn + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.RegisterReceiverFlags +import org.amnezia.vpn.protocol.ProtocolState +import org.amnezia.vpn.protocol.ProtocolState.CONNECTED +import org.amnezia.vpn.protocol.ProtocolState.CONNECTING +import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED +import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING +import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING +import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN + +fun Context.getString(state: ProtocolState): String = + getString( + when (state) { + DISCONNECTED, UNKNOWN -> R.string.disconnected + CONNECTED -> R.string.connected + CONNECTING -> R.string.connecting + DISCONNECTING -> R.string.disconnecting + RECONNECTING -> R.string.reconnecting + } + ) + +fun Context.registerBroadcastReceiver( + action: String, + @RegisterReceiverFlags flags: Int = ContextCompat.RECEIVER_EXPORTED, + onReceive: (Intent?) -> Unit +): BroadcastReceiver = registerBroadcastReceiver(arrayOf(action), flags, onReceive) + +fun Context.registerBroadcastReceiver( + actions: Array, + @RegisterReceiverFlags flags: Int = ContextCompat.RECEIVER_EXPORTED, + onReceive: (Intent?) -> Unit +): BroadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + onReceive(intent) + } + }.also { + ContextCompat.registerReceiver( + this, + it, + IntentFilter().apply { + actions.forEach(::addAction) + }, + flags + ) + } + +fun Context.unregisterBroadcastReceiver(receiver: BroadcastReceiver?) { + receiver?.let { this.unregisterReceiver(it) } +} diff --git a/client/android/src/org/amnezia/vpn/AmneziaTileService.kt b/client/android/src/org/amnezia/vpn/AmneziaTileService.kt index 5ad872a0..1d13feac 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaTileService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaTileService.kt @@ -188,11 +188,16 @@ class AmneziaTileService : TileService() { true } - private fun startVpnService() = - ContextCompat.startForegroundService( - applicationContext, - Intent(this, AmneziaVpnService::class.java) - ) + private fun startVpnService() { + try { + ContextCompat.startForegroundService( + applicationContext, + Intent(this, AmneziaVpnService::class.java) + ) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to start AmneziaVpnService: $e") + } + } private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT) @@ -230,7 +235,7 @@ class AmneziaTileService : TileService() { val tile = qsTile ?: return tile.apply { label = vpnState.serverName ?: DEFAULT_TILE_LABEL - when (vpnState.protocolState) { + when (val protocolState = vpnState.protocolState) { CONNECTED -> { state = Tile.STATE_ACTIVE subtitleCompat = null @@ -241,14 +246,9 @@ class AmneziaTileService : TileService() { subtitleCompat = null } - CONNECTING, RECONNECTING -> { + CONNECTING, DISCONNECTING, RECONNECTING -> { state = Tile.STATE_UNAVAILABLE - subtitleCompat = resources.getString(R.string.connecting) - } - - DISCONNECTING -> { - state = Tile.STATE_UNAVAILABLE - subtitleCompat = resources.getString(R.string.disconnecting) + subtitleCompat = getString(protocolState) } } updateTile() diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index d0a1e8e8..89c53481 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -2,8 +2,8 @@ package org.amnezia.vpn import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE -import android.app.Notification -import android.app.PendingIntent +import android.app.NotificationManager +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST @@ -15,10 +15,12 @@ import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.os.PowerManager import android.os.Process import androidx.annotation.MainThread -import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import java.util.concurrent.ConcurrentHashMap import kotlin.LazyThreadSafetyMode.NONE import kotlinx.coroutines.CoroutineExceptionHandler @@ -54,11 +56,14 @@ import org.amnezia.vpn.protocol.wireguard.Wireguard import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.net.NetworkState +import org.amnezia.vpn.util.net.TrafficStats import org.json.JSONException import org.json.JSONObject private const val TAG = "AmneziaVpnService" +const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect" + const val MSG_VPN_CONFIG = "VPN_CONFIG" const val MSG_ERROR = "ERROR" const val MSG_SAVE_LOGS = "SAVE_LOGS" @@ -69,8 +74,8 @@ private const val PREFS_CONFIG_KEY = "LAST_CONF" private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME" private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX" private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService" -private const val NOTIFICATION_ID = 1337 -private const val STATISTICS_SENDING_TIMEOUT = 1000L +// private const val STATISTICS_SENDING_TIMEOUT = 1000L +private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L private const val DISCONNECT_TIMEOUT = 5000L private const val STOP_SERVICE_TIMEOUT = 5000L @@ -96,8 +101,14 @@ class AmneziaVpnService : VpnService() { private var connectionJob: Job? = null private var disconnectionJob: Job? = null - private var statisticsSendingJob: Job? = null + private var trafficStatsUpdateJob: Job? = null + // private var statisticsSendingJob: Job? = null private lateinit var networkState: NetworkState + private lateinit var trafficStats: TrafficStats + private var disconnectReceiver: BroadcastReceiver? = null + private var notificationStateReceiver: BroadcastReceiver? = null + private var screenOnReceiver: BroadcastReceiver? = null + private var screenOffReceiver: BroadcastReceiver? = null private val clientMessengers = ConcurrentHashMap() private val isActivityConnected @@ -131,13 +142,13 @@ class AmneziaVpnService : VpnService() { val messenger = IpcMessenger(msg.replyTo, clientName) clientMessengers[msg.replyTo] = messenger Log.d(TAG, "Messenger client '$clientName' was registered") - if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics() + // if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics() } Action.UNREGISTER_CLIENT -> { clientMessengers.remove(msg.replyTo)?.let { Log.d(TAG, "Messenger client '${it.name}' was unregistered") - if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics() + // if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics() } } @@ -159,6 +170,10 @@ class AmneziaVpnService : VpnService() { } } + Action.NOTIFICATION_PERMISSION_GRANTED -> { + enableNotification() + } + Action.SET_SAVE_LOGS -> { Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS) } @@ -181,25 +196,7 @@ class AmneziaVpnService : VpnService() { else -> 0 } - private val notification: Notification by lazy(NONE) { - NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_amnezia_round) - .setShowWhen(false) - .setContentIntent( - PendingIntent.getActivity( - this, - 0, - Intent(this, AmneziaActivity::class.java), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - .setOngoing(true) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) - .build() - } + private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) } /** * Service overloaded methods @@ -212,6 +209,8 @@ class AmneziaVpnService : VpnService() { loadServerData() launchProtocolStateHandler() networkState = NetworkState(this, ::reconnect) + trafficStats = TrafficStats() + registerBroadcastReceivers() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -227,7 +226,10 @@ class AmneziaVpnService : VpnService() { Log.d(TAG, "Start service") connect(intent?.getStringExtra(MSG_VPN_CONFIG)) } - ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat) + ServiceCompat.startForeground( + this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value), + foregroundServiceTypeCompat + ) return START_REDELIVER_INTENT } @@ -267,6 +269,7 @@ class AmneziaVpnService : VpnService() { override fun onDestroy() { Log.d(TAG, "Destroy service") + unregisterBroadcastReceivers() runBlocking { disconnect() disconnectionJob?.join() @@ -287,6 +290,63 @@ class AmneziaVpnService : VpnService() { stopSelf() } + private fun registerBroadcastReceivers() { + Log.d(TAG, "Register broadcast receivers") + disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) { + Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT") + disconnect() + } + + notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + registerBroadcastReceiver( + arrayOf( + NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED, + NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED + ) + ) { + val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false) + Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state") + if (state == false) { + enableNotification() + } else { + disableNotification() + } + } + } else null + + registerScreenStateBroadcastReceivers() + } + + private fun registerScreenStateBroadcastReceivers() { + if (serviceNotification.isNotificationEnabled()) { + Log.d(TAG, "Register screen state broadcast receivers") + screenOnReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_ON) { + if (isConnected && serviceNotification.isNotificationEnabled()) startTrafficStatsUpdateJob() + } + + screenOffReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_OFF) { + stopTrafficStatsUpdateJob() + } + } + } + + private fun unregisterScreenStateBroadcastReceivers() { + Log.d(TAG, "Unregister screen state broadcast receivers") + unregisterBroadcastReceiver(screenOnReceiver) + unregisterBroadcastReceiver(screenOffReceiver) + screenOnReceiver = null + screenOffReceiver = null + } + + private fun unregisterBroadcastReceivers() { + Log.d(TAG, "Unregister broadcast receivers") + unregisterBroadcastReceiver(disconnectReceiver) + unregisterBroadcastReceiver(notificationStateReceiver) + unregisterScreenStateBroadcastReceivers() + disconnectReceiver = null + notificationStateReceiver = null + } + /** * Methods responsible for processing VPN connection */ @@ -295,29 +355,8 @@ class AmneziaVpnService : VpnService() { // drop first default UNKNOWN state protocolState.drop(1).collect { protocolState -> Log.d(TAG, "Protocol state changed: $protocolState") - when (protocolState) { - CONNECTED -> { - networkState.bindNetworkListener() - if (isActivityConnected) launchSendingStatistics() - } - DISCONNECTED -> { - networkState.unbindNetworkListener() - stopSendingStatistics() - if (!isServiceBound) stopService() - } - - DISCONNECTING -> { - networkState.unbindNetworkListener() - stopSendingStatistics() - } - - RECONNECTING -> { - stopSendingStatistics() - } - - CONNECTING, UNKNOWN -> {} - } + serviceNotification.updateNotification(serverName, protocolState) clientMessengers.send { ServiceEvent.STATUS_CHANGED.packToMessage { @@ -326,13 +365,41 @@ class AmneziaVpnService : VpnService() { } VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) } + + when (protocolState) { + CONNECTED -> { + networkState.bindNetworkListener() + // if (isActivityConnected) launchSendingStatistics() + launchTrafficStatsUpdate() + } + + DISCONNECTED -> { + networkState.unbindNetworkListener() + stopTrafficStatsUpdateJob() + // stopSendingStatistics() + if (!isServiceBound) stopService() + } + + DISCONNECTING -> { + networkState.unbindNetworkListener() + stopTrafficStatsUpdateJob() + // stopSendingStatistics() + } + + RECONNECTING -> { + stopTrafficStatsUpdateJob() + // stopSendingStatistics() + } + + CONNECTING, UNKNOWN -> {} + } } } } - @MainThread +/* @MainThread private fun launchSendingStatistics() { - /* if (isServiceBound && isConnected) { + if (isServiceBound && isConnected) { statisticsSendingJob = mainScope.launch { while (true) { clientMessenger.send { @@ -343,12 +410,62 @@ class AmneziaVpnService : VpnService() { delay(STATISTICS_SENDING_TIMEOUT) } } - } */ + } } @MainThread private fun stopSendingStatistics() { statisticsSendingJob?.cancel() + } */ + + @MainThread + private fun enableNotification() { + registerScreenStateBroadcastReceivers() + serviceNotification.updateNotification(serverName, protocolState.value) + launchTrafficStatsUpdate() + } + + @MainThread + private fun disableNotification() { + unregisterScreenStateBroadcastReceivers() + stopTrafficStatsUpdateJob() + } + + @MainThread + private fun launchTrafficStatsUpdate() { + stopTrafficStatsUpdateJob() + if (isConnected && + serviceNotification.isNotificationEnabled() && + getSystemService()?.isInteractive != false + ) { + Log.d(TAG, "Launch traffic stats update") + trafficStats.reset() + startTrafficStatsUpdateJob() + } + } + + @MainThread + private fun startTrafficStatsUpdateJob() { + if (trafficStatsUpdateJob == null && trafficStats.isSupported()) { + Log.d(TAG, "Start traffic stats update") + trafficStatsUpdateJob = mainScope.launch { + while (true) { + trafficStats.getSpeed().let { speed -> + if (isConnected) { + serviceNotification.updateSpeed(speed) + } + } + delay(TRAFFIC_STATS_UPDATE_TIMEOUT) + } + } + } + } + + @MainThread + private fun stopTrafficStatsUpdateJob() { + Log.d(TAG, "Stop traffic stats update") + trafficStatsUpdateJob?.cancel() + trafficStatsUpdateJob = null } @MainThread @@ -471,6 +588,7 @@ class AmneziaVpnService : VpnService() { private fun saveServerData(config: JSONObject?) { serverName = config?.opt("description") as String? serverIndex = config?.opt("serverIndex") as Int? ?: -1 + Log.d(TAG, "Save server data: ($serverIndex, $serverName)") Prefs.save(PREFS_SERVER_NAME, serverName) Prefs.save(PREFS_SERVER_INDEX, serverIndex) } @@ -478,6 +596,7 @@ class AmneziaVpnService : VpnService() { private fun loadServerData() { serverName = Prefs.load(PREFS_SERVER_NAME).ifBlank { null } if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX) + Log.d(TAG, "Load server data: ($serverIndex, $serverName)") } private fun checkPermission(): Boolean = @@ -494,9 +613,8 @@ class AmneziaVpnService : VpnService() { companion object { fun isRunning(context: Context): Boolean = - (context.getSystemService(ACTIVITY_SERVICE) as ActivityManager) - .runningAppProcesses.any { - it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE - } + context.getSystemService()!!.runningAppProcesses.any { + it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE + } } } diff --git a/client/android/src/org/amnezia/vpn/IpcMessage.kt b/client/android/src/org/amnezia/vpn/IpcMessage.kt index 2ddff4ef..bd206004 100644 --- a/client/android/src/org/amnezia/vpn/IpcMessage.kt +++ b/client/android/src/org/amnezia/vpn/IpcMessage.kt @@ -32,6 +32,7 @@ enum class Action : IpcMessage { CONNECT, DISCONNECT, REQUEST_STATUS, + NOTIFICATION_PERMISSION_GRANTED, SET_SAVE_LOGS } diff --git a/client/android/src/org/amnezia/vpn/PackageManagerHelper.java b/client/android/src/org/amnezia/vpn/PackageManagerHelper.java deleted file mode 100644 index 55bfbf93..00000000 --- a/client/android/src/org/amnezia/vpn/PackageManagerHelper.java +++ /dev/null @@ -1,189 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.amnezia.vpn; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.Manifest.permission; -import android.net.Uri; -import android.os.Build; -import android.util.Log; -import android.webkit.WebView; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; - -// Gets used by /platforms/android/androidAppListProvider.cpp -public class PackageManagerHelper { - final static String TAG = "PackageManagerHelper"; - final static int MIN_CHROME_VERSION = 65; - - final static List CHROME_BROWSERS = Arrays.asList( - new String[] {"com.google.android.webview", "com.android.webview", "com.google.chrome"}); - - private static String getAllAppNames(Context ctx) { - JSONObject output = new JSONObject(); - PackageManager pm = ctx.getPackageManager(); - List browsers = getBrowserIDs(pm); - List packs = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS); - for (int i = 0; i < packs.size(); i++) { - PackageInfo p = packs.get(i); - // Do not add ourselves and System Apps to the list, unless it might be a browser - if ((!isSystemPackage(p,pm) || browsers.contains(p.packageName)) - && !isSelf(p)) { - String appid = p.packageName; - String appName = p.applicationInfo.loadLabel(pm).toString(); - try { - output.put(appid, appName); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - return output.toString(); - } - - private static Drawable getAppIcon(Context ctx, String id) { - try { - return ctx.getPackageManager().getApplicationIcon(id); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return new ColorDrawable(Color.TRANSPARENT); - } - - private static boolean isSystemPackage(PackageInfo pkgInfo, PackageManager pm) { - if( (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0){ - // no system app - return false; - } - // For Systems Packages there are Cases where we want to add it anyway: - // Has the use Internet permission (otherwise makes no sense) - // Had at least 1 update (this means it's probably on any AppStore) - // Has a a launch activity (has a ui and is not just a system service) - - if(!usesInternet(pkgInfo)){ - return true; - } - if(!hadUpdate(pkgInfo)){ - return true; - } - if(pm.getLaunchIntentForPackage(pkgInfo.packageName) == null){ - // If there is no way to launch this from a homescreen, def a sys package - return true; - } - return false; - } - private static boolean isSelf(PackageInfo pkgInfo) { - return pkgInfo.packageName.equals("org.amnezia.vpn") - || pkgInfo.packageName.equals("org.amnezia.vpn.debug"); - } - private static boolean usesInternet(PackageInfo pkgInfo){ - if(pkgInfo.requestedPermissions == null){ - return false; - } - for(int i=0; i < pkgInfo.requestedPermissions.length; i++) { - String permission = pkgInfo.requestedPermissions[i]; - if(Manifest.permission.INTERNET.equals(permission)){ - return true; - } - } - return false; - } - private static boolean hadUpdate(PackageInfo pkgInfo){ - return pkgInfo.lastUpdateTime > pkgInfo.firstInstallTime; - } - - // Returns List of all Packages that can classify themselves as browsers - private static List getBrowserIDs(PackageManager pm) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.amnezia.org/")); - intent.addCategory(Intent.CATEGORY_BROWSABLE); - // We've tried using PackageManager.MATCH_DEFAULT_ONLY flag and found that browsers that - // are not set as the default browser won't be matched even if they had CATEGORY_DEFAULT set - // in the intent filter - - List resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL); - List browsers = new ArrayList(); - for (int i = 0; i < resolveInfos.size(); i++) { - ResolveInfo info = resolveInfos.get(i); - String browserID = info.activityInfo.packageName; - browsers.add(browserID); - } - return browsers; - } - - // Gets called in AndroidAuthenticationListener; - public static boolean isWebViewSupported(Context ctx) { - Log.v(TAG, "Checking if installed Webview is compatible with FxA"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // The default Webview is able do to FXA - return true; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PackageInfo pi = WebView.getCurrentWebViewPackage(); - if (CHROME_BROWSERS.contains(pi.packageName)) { - return isSupportedChromeBrowser(pi); - } - return isNotAncientBrowser(pi); - } - - // Before O the webview is hardcoded, but we dont know which package it is. - // Check if com.google.android.webview is installed - PackageManager pm = ctx.getPackageManager(); - try { - PackageInfo pi = pm.getPackageInfo("com.google.android.webview", 0); - return isSupportedChromeBrowser(pi); - } catch (PackageManager.NameNotFoundException e) { - } - // Otherwise check com.android.webview - try { - PackageInfo pi = pm.getPackageInfo("com.android.webview", 0); - return isSupportedChromeBrowser(pi); - } catch (PackageManager.NameNotFoundException e) { - } - Log.e(TAG, "Android System WebView is not found"); - // Giving up :( - return false; - } - - private static boolean isSupportedChromeBrowser(PackageInfo pi) { - Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName); - Log.d(TAG, "version name: " + pi.versionName); - Log.d(TAG, "version code: " + pi.versionCode); - try { - String versionCode = pi.versionName.split(Pattern.quote(" "))[0]; - String majorVersion = versionCode.split(Pattern.quote("."))[0]; - int version = Integer.parseInt(majorVersion); - return version >= MIN_CHROME_VERSION; - } catch (Exception e) { - Log.e(TAG, "Failed to check Chrome Version Code " + pi.versionName); - return false; - } - } - - private static boolean isNotAncientBrowser(PackageInfo pi) { - // Not a google chrome - So the version name is worthless - // Lets just make sure the WebView - // used is not ancient ==> Was updated in at least the last 365 days - Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName); - Log.d(TAG, "version name: " + pi.versionName); - Log.d(TAG, "version code: " + pi.versionCode); - double oneYearInMillis = 31536000000L; - return pi.lastUpdateTime > (System.currentTimeMillis() - oneYearInMillis); - } -} diff --git a/client/android/src/org/amnezia/vpn/ServiceNotification.kt b/client/android/src/org/amnezia/vpn/ServiceNotification.kt new file mode 100644 index 00000000..efdd04d3 --- /dev/null +++ b/client/android/src/org/amnezia/vpn/ServiceNotification.kt @@ -0,0 +1,180 @@ +package org.amnezia.vpn + +import android.Manifest.permission +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.NotificationChannelCompat.Builder +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.Action +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import org.amnezia.vpn.protocol.ProtocolState +import org.amnezia.vpn.protocol.ProtocolState.CONNECTED +import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED +import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.net.TrafficStats.TrafficData + +private const val TAG = "ServiceNotification" + +private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification" +private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications" +const val NOTIFICATION_ID = 1337 + +private const val GET_ACTIVITY_REQUEST_CODE = 0 +private const val CONNECT_REQUEST_CODE = 1 +private const val DISCONNECT_REQUEST_CODE = 2 + +class ServiceNotification(private val context: Context) { + + private val upDownSymbols = when (Build.BRAND) { + "Infinix" -> '˅' to '˄' + else -> '↓' to '↑' + } + + private val notificationManager = NotificationManagerCompat.from(context) + + private val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) + .setShowWhen(false) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setContentIntent( + PendingIntent.getActivity( + context, + GET_ACTIVITY_REQUEST_CODE, + Intent(context, AmneziaActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + + private val zeroSpeed: String = with(TrafficData.ZERO) { + formatSpeedString(rxString, txString) + } + + fun buildNotification(serverName: String?, state: ProtocolState): Notification { + val speedString = if (state == CONNECTED) zeroSpeed else null + + Log.d(TAG, "Build notification: $serverName, $state") + + return notificationBuilder + .setSmallIcon(R.drawable.ic_amnezia_round) + .setContentTitle(serverName ?: "AmneziaVPN") + .setContentText(context.getString(state)) + .setSubText(speedString) + .setWhen(System.currentTimeMillis()) + .clearActions() + .apply { + getAction(state)?.let { + addAction(it) + } + } + .build() + } + + private fun buildNotification(speed: TrafficData): Notification = + notificationBuilder + .setWhen(System.currentTimeMillis()) + .setSubText(getSpeedString(speed)) + .build() + + fun isNotificationEnabled(): Boolean { + if (!context.isNotificationPermissionGranted()) return false + if (!notificationManager.areNotificationsEnabled()) return false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) + ?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true + } + return true + } + + @SuppressLint("MissingPermission") + fun updateNotification(serverName: String?, state: ProtocolState) { + if (context.isNotificationPermissionGranted()) { + Log.d(TAG, "Update notification: $serverName, $state") + notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, state)) + } + } + + @SuppressLint("MissingPermission") + fun updateSpeed(speed: TrafficData) { + if (context.isNotificationPermissionGranted()) { + notificationManager.notify(NOTIFICATION_ID, buildNotification(speed)) + } + } + + private fun getSpeedString(traffic: TrafficData) = + if (traffic == TrafficData.ZERO) zeroSpeed + else formatSpeedString(traffic.rxString, traffic.txString) + + private fun formatSpeedString(rx: String, tx: String) = with(upDownSymbols) { "$first $rx $second $tx" } + + private fun getAction(state: ProtocolState): Action? { + return when (state) { + CONNECTED -> { + Action( + 0, context.getString(R.string.disconnect), + PendingIntent.getBroadcast( + context, + DISCONNECT_REQUEST_CODE, + Intent(ACTION_DISCONNECT).apply { + setPackage("org.amnezia.vpn") + }, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + + DISCONNECTED -> { + Action( + 0, context.getString(R.string.connect), + createServicePendingIntent( + context, + CONNECT_REQUEST_CODE, + Intent(context, AmneziaVpnService::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + + else -> null + } + } + + private val createServicePendingIntent: (Context, Int, Intent, Int) -> PendingIntent = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PendingIntent::getForegroundService + } else { + PendingIntent::getService + } + + companion object { + fun createNotificationChannel(context: Context) { + with(NotificationManagerCompat.from(context)) { + deleteNotificationChannel(OLD_NOTIFICATION_CHANNEL_ID) + createNotificationChannel( + Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT) + .setShowBadge(false) + .setSound(null, null) + .setVibrationEnabled(false) + .setLightsEnabled(false) + .setName("AmneziaVPN") + .setDescription(context.resources.getString(R.string.notificationChannelDescription)) + .build() + ) + } + } + } +} + +fun Context.isNotificationPermissionGranted(): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission(this, permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED diff --git a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt index 74aeb578..12d3fb3d 100644 --- a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt +++ b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt @@ -3,9 +3,7 @@ package org.amnezia.vpn import android.app.AlertDialog import android.app.KeyguardManager import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.net.VpnService @@ -33,11 +31,9 @@ class VpnRequestActivity : ComponentActivity() { val requestIntent = VpnService.prepare(applicationContext) if (requestIntent != null) { if (getSystemService()!!.isKeyguardLocked) { - userPresentReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) = - requestLauncher.launch(requestIntent) + userPresentReceiver = registerBroadcastReceiver(Intent.ACTION_USER_PRESENT) { + requestLauncher.launch(requestIntent) } - registerReceiver(userPresentReceiver, IntentFilter(Intent.ACTION_USER_PRESENT)) } else { requestLauncher.launch(requestIntent) } @@ -49,9 +45,8 @@ class VpnRequestActivity : ComponentActivity() { } override fun onDestroy() { - userPresentReceiver?.let { - unregisterReceiver(it) - } + unregisterBroadcastReceiver(userPresentReceiver) + userPresentReceiver = null super.onDestroy() } diff --git a/client/android/src/org/amnezia/vpn/VpnState.kt b/client/android/src/org/amnezia/vpn/VpnState.kt index 4d5e9c99..fbc4ef59 100644 --- a/client/android/src/org/amnezia/vpn/VpnState.kt +++ b/client/android/src/org/amnezia/vpn/VpnState.kt @@ -4,6 +4,8 @@ import android.app.Application import androidx.datastore.core.MultiProcessDataStoreFactory import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.InputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -59,7 +61,8 @@ private class VpnStateSerializer : Serializer { override suspend fun readFrom(input: InputStream): VpnState { return withContext(Dispatchers.IO) { - ObjectInputStream(input).use { + val bios = ByteArrayInputStream(input.readBytes()) + ObjectInputStream(bios).use { it.readObject() as VpnState } } @@ -67,9 +70,11 @@ private class VpnStateSerializer : Serializer { override suspend fun writeTo(t: VpnState, output: OutputStream) { withContext(Dispatchers.IO) { - ObjectOutputStream(output).use { + val baos = ByteArrayOutputStream() + ObjectOutputStream(baos).use { it.writeObject(t) } + output.write(baos.toByteArray()) } } } diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index 537d9925..e382b080 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -17,6 +17,7 @@ object QtAndroidController { external fun onServiceError() external fun onVpnPermissionRejected() + external fun onNotificationStateChanged() external fun onVpnStateChanged(stateCode: Int) external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) diff --git a/client/android/utils/build.gradle.kts b/client/android/utils/build.gradle.kts index 2ad03d61..6f44624c 100644 --- a/client/android/utils/build.gradle.kts +++ b/client/android/utils/build.gradle.kts @@ -17,5 +17,7 @@ android { } dependencies { + implementation(libs.androidx.core) + implementation(libs.kotlinx.coroutines) implementation(libs.androidx.security.crypto) } diff --git a/client/android/utils/src/main/kotlin/net/InetEndpoint.kt b/client/android/utils/src/main/kotlin/net/InetEndpoint.kt index f131182c..1bf63bf7 100644 --- a/client/android/utils/src/main/kotlin/net/InetEndpoint.kt +++ b/client/android/utils/src/main/kotlin/net/InetEndpoint.kt @@ -1,16 +1,21 @@ package org.amnezia.vpn.util.net +import java.net.Inet4Address import java.net.InetAddress data class InetEndpoint(val address: InetAddress, val port: Int) { - override fun toString(): String = "${address.hostAddress}:$port" + override fun toString(): String = if (address is Inet4Address) { + "${address.ip}:$port" + } else { + "[${address.ip}]:$port" + } companion object { fun parse(data: String): InetEndpoint { - val split = data.split(":") - val address = parseInetAddress(split.first()) - val port = split.last().toInt() + val i = data.lastIndexOf(':') + val address = parseInetAddress(data.substring(0, i)) + val port = data.substring(i + 1).toInt() return InetEndpoint(address, port) } } diff --git a/client/android/utils/src/main/kotlin/net/InetNetwork.kt b/client/android/utils/src/main/kotlin/net/InetNetwork.kt index 2285454b..a21528b0 100644 --- a/client/android/utils/src/main/kotlin/net/InetNetwork.kt +++ b/client/android/utils/src/main/kotlin/net/InetNetwork.kt @@ -9,7 +9,11 @@ data class InetNetwork(val address: InetAddress, val mask: Int) { constructor(address: InetAddress) : this(address, address.maxPrefixLength) - override fun toString(): String = "${address.hostAddress}/$mask" + val isIpv4: Boolean = address is Inet4Address + val isIpv6: Boolean + get() = !isIpv4 + + override fun toString(): String = "${address.ip}/$mask" companion object { fun parse(data: String): InetNetwork { diff --git a/client/android/utils/src/main/kotlin/net/IpAddress.kt b/client/android/utils/src/main/kotlin/net/IpAddress.kt index 83880b91..2f046b8b 100644 --- a/client/android/utils/src/main/kotlin/net/IpAddress.kt +++ b/client/android/utils/src/main/kotlin/net/IpAddress.kt @@ -3,12 +3,17 @@ package org.amnezia.vpn.util.net import java.net.InetAddress @OptIn(ExperimentalUnsignedTypes::class) -class IpAddress private constructor(private val address: UByteArray) : Comparable { +internal class IpAddress private constructor(private val address: UByteArray) : Comparable { val size: Int = address.size val lastIndex: Int = address.lastIndex val maxMask: Int = size * 8 + @OptIn(ExperimentalStdlibApi::class) + val hexFormat: HexFormat by lazy { + HexFormat { number.removeLeadingZeros = true } + } + constructor(inetAddress: InetAddress) : this(inetAddress.address.asUByteArray()) constructor(ipAddress: String) : this(parseInetAddress(ipAddress)) @@ -43,6 +48,8 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl return copy } + fun isMinIp(): Boolean = address.all { it == 0x00u.toUByte() } + fun isMaxIp(): Boolean = address.all { it == 0xffu.toUByte() } override fun compareTo(other: IpAddress): Int { @@ -74,12 +81,14 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl private fun toIpv6String(): String { val sb = StringBuilder() var i = 0 + var block: Int while (i < size) { - sb.append(address[i++].toHexString()) - sb.append(address[i++].toHexString()) + block = address[i++].toInt() shl 8 + block += address[i++].toInt() + sb.append(block.toHexString(hexFormat)) sb.append(':') } sb.deleteAt(sb.lastIndex) - return sb.toString() + return convertIpv6ToCanonicalForm(sb.toString()) } } diff --git a/client/android/utils/src/main/kotlin/net/IpRange.kt b/client/android/utils/src/main/kotlin/net/IpRange.kt index 834c762c..cf169791 100644 --- a/client/android/utils/src/main/kotlin/net/IpRange.kt +++ b/client/android/utils/src/main/kotlin/net/IpRange.kt @@ -2,14 +2,24 @@ package org.amnezia.vpn.util.net import java.net.InetAddress -class IpRange(private val start: IpAddress, private val end: IpAddress) : Comparable { +class IpRange internal constructor( + internal val start: IpAddress, + internal val end: IpAddress +) : Comparable { init { - if (start > end) throw IllegalArgumentException("Start IP: $start is greater then end IP: $end") + if (start.size != end.size) { + throw IllegalArgumentException( + "Unable to create a range between IPv4 and IPv6 addresses (start IP: [$start], end IP: [$end])" + ) + } + if (start > end) throw IllegalArgumentException("Start IP: [$start] is greater then end IP: [$end]") } private constructor(addresses: Pair) : this(addresses.first, addresses.second) + internal constructor(ipAddress: IpAddress) : this(ipAddress, ipAddress) + constructor(inetAddress: InetAddress, mask: Int) : this(from(inetAddress, mask)) constructor(address: String, mask: Int) : this(parseInetAddress(address), mask) @@ -22,6 +32,13 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar private fun isIntersect(other: IpRange): Boolean = (start <= other.end) && (end >= other.start) + operator fun plus(other: IpRange): IpRange? { + if (start > other.end && !start.isMinIp() && start.dec() == other.end) return IpRange(other.start, end) + if (end < other.start && !end.isMaxIp() && end.inc() == other.start) return IpRange(start, other.end) + if (!isIntersect(other)) return null + return IpRange(minOf(start, other.start), maxOf(end, other.end)) + } + operator fun minus(other: IpRange): List? { if (this in other) return emptyList() if (!isIntersect(other)) return null @@ -94,9 +111,7 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar return result } - override fun toString(): String { - return "$start - $end" - } + override fun toString(): String = if (start == end) "<$start>" else "<$start - $end>" companion object { private fun from(inetAddress: InetAddress, mask: Int): Pair { diff --git a/client/android/utils/src/main/kotlin/net/IpRangeSet.kt b/client/android/utils/src/main/kotlin/net/IpRangeSet.kt index 45bae854..1053a0c6 100644 --- a/client/android/utils/src/main/kotlin/net/IpRangeSet.kt +++ b/client/android/utils/src/main/kotlin/net/IpRangeSet.kt @@ -1,15 +1,35 @@ package org.amnezia.vpn.util.net -class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) { +class IpRangeSet { - private val ranges = sortedSetOf(ipRange) + private val ranges = sortedSetOf() + + fun add(ipRange: IpRange) { + val iterator = ranges.iterator() + var rangeToAdd = ipRange + run { + while (iterator.hasNext()) { + val curRange = iterator.next() + if (rangeToAdd.end < curRange.start && + !rangeToAdd.end.isMaxIp() && + rangeToAdd.end.inc() != curRange.start) break + (curRange + rangeToAdd)?.let { resultRange -> + if (resultRange == curRange) return@run + iterator.remove() + rangeToAdd = resultRange + } + } + ranges += rangeToAdd + } + } fun remove(ipRange: IpRange) { val iterator = ranges.iterator() val splitRanges = mutableListOf() while (iterator.hasNext()) { - val range = iterator.next() - (range - ipRange)?.let { resultRanges -> + val curRange = iterator.next() + if (ipRange.end < curRange.start) break + (curRange - ipRange)?.let { resultRanges -> iterator.remove() splitRanges += resultRanges } @@ -17,10 +37,7 @@ class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) { ranges += splitRanges } - fun subnets(): List = - ranges.map(IpRange::subnets).flatten() + fun subnets(): List = ranges.map(IpRange::subnets).flatten() - override fun toString(): String { - return ranges.toString() - } + override fun toString(): String = ranges.toString() } diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index 957fc3cb..3cff8c04 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -10,7 +10,9 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED import android.net.NetworkRequest import android.os.Build import android.os.Handler +import androidx.core.content.getSystemService import kotlin.LazyThreadSafetyMode.NONE +import kotlinx.coroutines.delay import org.amnezia.vpn.util.Log private const val TAG = "NetworkState" @@ -28,7 +30,7 @@ class NetworkState( } private val connectivityManager: ConnectivityManager by lazy(NONE) { - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + context.getSystemService()!! } private val networkRequest: NetworkRequest by lazy(NONE) { @@ -80,13 +82,24 @@ class NetworkState( } } - fun bindNetworkListener() { + suspend fun bindNetworkListener() { if (isListenerBound) return Log.d(TAG, "Bind network listener") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - connectivityManager.requestNetwork(networkRequest, networkCallback, handler) + try { + connectivityManager.requestNetwork(networkRequest, networkCallback, handler) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to bind network listener: $e") + // Android 11 bug: https://issuetracker.google.com/issues/175055271 + if (e.message?.startsWith("Package android does not belong to") == true) { + delay(1000) + connectivityManager.requestNetwork(networkRequest, networkCallback, handler) + } else { + throw e + } + } } else { connectivityManager.requestNetwork(networkRequest, networkCallback) } diff --git a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt index 83160e70..b75748be 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt @@ -5,12 +5,14 @@ import android.net.ConnectivityManager import android.net.InetAddresses import android.net.NetworkCapabilities import android.os.Build +import androidx.core.content.getSystemService +import java.lang.reflect.InvocationTargetException import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress fun getLocalNetworks(context: Context, ipv6: Boolean): List { - val connectivityManager = context.getSystemService(ConnectivityManager::class.java) + val connectivityManager = context.getSystemService()!! connectivityManager.activeNetwork?.let { network -> val netCapabilities = connectivityManager.getNetworkCapabilities(network) val linkProperties = connectivityManager.getLinkProperties(network) @@ -39,8 +41,28 @@ private val parseNumericAddressCompat: (String) -> InetAddress = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { InetAddresses::parseNumericAddress } else { - val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java) - fun(address: String): InetAddress { - return m.invoke(null, address) as InetAddress + try { + val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java) + fun(address: String): InetAddress { + try { + return m.invoke(null, address) as InetAddress + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } + } + } catch (_: NoSuchMethodException) { + fun(address: String): InetAddress { + return InetAddress.getByName(address) + } } } + +internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6 + .replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2") + +internal val InetAddress.ip: String + get() = if (this is Inet4Address) { + hostAddress!! + } else { + convertIpv6ToCanonicalForm(hostAddress!!) + } diff --git a/client/android/utils/src/main/kotlin/net/TrafficStats.kt b/client/android/utils/src/main/kotlin/net/TrafficStats.kt new file mode 100644 index 00000000..170d164e --- /dev/null +++ b/client/android/utils/src/main/kotlin/net/TrafficStats.kt @@ -0,0 +1,93 @@ +package org.amnezia.vpn.util.net + +import android.net.TrafficStats +import android.os.Build +import android.os.Process +import android.os.SystemClock +import kotlin.math.roundToLong + +private const val BYTE = 1L +private const val KiB = BYTE shl 10 +private const val MiB = KiB shl 10 +private const val GiB = MiB shl 10 +private const val TiB = GiB shl 10 + +class TrafficStats { + + private var lastTrafficData = TrafficData.ZERO + private var lastTimestamp = 0L + + private val getTrafficDataCompat: () -> TrafficData = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val iface = "tun0" + fun(): TrafficData { + return TrafficData(TrafficStats.getRxBytes(iface), TrafficStats.getTxBytes(iface)) + } + } else { + val uid = Process.myUid() + fun(): TrafficData { + return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid)) + } + } + + fun reset() { + lastTrafficData = getTrafficDataCompat() + lastTimestamp = SystemClock.elapsedRealtime() + } + + fun isSupported(): Boolean = + lastTrafficData.rx != TrafficStats.UNSUPPORTED.toLong() && lastTrafficData.tx != TrafficStats.UNSUPPORTED.toLong() + + fun getSpeed(): TrafficData { + val timestamp = SystemClock.elapsedRealtime() + val elapsedSeconds = (timestamp - lastTimestamp) / 1000.0 + val trafficData = getTrafficDataCompat() + val speed = trafficData.diff(lastTrafficData, elapsedSeconds) + lastTrafficData = trafficData + lastTimestamp = timestamp + return speed + } + + class TrafficData(val rx: Long, val tx: Long) { + + private var _rxString: String? = null + val rxString: String + get() { + if (_rxString == null) _rxString = rx.speedToString() + return _rxString ?: throw AssertionError("Set to null by another thread") + } + + private var _txString: String? = null + val txString: String + get() { + if (_txString == null) _txString = tx.speedToString() + return _txString ?: throw AssertionError("Set to null by another thread") + } + + fun diff(other: TrafficData, elapsedSeconds: Double): TrafficData { + val rx = ((this.rx - other.rx) / elapsedSeconds).round() + val tx = ((this.tx - other.tx) / elapsedSeconds).round() + return if (rx == 0L && tx == 0L) ZERO else TrafficData(rx, tx) + } + + private fun Double.round() = if (isNaN()) 0L else roundToLong() + + private fun Long.speedToString() = + when { + this < KiB -> formatSize(this, BYTE, "B/s") + this < MiB -> formatSize(this, KiB, "KiB/s") + this < GiB -> formatSize(this, MiB, "MiB/s") + this < TiB -> formatSize(this, GiB, "GiB/s") + else -> formatSize(this, TiB, "TiB/s") + } + + private fun formatSize(bytes: Long, divider: Long, unit: String): String { + val s = (bytes.toDouble() / divider * 100).roundToLong() / 100.0 + return "${s.toString().removeSuffix(".0")} $unit" + } + + companion object { + val ZERO: TrafficData = TrafficData(0L, 0L) + } + } +} diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt index 0e303f0e..09269f54 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt @@ -37,8 +37,8 @@ open class WireguardConfig protected constructor( open fun appendPeerLine(sb: StringBuilder) = with(sb) { appendLine("public_key=$publicKeyHex") - routes.forEach { route -> - appendLine("allowed_ip=$route") + routes.filter { it.include }.forEach { route -> + appendLine("allowed_ip=${route.inetNetwork}") } appendLine("endpoint=$endpoint") if (persistentKeepalive != 0) diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index d3ba196d..c39642ff 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -26,7 +26,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h @@ -35,7 +34,6 @@ set(HEADERS ${HEADERS} set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp diff --git a/client/configurators/awg_configurator.cpp b/client/configurators/awg_configurator.cpp index d0a1034e..21b61ba4 100644 --- a/client/configurators/awg_configurator.cpp +++ b/client/configurators/awg_configurator.cpp @@ -9,7 +9,7 @@ AwgConfigurator::AwgConfigurator(std::shared_ptr settings, const QShar } QString AwgConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode errorCode) + ErrorCode &errorCode) { QString config = WireguardConfigurator::createConfig(credentials, container, containerConfig, errorCode); diff --git a/client/configurators/awg_configurator.h b/client/configurators/awg_configurator.h index 0a64bbb1..301b927c 100644 --- a/client/configurators/awg_configurator.h +++ b/client/configurators/awg_configurator.h @@ -12,7 +12,7 @@ public: AwgConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent = nullptr); QString createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode); + const QJsonObject &containerConfig, ErrorCode &errorCode); }; #endif // AWGCONFIGURATOR_H diff --git a/client/configurators/cloak_configurator.cpp b/client/configurators/cloak_configurator.cpp index d876e7b8..fd0c0391 100644 --- a/client/configurators/cloak_configurator.cpp +++ b/client/configurators/cloak_configurator.cpp @@ -13,7 +13,7 @@ CloakConfigurator::CloakConfigurator(std::shared_ptr settings, const Q } QString CloakConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode errorCode) + ErrorCode &errorCode) { QString cloakPublicKey = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::cloak::ckPublicKeyPath, errorCode); diff --git a/client/configurators/cloak_configurator.h b/client/configurators/cloak_configurator.h index 6c2523fb..d117a821 100644 --- a/client/configurators/cloak_configurator.h +++ b/client/configurators/cloak_configurator.h @@ -14,7 +14,7 @@ public: CloakConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent = nullptr); QString createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode); + const QJsonObject &containerConfig, ErrorCode &errorCode); }; #endif // CLOAK_CONFIGURATOR_H diff --git a/client/configurators/configurator_base.h b/client/configurators/configurator_base.h index f63892eb..2427b604 100644 --- a/client/configurators/configurator_base.h +++ b/client/configurators/configurator_base.h @@ -15,7 +15,7 @@ public: explicit ConfiguratorBase(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent = nullptr); virtual QString createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode) = 0; + const QJsonObject &containerConfig, ErrorCode &errorCode) = 0; virtual QString processConfigWithLocalSettings(const QPair &dns, const bool isApiConfig, QString &protocolConfigString); diff --git a/client/configurators/ikev2_configurator.cpp b/client/configurators/ikev2_configurator.cpp index bf1a8ad4..894a0e3d 100644 --- a/client/configurators/ikev2_configurator.cpp +++ b/client/configurators/ikev2_configurator.cpp @@ -20,7 +20,7 @@ Ikev2Configurator::Ikev2Configurator(std::shared_ptr settings, const Q } Ikev2Configurator::ConnectionData Ikev2Configurator::prepareIkev2Config(const ServerCredentials &credentials, DockerContainer container, - ErrorCode errorCode) + ErrorCode &errorCode) { Ikev2Configurator::ConnectionData connData; connData.host = credentials.hostName; @@ -55,7 +55,7 @@ Ikev2Configurator::ConnectionData Ikev2Configurator::prepareIkev2Config(const Se } QString Ikev2Configurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode errorCode) + ErrorCode &errorCode) { Q_UNUSED(containerConfig) diff --git a/client/configurators/ikev2_configurator.h b/client/configurators/ikev2_configurator.h index 322028c1..e3a85216 100644 --- a/client/configurators/ikev2_configurator.h +++ b/client/configurators/ikev2_configurator.h @@ -22,14 +22,14 @@ public: }; QString createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode); + const QJsonObject &containerConfig, ErrorCode &errorCode); QString genIkev2Config(const ConnectionData &connData); QString genMobileConfig(const ConnectionData &connData); QString genStrongSwanConfig(const ConnectionData &connData); ConnectionData prepareIkev2Config(const ServerCredentials &credentials, - DockerContainer container, ErrorCode errorCode); + DockerContainer container, ErrorCode &errorCode); }; #endif // IKEV2_CONFIGURATOR_H diff --git a/client/configurators/openvpn_configurator.cpp b/client/configurators/openvpn_configurator.cpp index 03adaa49..c4bdf860 100644 --- a/client/configurators/openvpn_configurator.cpp +++ b/client/configurators/openvpn_configurator.cpp @@ -31,7 +31,7 @@ OpenVpnConfigurator::OpenVpnConfigurator(std::shared_ptr settings, con } OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(const ServerCredentials &credentials, - DockerContainer container, ErrorCode errorCode) + DockerContainer container, ErrorCode &errorCode) { OpenVpnConfigurator::ConnectionData connData = OpenVpnConfigurator::createCertRequest(); connData.host = credentials.hostName; @@ -72,7 +72,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(co } QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode) + const QJsonObject &containerConfig, ErrorCode &errorCode) { QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::openvpn_template, container), m_serverController->genVarsForScript(credentials, container, containerConfig)); diff --git a/client/configurators/openvpn_configurator.h b/client/configurators/openvpn_configurator.h index f98831db..48e3f4be 100644 --- a/client/configurators/openvpn_configurator.h +++ b/client/configurators/openvpn_configurator.h @@ -25,7 +25,7 @@ public: }; QString createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode); + const QJsonObject &containerConfig, ErrorCode &errorCode); QString processConfigWithLocalSettings(const QPair &dns, const bool isApiConfig, QString &protocolConfigString); @@ -36,7 +36,7 @@ public: private: ConnectionData prepareOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container, - ErrorCode errorCode); + ErrorCode &errorCode); ErrorCode signCert(DockerContainer container, const ServerCredentials &credentials, QString clientId); }; diff --git a/client/configurators/shadowsocks_configurator.cpp b/client/configurators/shadowsocks_configurator.cpp index 9cd1ee5b..fd6c4841 100644 --- a/client/configurators/shadowsocks_configurator.cpp +++ b/client/configurators/shadowsocks_configurator.cpp @@ -14,7 +14,7 @@ ShadowSocksConfigurator::ShadowSocksConfigurator(std::shared_ptr setti } QString ShadowSocksConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode) + const QJsonObject &containerConfig, ErrorCode &errorCode) { QString ssKey = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::shadowsocks::ssKeyPath, errorCode); diff --git a/client/configurators/shadowsocks_configurator.h b/client/configurators/shadowsocks_configurator.h index 1e8d997b..b21b3be3 100644 --- a/client/configurators/shadowsocks_configurator.h +++ b/client/configurators/shadowsocks_configurator.h @@ -13,7 +13,7 @@ public: ShadowSocksConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent = nullptr); QString createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode); + const QJsonObject &containerConfig, ErrorCode &errorCode); }; #endif // SHADOWSOCKS_CONFIGURATOR_H diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index f3ea9ea3..f7faaa52 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -65,7 +65,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::genClientKeys() WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode) + const QJsonObject &containerConfig, ErrorCode &errorCode) { WireguardConfigurator::ConnectionData connData = WireguardConfigurator::genClientKeys(); connData.host = credentials.hostName; @@ -158,7 +158,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon } QString WireguardConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode) + const QJsonObject &containerConfig, ErrorCode &errorCode) { QString scriptData = amnezia::scriptData(m_configTemplate, container); QString config = diff --git a/client/configurators/wireguard_configurator.h b/client/configurators/wireguard_configurator.h index 5a3a8d1f..22e8a8be 100644 --- a/client/configurators/wireguard_configurator.h +++ b/client/configurators/wireguard_configurator.h @@ -27,7 +27,7 @@ public: }; QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode errorCode); + ErrorCode &errorCode); QString processConfigWithLocalSettings(const QPair &dns, const bool isApiConfig, QString &protocolConfigString); QString processConfigWithExportSettings(const QPair &dns, const bool isApiConfig, QString &protocolConfigString); @@ -36,7 +36,7 @@ public: private: ConnectionData prepareWireguardConfig(const ServerCredentials &credentials, DockerContainer container, - const QJsonObject &containerConfig, ErrorCode errorCode); + const QJsonObject &containerConfig, ErrorCode &errorCode); bool m_isAwg; QString m_serverConfigPath; diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index a1469184..786da47c 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -14,7 +14,7 @@ XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSh } QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode errorCode) + ErrorCode &errorCode) { QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), m_serverController->genVarsForScript(credentials, container, containerConfig)); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 62aa13b9..2acfdf71 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -13,7 +13,7 @@ public: XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent = nullptr); QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode errorCode); + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 2cb914ec..b66ef5c5 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -173,7 +173,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container, } QByteArray ServerController::getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials, const QString &path, - ErrorCode errorCode) + ErrorCode &errorCode) { errorCode = ErrorCode::NoError; @@ -618,7 +618,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential return vars; } -QString ServerController::checkSshConnection(const ServerCredentials &credentials, ErrorCode errorCode) +QString ServerController::checkSshConnection(const ServerCredentials &credentials, ErrorCode &errorCode) { QString stdOut; auto cbReadStdOut = [&](const QString &data, libssh::Client &) { diff --git a/client/core/controllers/serverController.h b/client/core/controllers/serverController.h index 9830a395..c87d1523 100644 --- a/client/core/controllers/serverController.h +++ b/client/core/controllers/serverController.h @@ -36,7 +36,7 @@ public: const QString &path, libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting); QByteArray getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials, const QString &path, - ErrorCode errorCode); + ErrorCode &errorCode); QString replaceVars(const QString &script, const Vars &vars); Vars genVarsForScript(const ServerCredentials &credentials, DockerContainer container = DockerContainer::None, @@ -50,7 +50,7 @@ public: const std::function &cbReadStdOut = nullptr, const std::function &cbReadStdErr = nullptr); - QString checkSshConnection(const ServerCredentials &credentials, ErrorCode errorCode); + QString checkSshConnection(const ServerCredentials &credentials, ErrorCode &errorCode); void cancelInstallation(); diff --git a/client/core/controllers/vpnConfigurationController.cpp b/client/core/controllers/vpnConfigurationController.cpp index f84919e7..23ed0804 100644 --- a/client/core/controllers/vpnConfigurationController.cpp +++ b/client/core/controllers/vpnConfigurationController.cpp @@ -77,7 +77,7 @@ ErrorCode VpnConfigurationsController::createProtocolConfigString(const bool isA QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair &dns, const QJsonObject &serverConfig, const QJsonObject &containerConfig, const DockerContainer container, - ErrorCode errorCode) + ErrorCode &errorCode) { QJsonObject vpnConfiguration {}; diff --git a/client/core/controllers/vpnConfigurationController.h b/client/core/controllers/vpnConfigurationController.h index d0328ec5..1b10d448 100644 --- a/client/core/controllers/vpnConfigurationController.h +++ b/client/core/controllers/vpnConfigurationController.h @@ -21,7 +21,7 @@ public slots: const DockerContainer container, const QJsonObject &containerConfig, const Proto protocol, QString &protocolConfigString); QJsonObject createVpnConfiguration(const QPair &dns, const QJsonObject &serverConfig, - const QJsonObject &containerConfig, const DockerContainer container, ErrorCode errorCode); + const QJsonObject &containerConfig, const DockerContainer container, ErrorCode &errorCode); static void updateContainerConfigAfterInstallation(const DockerContainer container, QJsonObject &containerConfig, const QString &stdOut); signals: diff --git a/client/core/defs.h b/client/core/defs.h index ba4fc7ad..87b03824 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -42,6 +42,7 @@ namespace amnezia UnknownError = 100, InternalError = 101, NotImplementedError = 102, + AmneziaServiceNotRunning = 103, // Server errors ServerCheckFailed = 200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 99b55fcd..2a14b3cd 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -11,6 +11,7 @@ QString errorString(ErrorCode code) { case(NoError): errorMessage = QObject::tr("No error"); break; case(UnknownError): errorMessage = QObject::tr("Unknown Error"); break; case(NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break; + case(AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break; // Server errors case(ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break; diff --git a/client/mozilla/networkwatcher.cpp b/client/mozilla/networkwatcher.cpp index 47fdb622..59caf1f2 100644 --- a/client/mozilla/networkwatcher.cpp +++ b/client/mozilla/networkwatcher.cpp @@ -171,9 +171,15 @@ void NetworkWatcher::unsecuredNetwork(const QString& networkName, } -QString NetworkWatcher::getCurrentTransport() { - auto type = m_impl->getTransportType(); - QMetaEnum metaEnum = QMetaEnum::fromType(); - return QString(metaEnum.valueToKey(type)) - .remove("TransportType_", Qt::CaseSensitive); +QNetworkInformation::Reachability NetworkWatcher::getReachability() { + if (m_simulatedDisconnection) { + return QNetworkInformation::Reachability::Disconnected; + } else if (QNetworkInformation::instance()) { + return QNetworkInformation::instance()->reachability(); + } + return QNetworkInformation::Reachability::Unknown; +} + +void NetworkWatcher::simulateDisconnection(bool simulatedDisconnection) { + m_simulatedDisconnection = simulatedDisconnection; } diff --git a/client/mozilla/networkwatcher.h b/client/mozilla/networkwatcher.h index 7c30416e..43536dc3 100644 --- a/client/mozilla/networkwatcher.h +++ b/client/mozilla/networkwatcher.h @@ -7,45 +7,50 @@ #include #include -#include +#include + class NetworkWatcherImpl; // This class watches for network changes to detect unsecured wifi. class NetworkWatcher final : public QObject { - Q_OBJECT - Q_DISABLE_COPY_MOVE(NetworkWatcher) + Q_OBJECT + Q_DISABLE_COPY_MOVE(NetworkWatcher) - public: - NetworkWatcher(); - ~NetworkWatcher(); +public: + NetworkWatcher(); + ~NetworkWatcher(); - void initialize(); + void initialize(); - // public for the inspector. - void unsecuredNetwork(const QString& networkName, const QString& networkId); + // Public for the Inspector. + void unsecuredNetwork(const QString& networkName, const QString& networkId); + // Used for the Inspector. simulateOffline = true to mock being disconnected, + // false to restore. + void simulateDisconnection(bool simulatedDisconnection); - QString getCurrentTransport(); + QNetworkInformation::Reachability getReachability(); - signals: - void networkChange(); +signals: + void networkChange(); - private: - void settingsChanged(); +private: + void settingsChanged(); - // void notificationClicked(NotificationHandler::Message message); +private: + bool m_active = false; + bool m_reportUnsecuredNetwork = false; - private: - bool m_active = false; - bool m_reportUnsecuredNetwork = false; + // Platform-specific implementation. + NetworkWatcherImpl* m_impl = nullptr; - // Platform-specific implementation. - NetworkWatcherImpl* m_impl = nullptr; + QMap m_networks; - QMap m_networks; + // This is used to connect NotificationHandler lazily. + bool m_firstNotification = true; - // This is used to connect NotificationHandler lazily. - bool m_firstNotification = true; + // Used to simulate network disconnection in the Inspector + bool m_simulatedDisconnection = false; }; #endif // NETWORKWATCHER_H diff --git a/client/mozilla/networkwatcherimpl.h b/client/mozilla/networkwatcherimpl.h index d156c0db..78117dca 100644 --- a/client/mozilla/networkwatcherimpl.h +++ b/client/mozilla/networkwatcherimpl.h @@ -5,50 +5,45 @@ #ifndef NETWORKWATCHERIMPL_H #define NETWORKWATCHERIMPL_H +#include #include class NetworkWatcherImpl : public QObject { - Q_OBJECT - Q_DISABLE_COPY_MOVE(NetworkWatcherImpl) + Q_OBJECT + Q_DISABLE_COPY_MOVE(NetworkWatcherImpl) - public: - NetworkWatcherImpl(QObject* parent) : QObject(parent) {} +public: + NetworkWatcherImpl(QObject* parent) : QObject(parent) {} - virtual ~NetworkWatcherImpl() = default; + virtual ~NetworkWatcherImpl() = default; - virtual void initialize() = 0; + virtual void initialize() = 0; - virtual void start() { m_active = true; } - virtual void stop() { m_active = false; } + virtual void start() { m_active = true; } + virtual void stop() { m_active = false; } - bool isActive() const { return m_active; } + bool isActive() const { return m_active; } - enum TransportType { - TransportType_Unknown = 0, - TransportType_Ethernet = 1, - TransportType_WiFi = 2, - TransportType_Cellular = 3, // In Case the API does not retun the gsm type - TransportType_Other = 4, // I.e USB thethering - TransportType_None = 5 // I.e Airplane Mode or no active network device - }; - Q_ENUM(TransportType); + enum TransportType { + TransportType_Unknown = 0, + TransportType_Ethernet = 1, + TransportType_WiFi = 2, + TransportType_Cellular = 3, // In Case the API does not retun the gsm type + TransportType_Other = 4, // I.e USB thethering + TransportType_None = 5 // I.e Airplane Mode or no active network device + }; + Q_ENUM(TransportType); - // Returns the current type of Network Connection - virtual TransportType getTransportType() = 0; +signals: + // Fires when the Device Connects to an unsecured Network + void unsecuredNetwork(const QString& networkName, const QString& networkId); + // Fires on when the connected WIFI Changes + // TODO: Only windows-networkwatcher has this, the other plattforms should + // too. + void networkChanged(QString newBSSID); - signals: - // Fires when the Device Connects to an unsecured Network - void unsecuredNetwork(const QString& networkName, const QString& networkId); - // Fires on when the connected WIFI Changes - // TODO: Only windows-networkwatcher has this, the other plattforms should - // too. - void networkChanged(QString newBSSID); - - // Fired when the Device changed the Type of Transport - void transportChanged(NetworkWatcherImpl::TransportType transportType); - - private: - bool m_active = false; +private: + bool m_active = false; }; #endif // NETWORKWATCHERIMPL_H diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index ce2aeb4c..0311fd0a 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -93,6 +93,7 @@ bool AndroidController::initialize() {"onServiceDisconnected", "()V", reinterpret_cast(onServiceDisconnected)}, {"onServiceError", "()V", reinterpret_cast(onServiceError)}, {"onVpnPermissionRejected", "()V", reinterpret_cast(onVpnPermissionRejected)}, + {"onNotificationStateChanged", "()V", reinterpret_cast(onNotificationStateChanged)}, {"onVpnStateChanged", "(I)V", reinterpret_cast(onVpnStateChanged)}, {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)}, @@ -173,14 +174,6 @@ QString AndroidController::openFile(const QString &filter) return fileName; } -void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec) -{ - callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V", - QJniObject::fromString(title).object(), - QJniObject::fromString(message).object(), - (jint) timerSec); -} - bool AndroidController::isCameraPresent() { return callActivityMethod("isCameraPresent", "()Z"); @@ -257,6 +250,16 @@ QPixmap AndroidController::getAppIcon(const QString &package, QSize *size, const return QPixmap::fromImage(image); } +bool AndroidController::isNotificationPermissionGranted() +{ + return callActivityMethod("isNotificationPermissionGranted", "()Z"); +} + +void AndroidController::requestNotificationPermission() +{ + callActivityMethod("requestNotificationPermission", "()V"); +} + // Moving log processing to the Android side jclass AndroidController::log; jmethodID AndroidController::logDebug; @@ -409,6 +412,15 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz) emit AndroidController::instance()->vpnPermissionRejected(); } +// static +void AndroidController::onNotificationStateChanged(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->notificationStateChanged(); +} + // static void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode) { diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 15de0ccc..d015dbe3 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -32,7 +32,6 @@ public: ErrorCode start(const QJsonObject &vpnConfig); void stop(); void resetLastServer(int serverIndex); - void setNotificationText(const QString &title, const QString &message, int timerSec); void saveFile(const QString &fileName, const QString &data); QString openFile(const QString &filter); bool isCameraPresent(); @@ -44,6 +43,8 @@ public: void minimizeApp(); QJsonArray getAppList(); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); + bool isNotificationPermissionGranted(); + void requestNotificationPermission(); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); @@ -54,6 +55,7 @@ signals: void serviceDisconnected(); void serviceError(); void vpnPermissionRejected(); + void notificationStateChanged(); void vpnStateChanged(ConnectionState state); void statisticsUpdated(quint64 rxBytes, quint64 txBytes); void fileOpened(QString uri); @@ -81,6 +83,7 @@ private: static void onServiceDisconnected(JNIEnv *env, jobject thiz); static void onServiceError(JNIEnv *env, jobject thiz); static void onVpnPermissionRejected(JNIEnv *env, jobject thiz); + static void onNotificationStateChanged(JNIEnv *env, jobject thiz); static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode); static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); diff --git a/client/platforms/android/android_notificationhandler.cpp b/client/platforms/android/android_notificationhandler.cpp deleted file mode 100644 index 2bd26b7f..00000000 --- a/client/platforms/android/android_notificationhandler.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "android_notificationhandler.h" -#include "platforms/android/android_controller.h" - -AndroidNotificationHandler::AndroidNotificationHandler(QObject* parent) - : NotificationHandler(parent) { -} -AndroidNotificationHandler::~AndroidNotificationHandler() { -} - -void AndroidNotificationHandler::notify(NotificationHandler::Message type, - const QString& title, - const QString& message, int timerMsec) { - Q_UNUSED(type); - qDebug() << "Send notification - " << message; - AndroidController::instance()->setNotificationText(title, message, - timerMsec / 1000); -} diff --git a/client/platforms/android/android_notificationhandler.h b/client/platforms/android/android_notificationhandler.h deleted file mode 100644 index e3e7325e..00000000 --- a/client/platforms/android/android_notificationhandler.h +++ /dev/null @@ -1,24 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef ANDROIDNOTIFICATIONHANDLER_H -#define ANDROIDNOTIFICATIONHANDLER_H - -#include "ui/notificationhandler.h" - -#include - -class AndroidNotificationHandler final : public NotificationHandler { - Q_DISABLE_COPY_MOVE(AndroidNotificationHandler) - - public: - AndroidNotificationHandler(QObject* parent); - ~AndroidNotificationHandler(); - - protected: - void notify(Message type, const QString& title, const QString& message, - int timerMsec) override; -}; - -#endif // ANDROIDNOTIFICATIONHANDLER_H diff --git a/client/platforms/dummy/dummynetworkwatcher.cpp b/client/platforms/dummy/dummynetworkwatcher.cpp index b4291494..fd7db344 100644 --- a/client/platforms/dummy/dummynetworkwatcher.cpp +++ b/client/platforms/dummy/dummynetworkwatcher.cpp @@ -8,11 +8,11 @@ DummyNetworkWatcher::DummyNetworkWatcher(QObject* parent) : NetworkWatcherImpl(parent) { - MZ_COUNT_CTOR(DummyNetworkWatcher); + MZ_COUNT_CTOR(DummyNetworkWatcher); } DummyNetworkWatcher::~DummyNetworkWatcher() { - MZ_COUNT_DTOR(DummyNetworkWatcher); + MZ_COUNT_DTOR(DummyNetworkWatcher); } void DummyNetworkWatcher::initialize() {} diff --git a/client/platforms/dummy/dummynetworkwatcher.h b/client/platforms/dummy/dummynetworkwatcher.h index 4e6d2a66..7374ab74 100644 --- a/client/platforms/dummy/dummynetworkwatcher.h +++ b/client/platforms/dummy/dummynetworkwatcher.h @@ -8,15 +8,11 @@ #include "networkwatcherimpl.h" class DummyNetworkWatcher final : public NetworkWatcherImpl { - public: - DummyNetworkWatcher(QObject* parent); - ~DummyNetworkWatcher(); +public: + DummyNetworkWatcher(QObject* parent); + ~DummyNetworkWatcher(); - void initialize() override; - - NetworkWatcherImpl::TransportType getTransportType() override { - return TransportType_Other; - }; + void initialize() override; }; #endif // DUMMYNETWORKWATCHER_H diff --git a/client/platforms/ios/iosnetworkwatcher.h b/client/platforms/ios/iosnetworkwatcher.h index 5b779a87..70609e1f 100644 --- a/client/platforms/ios/iosnetworkwatcher.h +++ b/client/platforms/ios/iosnetworkwatcher.h @@ -15,7 +15,6 @@ class IOSNetworkWatcher : public NetworkWatcherImpl { ~IOSNetworkWatcher(); void initialize() override; - NetworkWatcherImpl::TransportType getTransportType() override; private: NetworkWatcherImpl::TransportType toTransportType(nw_path_t path); diff --git a/client/platforms/ios/iosnetworkwatcher.mm b/client/platforms/ios/iosnetworkwatcher.mm index 9c6ca411..720b303b 100644 --- a/client/platforms/ios/iosnetworkwatcher.mm +++ b/client/platforms/ios/iosnetworkwatcher.mm @@ -37,16 +37,6 @@ void IOSNetworkWatcher::initialize() { //TODO IMPL FOR AMNEZIA } -NetworkWatcherImpl::TransportType IOSNetworkWatcher::getTransportType() { - //TODO IMPL FOR AMNEZIA - - if (m_observableConnection != nil) { - return m_currentVPNTransport; - } - // If we don't have an open tunnel-observer, m_currentVPNTransport is probably wrong. - return NetworkWatcherImpl::TransportType_Unknown; -} - NetworkWatcherImpl::TransportType IOSNetworkWatcher::toTransportType(nw_path_t path) { if (path == nil) { return NetworkWatcherImpl::TransportType_Unknown; diff --git a/client/platforms/linux/linuxnetworkwatcher.h b/client/platforms/linux/linuxnetworkwatcher.h old mode 100644 new mode 100755 index ed8c76ba..d67602f8 --- a/client/platforms/linux/linuxnetworkwatcher.h +++ b/client/platforms/linux/linuxnetworkwatcher.h @@ -22,11 +22,6 @@ class LinuxNetworkWatcher final : public NetworkWatcherImpl { void start() override; - NetworkWatcherImpl::TransportType getTransportType() { - // TODO: Find out how to do that on linux generally. (VPN-2382) - return NetworkWatcherImpl::TransportType_Unknown; - }; - signals: void checkDevicesInThread(); diff --git a/client/platforms/macos/daemon/macosroutemonitor.cpp b/client/platforms/macos/daemon/macosroutemonitor.cpp index 9f1da4ec..395f008a 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.cpp +++ b/client/platforms/macos/daemon/macosroutemonitor.cpp @@ -95,6 +95,11 @@ void MacosRouteMonitor::handleRtmDelete(const struct rt_msghdr* rtm, !(rtm->rtm_addrs & RTA_NETMASK) || (addrlist.count() < 3)) { return; } + // Ignore interface-scoped routes, we want to find the default route to the + // internet in the global scope. + if (rtm->rtm_flags & RTF_IFSCOPE) { + return; + } // Check for a default route, which should have a netmask of zero. const struct sockaddr* sa = @@ -156,6 +161,11 @@ void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm, !(rtm->rtm_addrs & RTA_NETMASK) || (addrlist.count() < 3)) { return; } + // Ignore interface-scoped routes, we want to find the default route to the + // internet in the global scope. + if (rtm->rtm_flags & RTF_IFSCOPE) { + return; + } // Ignore route changes that we caused, or routes on the tunnel interface. if (rtm->rtm_index == m_ifindex) { return; diff --git a/client/platforms/macos/macospingsender.cpp b/client/platforms/macos/macospingsender.cpp index 3b5a09b0..8f108259 100644 --- a/client/platforms/macos/macospingsender.cpp +++ b/client/platforms/macos/macospingsender.cpp @@ -83,7 +83,7 @@ void MacOSPingSender::sendPing(const QHostAddress& dest, quint16 sequence) { packet.icmp_seq = htons(sequence); packet.icmp_cksum = inetChecksum(&packet, sizeof(packet)); - if (sendto(m_socket, (char*)&packet, sizeof(packet), 0, + if (sendto(m_socket, (char*)&packet, sizeof(packet), MSG_NOSIGNAL, (struct sockaddr*)&addr, sizeof(addr)) != sizeof(packet)) { logger.error() << "ping sending failed:" << strerror(errno); emit criticalPingError(); @@ -107,9 +107,9 @@ void MacOSPingSender::socketReady() { iov.iov_base = packet; iov.iov_len = IP_MAXPACKET; - ssize_t rc = recvmsg(m_socket, &msg, MSG_DONTWAIT); + ssize_t rc = recvmsg(m_socket, &msg, MSG_DONTWAIT | MSG_NOSIGNAL); if (rc <= 0) { - logger.error() << "Recvmsg failed"; + logger.error() << "Recvmsg failed:" << strerror(errno); return; } diff --git a/client/platforms/macos/macosutils.mm b/client/platforms/macos/macosutils.mm index cbe30583..177ed2a2 100644 --- a/client/platforms/macos/macosutils.mm +++ b/client/platforms/macos/macosutils.mm @@ -41,11 +41,33 @@ void MacOSUtils::enableLoginItem(bool startAtBoot) { Q_ASSERT(appId); NSString* loginItemAppId = - QString("%1.login-item").arg(QString::fromNSString(appId)).toNSString(); - CFStringRef cfs = (__bridge CFStringRef)loginItemAppId; + QString("%1.login-item").arg(QString::fromNSString(appId)).toNSString(); - Boolean ok = SMLoginItemSetEnabled(cfs, startAtBoot ? YES : NO); - logger.debug() << "Result: " << ok; + // For macOS 13 and beyond, register() and unregister() methods + // are used for managing login items since SMLoginItemSetEnabled() is deprecated. + // For versions prior to macOS 13, SMLoginItemSetEnabled() is used. + if (@available(macOS 13, *)) { + // Use register() or unregister() based on the startAtBoot flag + NSError* error = nil; + + if (startAtBoot) { + if (![[SMAppService mainAppService] registerAndReturnError: & error]) { + logger.error() << "Failed to register Amnezia VPN LoginItem: " << error.localizedDescription; + } else { + logger.debug() << "Amnezia VPN LoginItem registered successfully."; + } + } else { + if (![[SMAppService mainAppService] unregisterAndReturnError: & error]) { + logger.error() << "Failed to unregister Amnezia VPN LoginItem: " << error.localizedDescription; + } else { + logger.debug() << "LoginItem unregistered successfully."; + } + } + } else { + CFStringRef cfs = (__bridge CFStringRef) loginItemAppId; + Boolean ok = SMLoginItemSetEnabled(cfs, startAtBoot ? YES : NO); + logger.debug() << "Result: " << ok; + } } namespace { diff --git a/client/platforms/windows/daemon/dnsutilswindows.cpp b/client/platforms/windows/daemon/dnsutilswindows.cpp index a6485529..1e807834 100644 --- a/client/platforms/windows/daemon/dnsutilswindows.cpp +++ b/client/platforms/windows/daemon/dnsutilswindows.cpp @@ -4,8 +4,11 @@ #include "dnsutilswindows.h" +#include #include #include +#include +#include #include #include @@ -39,30 +42,27 @@ DnsUtilsWindows::~DnsUtilsWindows() { bool DnsUtilsWindows::updateResolvers(const QString& ifname, const QList& resolvers) { - NET_LUID luid; - if (ConvertInterfaceAliasToLuid((wchar_t*)ifname.utf16(), &luid) != 0) { + MIB_IF_ROW2 entry; + if (ConvertInterfaceAliasToLuid((wchar_t*)ifname.utf16(), + &entry.InterfaceLuid) != 0) { logger.error() << "Failed to resolve LUID for" << ifname; return false; } - m_luid = luid.Value; + if (GetIfEntry2(&entry) != NO_ERROR) { + logger.error() << "Failed to resolve interface for" << ifname; + return false; + } + m_luid = entry.InterfaceLuid.Value; logger.debug() << "Configuring DNS for" << ifname; if (m_setInterfaceDnsSettingsProcAddr == nullptr) { - return updateResolversNetsh(resolvers); + return updateResolversNetsh(entry.InterfaceIndex, resolvers); } - return updateResolversWin32(resolvers); + return updateResolversWin32(entry.InterfaceGuid, resolvers); } bool DnsUtilsWindows::updateResolversWin32( - const QList& resolvers) { - GUID guid; - NET_LUID luid; - luid.Value = m_luid; - if (ConvertInterfaceLuidToGuid(&luid, &guid) != NO_ERROR) { - logger.error() << "Failed to resolve GUID"; - return false; - } - + GUID guid, const QList& resolvers) { QStringList v4resolvers; QStringList v6resolvers; for (const QHostAddress& addr : resolvers) { @@ -113,16 +113,8 @@ constexpr const char* netshAddTemplate = "interface %1 add dnsservers name=%2 address=%3 validate=no\r\n"; bool DnsUtilsWindows::updateResolversNetsh( - const QList& resolvers) { + int ifindex, const QList& resolvers) { QProcess netsh; - NET_LUID luid; - NET_IFINDEX ifindex; - luid.Value = m_luid; - if (ConvertInterfaceLuidToIndex(&luid, &ifindex) != NO_ERROR) { - logger.error() << "Failed to resolve GUID"; - return false; - } - netsh.setProgram("netsh"); netsh.start(); if (!netsh.waitForStarted(WINDOWS_NETSH_TIMEOUT_MSEC)) { @@ -166,12 +158,26 @@ bool DnsUtilsWindows::updateResolversNetsh( bool DnsUtilsWindows::restoreResolvers() { if (m_luid == 0) { + // If the DNS hasn't been configured, there is nothing to restore. return true; } + MIB_IF_ROW2 entry; + DWORD error; + entry.InterfaceLuid.Value = m_luid; + error = GetIfEntry2(&entry); + if (error == ERROR_FILE_NOT_FOUND) { + // If the interface no longer exists, there is nothing to restore. + return true; + } + if (error != NO_ERROR) { + logger.error() << "Failed to resolve interface entry:" << error; + return false; + } + QList empty; if (m_setInterfaceDnsSettingsProcAddr == nullptr) { - return updateResolversNetsh(empty); + return updateResolversNetsh(entry.InterfaceIndex, empty); } - return updateResolversWin32(empty); + return updateResolversWin32(entry.InterfaceGuid, empty); } diff --git a/client/platforms/windows/daemon/dnsutilswindows.h b/client/platforms/windows/daemon/dnsutilswindows.h index 7d0573e4..f9b7e94b 100644 --- a/client/platforms/windows/daemon/dnsutilswindows.h +++ b/client/platforms/windows/daemon/dnsutilswindows.h @@ -27,8 +27,8 @@ class DnsUtilsWindows final : public DnsUtils { quint64 m_luid = 0; DWORD (*m_setInterfaceDnsSettingsProcAddr)(GUID, const void*) = nullptr; - bool updateResolversWin32(const QList& resolvers); - bool updateResolversNetsh(const QList& resolvers); + bool updateResolversWin32(GUID, const QList& resolvers); + bool updateResolversNetsh(int ifindex, const QList& resolvers); }; #endif // DNSUTILSWINDOWS_H diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h index 782c6814..d3e81a18 100644 --- a/client/platforms/windows/daemon/windowsdaemon.h +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -38,7 +38,6 @@ class WindowsDaemon final : public Daemon { Inactive, }; - State m_state = Inactive; int m_inetAdapterIndex = -1; WireguardUtilsWindows* m_wgutils = nullptr; diff --git a/client/platforms/windows/daemon/windowsroutemonitor.cpp b/client/platforms/windows/daemon/windowsroutemonitor.cpp index e60a9178..69967526 100644 --- a/client/platforms/windows/daemon/windowsroutemonitor.cpp +++ b/client/platforms/windows/daemon/windowsroutemonitor.cpp @@ -114,7 +114,7 @@ void WindowsRouteMonitor::updateValidInterfaces(int family) { void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, void* ptable) { PMIB_IPFORWARD_TABLE2 table = reinterpret_cast(ptable); - SOCKADDR_INET nexthop = {0}; + SOCKADDR_INET nexthop = {}; quint64 bestLuid = 0; int bestMatch = -1; ULONG bestMetric = ULONG_MAX; diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp index eb93bd26..39941933 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.cpp +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace { Logger logger("WindowsSplitTunnel"); @@ -29,6 +30,9 @@ WindowsSplitTunnel::WindowsSplitTunnel(QObject* parent) : QObject(parent) { uninstallDriver(); return; } + + m_tries = 0; + if (!isInstalled()) { logger.debug() << "Driver is not Installed, doing so"; auto handle = installDriver(); @@ -59,10 +63,10 @@ void WindowsSplitTunnel::initDriver() { m_driver = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); ; - - if (m_driver == INVALID_HANDLE_VALUE) { + if (m_driver == INVALID_HANDLE_VALUE && m_tries < 500) { WindowsUtils::windowsLog("Failed to open Driver: "); - + m_tries++; + Sleep(100); // If the handle is not present, try again after the serivce has started; auto driver_manager = WindowsServiceManager(DRIVER_SERVICE_NAME); QObject::connect(&driver_manager, &WindowsServiceManager::serviceStarted, diff --git a/client/platforms/windows/daemon/windowssplittunnel.h b/client/platforms/windows/daemon/windowssplittunnel.h index efe84395..466036d6 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.h +++ b/client/platforms/windows/daemon/windowssplittunnel.h @@ -158,6 +158,7 @@ class WindowsSplitTunnel final : public QObject { constexpr static const auto MV_SERVICE_NAME = L"MullvadVPN"; DRIVER_STATE getState(); + int m_tries; // Initializes the WFP Sublayer bool initSublayer(); diff --git a/client/platforms/windows/daemon/windowstunnellogger.cpp b/client/platforms/windows/daemon/windowstunnellogger.cpp index 7194f0cd..1d5e26b8 100644 --- a/client/platforms/windows/daemon/windowstunnellogger.cpp +++ b/client/platforms/windows/daemon/windowstunnellogger.cpp @@ -39,7 +39,7 @@ Logger logger("tunnel.dll"); WindowsTunnelLogger::WindowsTunnelLogger(const QString& filename, QObject* parent) - : QObject(parent), m_logfile(filename, this), m_timer(this) { + : QObject(parent), m_timer(this), m_logfile(filename, this) { MZ_COUNT_CTOR(WindowsTunnelLogger); m_startTime = QDateTime::currentMSecsSinceEpoch() * 1000000; diff --git a/client/platforms/windows/daemon/windowstunnelservice.cpp b/client/platforms/windows/daemon/windowstunnelservice.cpp index f873203b..ae358dd4 100644 --- a/client/platforms/windows/daemon/windowstunnelservice.cpp +++ b/client/platforms/windows/daemon/windowstunnelservice.cpp @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "WindowsTunnelService.h" +#include "windowstunnelservice.h" #include @@ -30,12 +30,22 @@ static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus); WindowsTunnelService::WindowsTunnelService(QObject* parent) : QObject(parent) { MZ_COUNT_CTOR(WindowsTunnelService); + logger.debug() << "WindowsTunnelService created."; m_scm = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); if (m_scm == nullptr) { WindowsUtils::windowsLog("Failed to open SCManager"); } + // Is the service already running? Terminate it. + SC_HANDLE service = + OpenService((SC_HANDLE)m_scm, TUNNEL_SERVICE_NAME, SERVICE_ALL_ACCESS); + if (service != nullptr) { + logger.info() << "Tunnel already exists. Terminating it."; + stopAndDeleteTunnelService(service); + CloseServiceHandle(service); + } + connect(&m_timer, &QTimer::timeout, this, &WindowsTunnelService::timeout); } diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp index 395859aa..c0a14dda 100644 --- a/client/platforms/windows/windowscommons.cpp +++ b/client/platforms/windows/windowscommons.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -19,9 +20,9 @@ #include "logger.h" #include "platforms/windows/windowsutils.h" -#define TUNNEL_SERVICE_NAME L"WireGuardTunnel$amnvpn" - constexpr const char* VPN_NAME = "AmneziaVPN"; +constexpr const char* WIREGUARD_DIR = "WireGuard"; +constexpr const char* DATA_DIR = "Data"; namespace { Logger logger("WindowsCommons"); @@ -67,27 +68,67 @@ QString WindowsCommons::tunnelConfigFile() { return QString(); } +// static QString WindowsCommons::tunnelLogFile() { - QStringList paths = - QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + static QString tunnelLogFilePath = getTunnelLogFilePath(); + return tunnelLogFilePath; +} - for (const QString& path : paths) { - QDir dir(path); - if (!dir.exists()) { - continue; +// static +QString WindowsCommons::getProgramFilesPath() { + wchar_t* path = nullptr; + + if (SUCCEEDED( + SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &path))) { + auto guard = qScopeGuard([&] { CoTaskMemFree(path); }); + return QString::fromWCharArray(path); + } + return QString(); +} + +// static +QString WindowsCommons::getTunnelLogFilePath() { + // Return WireGuard's log file path, "\Program Files\WireGuard\Data\log.bin", + // if the directory path exists + auto programFilesPath = getProgramFilesPath(); + if (!programFilesPath.isEmpty()) { + QDir programFilesDir(programFilesPath); + + if (programFilesDir.exists()) { + QDir wireGuardDir(programFilesDir.filePath(WIREGUARD_DIR)); + + if (wireGuardDir.exists()) { + QDir wireGuardDataDir(wireGuardDir.filePath(DATA_DIR)); + + if (wireGuardDataDir.exists()) { + return wireGuardDataDir.filePath("log.bin"); + } + } } - - QDir vpnDir(dir.filePath(VPN_NAME)); - if (!vpnDir.exists()) { - continue; - } - - return vpnDir.filePath("log.bin"); } + logger.debug() << "Failed to find WireGuard Tunnel log file"; return QString(); } +// static +int WindowsCommons::AdapterIndexTo(const QHostAddress& dst) { + logger.debug() << "Getting Current Internet Adapter that routes to" + << logger.sensitive(dst.toString()); + quint32_be ipBigEndian; + quint32 ip = dst.toIPv4Address(); + qToBigEndian(ip, &ipBigEndian); + _MIB_IPFORWARDROW routeInfo; + auto result = GetBestRoute(ipBigEndian, 0, &routeInfo); + if (result != NO_ERROR) { + return -1; + } + auto adapter = + QNetworkInterface::interfaceFromIndex(routeInfo.dwForwardIfIndex); + logger.debug() << "Internet Adapter:" << adapter.name(); + return routeInfo.dwForwardIfIndex; +} + // static int WindowsCommons::VPNAdapterIndex() { // For someReason QNetworkInterface::fromName(MozillaVPN) does not work >:( @@ -102,7 +143,7 @@ int WindowsCommons::VPNAdapterIndex() { // Static QString WindowsCommons::getCurrentPath() { - QByteArray buffer(2048, 0xFF); + QByteArray buffer(2048, 0xFFu); auto ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size()); if (ok == ERROR_INSUFFICIENT_BUFFER) { diff --git a/client/platforms/windows/windowscommons.h b/client/platforms/windows/windowscommons.h index eb3e675c..ec3b779e 100644 --- a/client/platforms/windows/windowscommons.h +++ b/client/platforms/windows/windowscommons.h @@ -19,9 +19,14 @@ class WindowsCommons final { // Returns the Interface Index of the VPN Adapter static int VPNAdapterIndex(); - + // Returns the Interface Index that could Route to dst + static int AdapterIndexTo(const QHostAddress& dst); // Returns the Path of the Current process static QString getCurrentPath(); + + private: + static QString getTunnelLogFilePath(); + static QString getProgramFilesPath(); }; #endif // WINDOWSCOMMONS_H diff --git a/client/platforms/windows/windowsnetworkwatcher.cpp b/client/platforms/windows/windowsnetworkwatcher.cpp index 2de5a726..7b1096b0 100644 --- a/client/platforms/windows/windowsnetworkwatcher.cpp +++ b/client/platforms/windows/windowsnetworkwatcher.cpp @@ -4,6 +4,7 @@ #include "windowsnetworkwatcher.h" +#include #include #include "leakdetector.h" @@ -136,9 +137,4 @@ void WindowsNetworkWatcher::processWlan(PWLAN_NOTIFICATION_DATA data) { logger.debug() << "Unsecure network:" << logger.sensitive(ssid) << "id:" << logger.sensitive(bssid); emit unsecuredNetwork(ssid, bssid); -} - -NetworkWatcherImpl::TransportType WindowsNetworkWatcher::getTransportType() { - // TODO: Implement this once we update to Qt6.3 (VPN-3511) - return TransportType_Other; -} +} \ No newline at end of file diff --git a/client/platforms/windows/windowsnetworkwatcher.h b/client/platforms/windows/windowsnetworkwatcher.h index 29b99808..1b153746 100644 --- a/client/platforms/windows/windowsnetworkwatcher.h +++ b/client/platforms/windows/windowsnetworkwatcher.h @@ -17,8 +17,6 @@ class WindowsNetworkWatcher final : public NetworkWatcherImpl { void initialize() override; - NetworkWatcherImpl::TransportType getTransportType() override; - private: static void wlanCallback(PWLAN_NOTIFICATION_DATA data, PVOID context); diff --git a/client/platforms/windows/windowspingsender.cpp b/client/platforms/windows/windowspingsender.cpp index ad78ad31..8b07a80f 100644 --- a/client/platforms/windows/windowspingsender.cpp +++ b/client/platforms/windows/windowspingsender.cpp @@ -7,6 +7,8 @@ #include #include #include +#include + // Note: This important must come after the previous three. // clang-format off #include @@ -16,17 +18,58 @@ #include "leakdetector.h" #include "logger.h" -#include "windowscommons.h" #include "platforms/windows/windowsutils.h" +#include "windowscommons.h" #pragma comment(lib, "Ws2_32") +/* + * On 64 Bit systems we need to use another struct. + */ +#ifdef _WIN64 +using MZ_ICMP_ECHO_REPLY = ICMP_ECHO_REPLY32; +#else +using MZ_ICMP_ECHO_REPLY = ICMP_ECHO_REPLY; +#endif + constexpr WORD WindowsPingPayloadSize = sizeof(quint16); +constexpr size_t ICMP_ERR_SIZE = 8; +/* + * IcmpSendEcho2 expects us to provide a Buffer that is + * at least this size + */ +constexpr size_t MinimumReplyBufferSize = + sizeof(ICMP_ECHO_REPLY) + WindowsPingPayloadSize + ICMP_ERR_SIZE + + sizeof(IO_STATUS_BLOCK); +/** + * ICMP_ECHO_REPLY32 is smaller than ICMP_ECHO_REPLY, so if we use that due to + * binary compat Windows will add some padding. + */ +constexpr auto reply_padding = + sizeof(ICMP_ECHO_REPLY) - sizeof(MZ_ICMP_ECHO_REPLY); + +// Disable Packing, so the compiler does not add padding in this struct between +// different sized types. +#pragma pack(push, 1) +struct ICMP_ECHO_REPLY_BUFFER { + MZ_ICMP_ECHO_REPLY reply; + std::array padding; + quint16 payload; + std::array icmp_error; + IO_STATUS_BLOCK status; +}; +#pragma pack(pop) + +// If the Size is not the MinimumReplyBufferSize, the compiler added +// padding, so the fields will not be properly aligned with +// what IcmpSendEcho2 will write. +static_assert(sizeof(ICMP_ECHO_REPLY_BUFFER) == MinimumReplyBufferSize, + "Fulfills the size requirements"); struct WindowsPingSenderPrivate { HANDLE m_handle; HANDLE m_event; - unsigned char m_buffer[sizeof(ICMP_ECHO_REPLY) + WindowsPingPayloadSize + 8]; + ICMP_ECHO_REPLY_BUFFER m_replyBuffer; }; namespace { @@ -58,7 +101,7 @@ WindowsPingSender::WindowsPingSender(const QHostAddress& source, QObject::connect(m_notifier, &QWinEventNotifier::activated, this, &WindowsPingSender::pingEventReady); - memset(m_private->m_buffer, 0, sizeof(m_private->m_buffer)); + m_private->m_replyBuffer = {}; } WindowsPingSender::~WindowsPingSender() { @@ -86,16 +129,33 @@ void WindowsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) { quint32 v4dst = dest.toIPv4Address(); if (m_source.isNull()) { - IcmpSendEcho2(m_private->m_handle, m_private->m_event, nullptr, nullptr, - qToBigEndian(v4dst), &sequence, sizeof(sequence), - nullptr, m_private->m_buffer, sizeof(m_private->m_buffer), - 10000); + IcmpSendEcho2(m_private->m_handle, // IcmpHandle, + m_private->m_event, // Event + nullptr, // ApcRoutine + nullptr, // ApcContext + qToBigEndian(v4dst), // DestinationAddress + &sequence, // RequestData + sizeof(sequence), // RequestSize + nullptr, // RequestOptions + &m_private->m_replyBuffer, // [OUT] ReplyBuffer + sizeof(m_private->m_replyBuffer), // ReplySize + 10000 // Timeout + ); } else { quint32 v4src = m_source.toIPv4Address(); - IcmpSendEcho2Ex(m_private->m_handle, m_private->m_event, nullptr, nullptr, - qToBigEndian(v4src), qToBigEndian(v4dst), - &sequence, sizeof(sequence), nullptr, m_private->m_buffer, - sizeof(m_private->m_buffer), 10000); + IcmpSendEcho2Ex(m_private->m_handle, // IcmpHandle + m_private->m_event, // Event + nullptr, // ApcRoutine + nullptr, // ApcContext + qToBigEndian(v4src), // SourceAddress + qToBigEndian(v4dst), // DestinationAddress + &sequence, // RequestData + sizeof(sequence), // RequestSize + nullptr, // RequestOptions + &m_private->m_replyBuffer, // [OUT] ReplyBuffer + sizeof(m_private->m_replyBuffer), // ReplySize + 10000 // Timeout + ); } DWORD status = GetLastError(); @@ -108,8 +168,11 @@ void WindowsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) { } void WindowsPingSender::pingEventReady() { - DWORD replyCount = - IcmpParseReplies(m_private->m_buffer, sizeof(m_private->m_buffer)); + // Cleanup all data once we're done with m_replyBuffer. + const auto guard = qScopeGuard([this]() { m_private->m_replyBuffer = {}; }); + + DWORD replyCount = IcmpParseReplies(&m_private->m_replyBuffer, + sizeof(m_private->m_replyBuffer)); if (replyCount == 0) { DWORD error = GetLastError(); if (error == IP_REQ_TIMED_OUT) { @@ -120,14 +183,25 @@ void WindowsPingSender::pingEventReady() { << " Message: " << errmsg; return; } - - const ICMP_ECHO_REPLY* replies = (const ICMP_ECHO_REPLY*)m_private->m_buffer; - for (DWORD i = 0; i < replyCount; i++) { - if (replies[i].DataSize < sizeof(quint16)) { - continue; - } - quint16 sequence; - memcpy(&sequence, replies[i].Data, sizeof(quint16)); - emit recvPing(sequence); + // We only allocated for one reply, so more should be impossible. + if (replyCount != 1) { + logger.error() << "Invalid amount of responses recieved"; + return; } + if (m_private->m_replyBuffer.reply.Data == nullptr) { + logger.error() << "Did get a ping response without payload"; + return; + } + // Assert that the (void*) pointer of Data is pointing + // to our ReplyBuffer payload. + if (m_private->m_replyBuffer.reply.Data == nullptr) { + logger.error() << "Did get a ping response without payload"; + return; + } + // Assert that the (void*) pointer of Data is pointing + // to our ReplyBuffer payload. + assert(m_private->m_replyBuffer.reply.Data == + static_cast(&m_private->m_replyBuffer.payload)); + + emit recvPing(m_private->m_replyBuffer.payload); } diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 35fec6b8..f1351b2e 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1088,7 +1088,17 @@ Already installed containers were found on the server. All installed containers Язык - + + Enable notifications + Включить уведомления + + + + Enable notifications to show the VPN state in the status bar + Включить уведомления для отображения статуса VPN в строке состояния + + + Logging Логирование diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index 1fe5f489..2308f7ad 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -9,6 +9,7 @@ #include "core/controllers/vpnConfigurationController.h" #include "core/errorstrings.h" +#include "version.h" ConnectionController::ConnectionController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, @@ -36,6 +37,14 @@ ConnectionController::ConnectionController(const QSharedPointer &s void ConnectionController::openConnection() { +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) + { + emit connectionErrorOccurred(errorString(ErrorCode::AmneziaServiceNotRunning)); + return; + } +#endif + int serverIndex = m_serversModel->getDefaultServerIndex(); QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 31ca2607..aceac551 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -30,6 +30,9 @@ SettingsController::SettingsController(const QSharedPointer &serve { m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH); checkIfNeedDisableLogs(); +#ifdef Q_OS_ANDROID + connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged); +#endif } void SettingsController::toggleAmneziaDns(bool enable) @@ -233,3 +236,19 @@ void SettingsController::toggleKillSwitch(bool enable) { m_settings->setKillSwitchEnabled(enable); } + +bool SettingsController::isNotificationPermissionGranted() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->isNotificationPermissionGranted(); +#else + return true; +#endif +} + +void SettingsController::requestNotificationPermission() +{ +#ifdef Q_OS_ANDROID + AndroidController::instance()->requestNotificationPermission(); +#endif +} diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index ac84856d..43ad10e8 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -23,6 +23,7 @@ public: Q_PROPERTY(QString primaryDns READ getPrimaryDns WRITE setPrimaryDns NOTIFY primaryDnsChanged) Q_PROPERTY(QString secondaryDns READ getSecondaryDns WRITE setSecondaryDns NOTIFY secondaryDnsChanged) Q_PROPERTY(bool isLoggingEnabled READ isLoggingEnabled WRITE toggleLogging NOTIFY loggingStateChanged) + Q_PROPERTY(bool isNotificationPermissionGranted READ isNotificationPermissionGranted NOTIFY onNotificationStateChanged) public slots: void toggleAmneziaDns(bool enable); @@ -66,6 +67,9 @@ public slots: bool isKillSwitchEnabled(); void toggleKillSwitch(bool enable); + bool isNotificationPermissionGranted(); + void requestNotificationPermission(); + signals: void primaryDnsChanged(); void secondaryDnsChanged(); @@ -83,6 +87,8 @@ signals: void loggingDisableByWatcher(); + void onNotificationStateChanged(); + private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; diff --git a/client/ui/notificationhandler.cpp b/client/ui/notificationhandler.cpp index 1f81c2c2..5efb45c4 100644 --- a/client/ui/notificationhandler.cpp +++ b/client/ui/notificationhandler.cpp @@ -7,10 +7,7 @@ #if defined(Q_OS_IOS) # include "platforms/ios/iosnotificationhandler.h" -#elif defined(Q_OS_ANDROID) -# include "platforms/android/android_notificationhandler.h" #else - # include "systemtray_notificationhandler.h" #endif @@ -18,8 +15,6 @@ NotificationHandler* NotificationHandler::create(QObject* parent) { #if defined(Q_OS_IOS) return new IOSNotificationHandler(parent); -#elif defined(Q_OS_ANDROID) - return new AndroidNotificationHandler(parent); #else # if defined(Q_OS_LINUX) diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index a6472656..2243915f 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -15,43 +15,9 @@ PageType { defaultActiveFocusItem: focusItem - function getNextComponentInFocusChain(componentId) { - const componentsList = [focusItem, - backButton, - switcher, - switcherAutoStart, - switcherAutoConnect, - switcherStartMinimized, - labelWithButtonLanguage, - labelWithButtonLogging, - labelWithButtonReset, - ] - - const idx = componentsList.indexOf(componentId) - - if (idx === -1) { - return null - } - - let nextIndex = idx + 1 - if (nextIndex >= componentsList.length) { - nextIndex = 0 - } - - if (componentsList[nextIndex].visible) { - if ((nextIndex) >= 6) { - return componentsList[nextIndex].rightButton - } else { - return componentsList[nextIndex] - } - } else { - return getNextComponentInFocusChain(componentsList[nextIndex]) - } - } - Item { id: focusItem - KeyNavigation.tab: root.getNextComponentInFocusChain(focusItem) + KeyNavigation.tab: backButton onFocusChanged: { if (focusItem.activeFocus) { @@ -68,7 +34,7 @@ PageType { anchors.right: parent.right anchors.topMargin: 20 - KeyNavigation.tab: root.getNextComponentInFocusChain(backButton) + KeyNavigation.tab: GC.isMobile() ? switcher : switcherAutoStart } FlickableType { @@ -108,7 +74,8 @@ PageType { } } - KeyNavigation.tab: root.getNextComponentInFocusChain(switcher) + KeyNavigation.tab: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted ? + labelWithButtonNotification.rightButton : labelWithButtonLanguage.rightButton parentFlickable: fl } @@ -116,6 +83,27 @@ PageType { visible: GC.isMobile() } + LabelWithButtonType { + id: labelWithButtonNotification + visible: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted + Layout.fillWidth: true + + text: qsTr("Enable notifications") + descriptionText: qsTr("Enable notifications to show the VPN state in the status bar") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + KeyNavigation.tab: labelWithButtonLanguage.rightButton + parentFlickable: fl + + clickedFunction: function() { + SettingsController.requestNotificationPermission() + } + } + + DividerType { + visible: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted + } + SwitcherType { id: switcherAutoStart visible: !GC.isMobile() @@ -126,7 +114,7 @@ PageType { text: qsTr("Auto start") descriptionText: qsTr("Launch the application every time the device is starts") - KeyNavigation.tab: root.getNextComponentInFocusChain(switcherAutoStart) + KeyNavigation.tab: switcherAutoConnect parentFlickable: fl checked: SettingsController.isAutoStartEnabled() @@ -151,7 +139,7 @@ PageType { text: qsTr("Auto connect") descriptionText: qsTr("Connect to VPN on app start") - KeyNavigation.tab: root.getNextComponentInFocusChain(switcherAutoConnect) + KeyNavigation.tab: switcherStartMinimized parentFlickable: fl checked: SettingsController.isAutoConnectEnabled() @@ -176,7 +164,7 @@ PageType { text: qsTr("Start minimized") descriptionText: qsTr("Launch application minimized") - KeyNavigation.tab: root.getNextComponentInFocusChain(switcherStartMinimized) + KeyNavigation.tab: labelWithButtonLanguage.rightButton parentFlickable: fl checked: SettingsController.isStartMinimizedEnabled() @@ -199,7 +187,7 @@ PageType { descriptionText: LanguageModel.currentLanguageName rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: root.getNextComponentInFocusChain(labelWithButtonLanguage) + KeyNavigation.tab: labelWithButtonLogging.rightButton parentFlickable: fl clickedFunction: function() { @@ -207,7 +195,6 @@ PageType { } } - DividerType {} LabelWithButtonType { @@ -218,7 +205,7 @@ PageType { descriptionText: SettingsController.isLoggingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: root.getNextComponentInFocusChain(labelWithButtonLogging) + KeyNavigation.tab: labelWithButtonReset.rightButton parentFlickable: fl clickedFunction: function() { diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index 18035f65..218ffe28 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -101,9 +101,15 @@ PageType { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) } - Keys.onTabPressed: splitTunnelingButton2.visible ? - splitTunnelingButton2.forceActiveFocus() : - lastItemTabClicked() + Keys.onTabPressed: { + if (splitTunnelingButton2.visible) { + return splitTunnelingButton2.rightButton.forceActiveFocus() + } else if (killSwitchSwitcher.visible) { + return killSwitchSwitcher.forceActiveFocus() + } else { + lastItemTabClicked() + } + } } DividerType { @@ -124,7 +130,13 @@ PageType { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) } - Keys.onTabPressed: lastItemTabClicked() + Keys.onTabPressed: { + if (killSwitchSwitcher.visible) { + return killSwitchSwitcher.forceActiveFocus() + } else { + lastItemTabClicked() + } + } } DividerType { @@ -132,6 +144,7 @@ PageType { } SwitcherType { + id: killSwitchSwitcher visible: !GC.isMobile() Layout.fillWidth: true @@ -152,6 +165,8 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot change killSwitch settings during active connection")) } } + + Keys.onTabPressed: lastItemTabClicked() } DividerType { diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 024f3488..b0d5ce44 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -150,7 +150,7 @@ PageType { Rectangle { Layout.fillWidth: true - Layout.bottomMargin: 16 + Layout.bottomMargin: 48 implicitHeight: configContent.implicitHeight @@ -173,7 +173,15 @@ PageType { } } + Rectangle { + anchors.fill: columnContent + anchors.bottomMargin: -24 + color: "#0E0E11" + opacity: 0.8 + } + ColumnLayout { + id: columnContent anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right diff --git a/client/utilities.cpp b/client/utilities.cpp index 46df1dda..bc026330 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -76,7 +76,7 @@ QString Utils::usrExecutable(const QString &baseName) return ("/usr/bin/" + baseName); } -bool Utils::processIsRunning(const QString &fileName) +bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) { #ifdef Q_OS_WIN QProcess process; @@ -107,10 +107,14 @@ bool Utils::processIsRunning(const QString &fileName) #else QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); - process.start("pgrep", QStringList({ fileName })); + process.start("pgrep", QStringList({ fullFlag ? "-f" : "", fileName })); process.waitForFinished(); if (process.exitStatus() == QProcess::NormalExit) { - return (process.readAll().toUInt() > 0); + if (fullFlag) { + return (process.readLine().toUInt() > 0); + } else { + return (process.readAll().toUInt() > 0); + } } return false; #endif diff --git a/client/utilities.h b/client/utilities.h index 8a4cdc7a..3fd919f5 100644 --- a/client/utilities.h +++ b/client/utilities.h @@ -22,7 +22,7 @@ public: static bool createEmptyFile(const QString &path); static bool initializePath(const QString &path); - static bool processIsRunning(const QString &fileName); + static bool processIsRunning(const QString &fileName, const bool fullFlag = false); static void killProcessByName(const QString &name); static QString openVpnExecPath();