From 1576aed1ea4dbfe14802f1598b497e4413fabf1f Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 11 Dec 2023 15:16:50 +0300 Subject: [PATCH] Add network state listening and reconnection Vpn reconnects when the default network is changed --- client/android/AndroidManifest.xml | 2 + .../amnezia/vpn/protocol/openvpn/OpenVpn.kt | 26 ++- .../vpn/protocol/openvpn/OpenVpnClient.kt | 13 +- .../protocolApi/src/main/kotlin/Protocol.kt | 57 +++-- .../src/main/kotlin/ProtocolConfig.kt | 1 + .../src/main/kotlin/ProtocolState.kt | 1 + .../src/org/amnezia/vpn/AmneziaActivity.kt | 4 + .../src/org/amnezia/vpn/AmneziaVpnService.kt | 31 ++- .../android/src/org/amnezia/vpn/IpcMessage.kt | 1 + .../src/org/amnezia/vpn/NetworkState.kt | 195 ------------------ .../org/amnezia/vpn/qt/QtAndroidController.kt | 1 + .../utils/src/main/kotlin/net/NetworkState.kt | 104 ++++++++++ .../vpn/protocol/wireguard/Wireguard.kt | 4 + .../platforms/android/android_controller.cpp | 20 ++ client/platforms/android/android_controller.h | 3 + 15 files changed, 240 insertions(+), 223 deletions(-) delete mode 100644 client/android/src/org/amnezia/vpn/NetworkState.kt create mode 100644 client/android/utils/src/main/kotlin/net/NetworkState.kt diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 4a8f519b..19e31c3c 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -15,6 +15,8 @@ + + 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 d363fbeb..ba7df01a 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 @@ -67,7 +67,7 @@ open class OpenVpn : Protocol() { configBuilder = configBuilder, state = state, getLocalNetworks = { ipv6 -> getLocalNetworks(context, ipv6) }, - establish = makeEstablish(configBuilder, vpnBuilder), + establish = makeEstablish(vpnBuilder), protect = protect, onError = onError ) @@ -109,22 +109,28 @@ open class OpenVpn : Protocol() { openVpnClient = null } + override fun reconnectVpn(vpnBuilder: Builder) { + openVpnClient?.let { + it.establish = makeEstablish(vpnBuilder) + it.reconnect(0) + } + } + protected open fun parseConfig(config: JSONObject): ClientAPI_Config { val openVpnConfig = ClientAPI_Config() openVpnConfig.content = config.getJSONObject("openvpn_config_data").getString("config") return openVpnConfig } - private fun makeEstablish(configBuilder: OpenVpnConfig.Builder, vpnBuilder: Builder): () -> Int = - { - val openVpnConfig = configBuilder.build() - buildVpnInterface(openVpnConfig, vpnBuilder) + private fun makeEstablish(vpnBuilder: Builder): (OpenVpnConfig.Builder) -> Int = { configBuilder -> + val openVpnConfig = configBuilder.build() + buildVpnInterface(openVpnConfig, vpnBuilder) - vpnBuilder.establish().use { tunFd -> - if (tunFd == null) { - throw VpnStartException("Create VPN interface: permission not granted or revoked") - } - return@use tunFd.detachFd() + vpnBuilder.establish().use { tunFd -> + if (tunFd == null) { + throw VpnStartException("Create VPN interface: permission not granted or revoked") } + return@use tunFd.detachFd() } + } } diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt index c02a4360..c716a970 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt @@ -3,6 +3,7 @@ package org.amnezia.vpn.protocol.openvpn import android.net.ProxyInfo import android.os.Build import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate import net.openvpn.ovpn3.ClientAPI_Config import net.openvpn.ovpn3.ClientAPI_EvalConfig import net.openvpn.ovpn3.ClientAPI_Event @@ -14,6 +15,7 @@ import net.openvpn.ovpn3.ClientAPI_TransportStats import org.amnezia.vpn.protocol.ProtocolState import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED +import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.parseInetAddress @@ -25,7 +27,7 @@ class OpenVpnClient( private val configBuilder: OpenVpnConfig.Builder, private val state: MutableStateFlow, private val getLocalNetworks: (Boolean) -> List, - private val establish: () -> Int, + internal var establish: (OpenVpnConfig.Builder) -> Int, private val protect: (Int) -> Boolean, private val onError: (String) -> Unit ) : ClientAPI_OpenVPNClient() { @@ -51,6 +53,7 @@ class OpenVpnClient( // Should be called first. override fun tun_builder_new(): Boolean { Log.v(TAG, "tun_builder_new") + configBuilder.clearAddresses() return true } @@ -147,7 +150,7 @@ class OpenVpnClient( // Always called last after tun_builder session has been configured. override fun tun_builder_establish(): Int { Log.v(TAG, "tun_builder_establish") - return establish() + return establish(configBuilder) } // Callback to reroute default gateway to VPN interface. @@ -368,6 +371,12 @@ class OpenVpnClient( "COMPRESSION_ENABLED", "WARN" -> Log.w(TAG, "$name: $info") "CONNECTED" -> state.value = CONNECTED "DISCONNECTED" -> state.value = DISCONNECTED + "RECONNECTING" -> { + state.getAndUpdate { state -> + if (state == DISCONNECTED || state == CONNECTED) RECONNECTING + else state + } + } } if (event.error || event.fatal) { state.value = DISCONNECTED diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index 57bded46..ce3c13f5 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -41,6 +41,8 @@ abstract class Protocol { abstract fun stopVpn() + abstract fun reconnectVpn(vpnBuilder: Builder) + protected fun ProtocolConfig.Builder.configSplitTunnel(config: JSONObject) { val splitTunnelType = config.optInt("splitTunnelType") if (splitTunnelType == SPLIT_TUNNEL_DISABLE) return @@ -85,33 +87,62 @@ abstract class Protocol { protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) { vpnBuilder.setSession(VPN_SESSION_NAME) - for (addr in config.addresses) vpnBuilder.addAddress(addr) - - for (addr in config.dnsServers) vpnBuilder.addDnsServer(addr) - // fix for Samsung android ignoring DNS servers outside the VPN route range - if (Build.BRAND == "samsung") { - for (addr in config.dnsServers) vpnBuilder.addRoute(InetNetwork(addr)) + for (addr in config.addresses) { + Log.d(TAG, "addAddress: $addr") + vpnBuilder.addAddress(addr) } - config.searchDomain?.let { vpnBuilder.addSearchDomain(it) } + for (addr in config.dnsServers) { + Log.d(TAG, "addDnsServer: $addr") + vpnBuilder.addDnsServer(addr) + } + // fix for Samsung android ignoring DNS servers outside the VPN route range + if (Build.BRAND == "samsung") { + for (addr in config.dnsServers) { + Log.d(TAG, "addRoute: $addr") + vpnBuilder.addRoute(InetNetwork(addr)) + } + } - for (addr in config.routes) vpnBuilder.addRoute(addr) + config.searchDomain?.let { + Log.d(TAG, "addSearchDomain: $it") + vpnBuilder.addSearchDomain(it) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - for (addr in config.excludedRoutes) vpnBuilder.excludeRoute(addr) + for (addr in config.routes) { + Log.d(TAG, "addRoute: $addr") + vpnBuilder.addRoute(addr) + } - for (app in config.excludedApplications) vpnBuilder.addDisallowedApplication(app) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + for (addr in config.excludedRoutes) { + Log.d(TAG, "excludeRoute: $addr") + vpnBuilder.excludeRoute(addr) + } + } + for (app in config.excludedApplications) { + Log.d(TAG, "addDisallowedApplication: $app") + vpnBuilder.addDisallowedApplication(app) + } + + Log.d(TAG, "setMtu: ${config.mtu}") vpnBuilder.setMtu(config.mtu) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - config.httpProxy?.let { vpnBuilder.setHttpProxy(it) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + config.httpProxy?.let { + Log.d(TAG, "setHttpProxy: $it") + vpnBuilder.setHttpProxy(it) + } + } if (config.allowAllAF) { + Log.d(TAG, "allowFamily") vpnBuilder.allowFamily(OsConstants.AF_INET) vpnBuilder.allowFamily(OsConstants.AF_INET6) } + Log.d(TAG, "setBlocking: ${config.blockingMode}") vpnBuilder.setBlocking(config.blockingMode) vpnBuilder.setUnderlyingNetworks(null) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt index df74206a..7050e79f 100644 --- a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt +++ b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt @@ -56,6 +56,7 @@ open class ProtocolConfig protected constructor( fun addAddress(addr: InetNetwork) = apply { this.addresses += addr } fun addAddresses(addresses: List) = apply { this.addresses += addresses } + fun clearAddresses() = apply { this.addresses.clear() } fun addDnsServer(dnsServer: InetAddress) = apply { this.dnsServers += dnsServer } fun addDnsServers(dnsServers: List) = apply { this.dnsServers += dnsServers } diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolState.kt b/client/android/protocolApi/src/main/kotlin/ProtocolState.kt index 977ef284..080690fa 100644 --- a/client/android/protocolApi/src/main/kotlin/ProtocolState.kt +++ b/client/android/protocolApi/src/main/kotlin/ProtocolState.kt @@ -6,5 +6,6 @@ enum class ProtocolState { CONNECTING, DISCONNECTED, DISCONNECTING, + RECONNECTING, UNKNOWN } diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index df70e102..cc2cee25 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -62,6 +62,10 @@ class AmneziaActivity : QtActivity() { QtAndroidController.onVpnDisconnected() } + ServiceEvent.RECONNECTING -> { + QtAndroidController.onVpnReconnecting() + } + ServiceEvent.STATUS -> { if (isWaitingStatus) { isWaitingStatus = false diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 1682dbce..094874c7 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -37,6 +37,7 @@ 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 import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.Status @@ -49,6 +50,7 @@ import org.amnezia.vpn.protocol.putStatistics import org.amnezia.vpn.protocol.putStatus import org.amnezia.vpn.protocol.wireguard.Wireguard import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.net.NetworkState import org.json.JSONException import org.json.JSONObject @@ -85,6 +87,7 @@ class AmneziaVpnService : VpnService() { private var disconnectionJob: Job? = null private var statisticsSendingJob: Job? = null private lateinit var clientMessenger: IpcMessenger + private lateinit var networkState: NetworkState private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> protocolState.value = DISCONNECTED @@ -181,6 +184,7 @@ class AmneziaVpnService : VpnService() { connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler) clientMessenger = IpcMessenger(messengerName = "Client") launchProtocolStateHandler() + networkState = NetworkState(this, ::reconnect) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -206,7 +210,7 @@ class AmneziaVpnService : VpnService() { override fun onBind(intent: Intent?): IBinder? { Log.d(TAG, "onBind by $intent") - if (intent?.action == "android.net.VpnService") return super.onBind(intent) + if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent) isServiceBound = true if (isConnected) launchSendingStatistics() return vpnServiceMessenger.binder @@ -214,7 +218,7 @@ class AmneziaVpnService : VpnService() { override fun onUnbind(intent: Intent?): Boolean { Log.d(TAG, "onUnbind by $intent") - if (intent?.action != "android.net.VpnService") { + if (intent?.action != SERVICE_INTERFACE) { isServiceBound = false stopSendingStatistics() clientMessenger.reset() @@ -225,7 +229,7 @@ class AmneziaVpnService : VpnService() { override fun onRebind(intent: Intent?) { Log.d(TAG, "onRebind by $intent") - if (intent?.action != "android.net.VpnService") { + if (intent?.action != SERVICE_INTERFACE) { isServiceBound = true if (isConnected) launchSendingStatistics() } @@ -272,16 +276,24 @@ class AmneziaVpnService : VpnService() { when (protocolState) { CONNECTED -> { clientMessenger.send(ServiceEvent.CONNECTED) + networkState.bindNetworkListener() if (isServiceBound) launchSendingStatistics() } DISCONNECTED -> { clientMessenger.send(ServiceEvent.DISCONNECTED) + networkState.unbindNetworkListener() stopSendingStatistics() if (!isServiceBound) stopService() } DISCONNECTING -> { + networkState.unbindNetworkListener() + stopSendingStatistics() + } + + RECONNECTING -> { + clientMessenger.send(ServiceEvent.RECONNECTING) stopSendingStatistics() } @@ -367,6 +379,19 @@ class AmneziaVpnService : VpnService() { } } + @MainThread + private fun reconnect() { + if (!isConnected) return + + Log.v(TAG, "Reconnect VPN") + + protocolState.value = RECONNECTING + + connectionJob = connectionScope.launch { + protocol?.reconnectVpn(Builder()) + } + } + @MainThread private fun getProtocol(protocolName: String): Protocol = protocolCache[protocolName] diff --git a/client/android/src/org/amnezia/vpn/IpcMessage.kt b/client/android/src/org/amnezia/vpn/IpcMessage.kt index c0183b45..c9d2bd3f 100644 --- a/client/android/src/org/amnezia/vpn/IpcMessage.kt +++ b/client/android/src/org/amnezia/vpn/IpcMessage.kt @@ -22,6 +22,7 @@ sealed interface IpcMessage { enum class ServiceEvent : IpcMessage { CONNECTED, DISCONNECTED, + RECONNECTING, STATUS, STATISTICS_UPDATE, ERROR diff --git a/client/android/src/org/amnezia/vpn/NetworkState.kt b/client/android/src/org/amnezia/vpn/NetworkState.kt deleted file mode 100644 index 7b896bc3..00000000 --- a/client/android/src/org/amnezia/vpn/NetworkState.kt +++ /dev/null @@ -1,195 +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.content.Context -import android.content.Intent -import android.os.* -import android.net.* -import android.system.ErrnoException -import android.net.NetworkCapabilities -import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL -import android.net.NetworkCapabilities.NET_CAPABILITY_DUN -import android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND -import android.net.NetworkCapabilities.NET_CAPABILITY_FOTA -import android.net.NetworkCapabilities.NET_CAPABILITY_IA -import android.net.NetworkCapabilities.NET_CAPABILITY_IMS -import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET -import android.net.NetworkCapabilities.NET_CAPABILITY_MCX -import android.net.NetworkCapabilities.NET_CAPABILITY_MMS -import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED -import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED -import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING -import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED -import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN -import android.net.NetworkCapabilities.NET_CAPABILITY_SUPL -import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED -import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED -import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED -import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P -import android.net.NetworkCapabilities.NET_CAPABILITY_XCAP -import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH -import android.net.NetworkCapabilities.TRANSPORT_CELLULAR -import android.net.NetworkCapabilities.TRANSPORT_ETHERNET -import android.net.NetworkCapabilities.TRANSPORT_LOWPAN -import android.net.NetworkCapabilities.TRANSPORT_USB -import android.net.NetworkCapabilities.TRANSPORT_VPN -import android.net.NetworkCapabilities.TRANSPORT_WIFI -import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE -import java.io.Closeable -import java.util.EnumSet -import java.io.File -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import java.io.FileDescriptor -import java.io.IOException -import java.lang.Exception -import org.amnezia.vpn.util.Log - -class NetworkState(var service: AmneziaVpnService) { - private var mService: AmneziaVpnService = service - var mCurrentContext: Context = service - private val tag = "NetworkState" - private var active = false - private var listeningForDefaultNetwork = false - private var metered = false - - - enum class Transport(val systemConstant: Int) { - BLUETOOTH(TRANSPORT_BLUETOOTH), - CELLULAR(TRANSPORT_CELLULAR), - ETHERNET(TRANSPORT_ETHERNET), - VPN(TRANSPORT_VPN), - WIFI(TRANSPORT_WIFI), - WIFI_AWARE(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) TRANSPORT_WIFI_AWARE else UNSUPPORTED_TRANSPORT), - LOWPAN(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) TRANSPORT_LOWPAN else UNSUPPORTED_TRANSPORT), - USB(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) TRANSPORT_USB else UNSUPPORTED_TRANSPORT) - } - - companion object { - - private const val UNSUPPORTED_TRANSPORT: Int = -1 // The TRANSPORT_* constants are non-negative. - private const val NOT_VPN = "NOT_VPN" - - private val defaultNetworkRequest = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) - .build() - - } - - private data class NetworkTransports( - val network: Network, - val transports: Set - ) - - private fun getTransports(networkCapabilities: NetworkCapabilities): EnumSet = - Transport.values().mapNotNullTo(EnumSet.noneOf(Transport::class.java)) { - if (networkCapabilities.hasTransport(it.systemConstant)) it else null - } - - private var defaultNetworkCapabilities: Map = LinkedHashMap() - private var defaultNetwork: NetworkTransports? = null - val defaultNetworkTransports: Set - get() = defaultNetwork?.transports ?: emptySet() - - private val capabilitiesConstantMap = mutableMapOf( - "MMS" to NET_CAPABILITY_MMS, - "SUPL" to NET_CAPABILITY_SUPL, - "DUN" to NET_CAPABILITY_DUN, - "FOTA" to NET_CAPABILITY_FOTA, - "IMS" to NET_CAPABILITY_IMS, - "WIFI_P2P" to NET_CAPABILITY_WIFI_P2P, - "IA" to NET_CAPABILITY_IA, - "XCAP" to NET_CAPABILITY_XCAP, - "NOT_METERED" to NET_CAPABILITY_NOT_METERED, - "INTERNET" to NET_CAPABILITY_INTERNET, - NOT_VPN to NET_CAPABILITY_NOT_VPN, - "TRUSTED" to NET_CAPABILITY_TRUSTED, - "TEMP NOT METERED" to NET_CAPABILITY_TEMPORARILY_NOT_METERED, - "NOT SUSPENDED" to NET_CAPABILITY_MCX, - ).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - put("VALIDATED", NET_CAPABILITY_VALIDATED) - put("CAPTIVE PORTAL", NET_CAPABILITY_CAPTIVE_PORTAL) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - put("NOT ROAMING", NET_CAPABILITY_NOT_ROAMING) - put("TRUSTED", NET_CAPABILITY_FOREGROUND) - put("NOT CONGESTED", NET_CAPABILITY_NOT_CONGESTED) - put("NOT SUSPENDED", NET_CAPABILITY_NOT_SUSPENDED) - } - } as Map - - - - private val connectivity by lazy { mCurrentContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } - - private var mLastNetworkCapabilities: String? = null - - private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - super.onAvailable(network) - - - Log.i(tag, "onAvailable $network") - } - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - val newCapabilities = capabilitiesConstantMap.mapValues { - networkCapabilities.hasCapability(it.value) - } - val newTransports = getTransports(networkCapabilities) - val capabilitiesChanged = defaultNetworkCapabilities != newCapabilities - if (defaultNetwork?.network != network || - defaultNetwork?.transports != newTransports || - capabilitiesChanged - ) { - Log.i( - tag, - "default network: $network; transports: ${newTransports.joinToString(", ")}; " + - "capabilities: $newCapabilities" - ) - defaultNetwork = NetworkTransports(network, newTransports) - } - if (capabilitiesChanged) { - // mService.networkChange() - - Log.i(tag, "onCapabilitiesChanged capabilitiesChanged $network $networkCapabilities") - defaultNetworkCapabilities = newCapabilities - } - super.onCapabilitiesChanged(network, networkCapabilities) - } - - override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { - super.onBlockedStatusChanged(network, blocked) - Log.i(tag, "onBlockedStatusChanged $network $blocked") - } - - - override fun onLost(network: Network) { - super.onLost(network) - Log.i(tag, "onLost") - } - } - - fun bindNetworkListener() { - if (Build.VERSION.SDK_INT >= 28) { - // we want REQUEST here instead of LISTEN - connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) - listeningForDefaultNetwork = true - } - } - - fun unBindNetworkListener() { - if (Build.VERSION.SDK_INT >= 28) { - connectivity.unregisterNetworkCallback(defaultNetworkCallback) - listeningForDefaultNetwork = false - } - } - - - - -} diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index 72f2c83c..30102a4a 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -12,6 +12,7 @@ object QtAndroidController { external fun onVpnPermissionRejected() external fun onVpnConnected() external fun onVpnDisconnected() + external fun onVpnReconnecting() external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) external fun onConfigImported() diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt new file mode 100644 index 00000000..42d7baee --- /dev/null +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -0,0 +1,104 @@ +package org.amnezia.vpn.util.net + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkRequest +import android.os.Build +import android.os.Handler +import kotlin.LazyThreadSafetyMode.NONE +import org.amnezia.vpn.util.Log + +private const val TAG = "NetworkState" + +class NetworkState( + private val context: Context, + private val onNetworkChange: () -> Unit +) { + private var currentNetwork: Network? = null + private var validated: Boolean = false + private var isListenerBound = false + + private val handler: Handler by lazy(NONE) { + Handler(context.mainLooper) + } + + private val connectivityManager: ConnectivityManager by lazy(NONE) { + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + + private val networkRequest: NetworkRequest by lazy(NONE) { + NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + } + + private val networkCallback: NetworkCallback by lazy(NONE) { + object : NetworkCallback() { + override fun onAvailable(network: Network) { + Log.d(TAG, "onAvailable: $network") + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + checkNetworkState(network, networkCapabilities) + } else { + handler.post { + checkNetworkState(network, networkCapabilities) + } + } + } + + private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) { + if (currentNetwork == null) { + currentNetwork = network + validated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) + } else { + if (currentNetwork != network) { + currentNetwork = network + validated = false + } + if (!validated) { + validated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) + if (validated) onNetworkChange() + } + } + } + + override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { + Log.d(TAG, "onBlockedStatusChanged: $network, $blocked") + } + + override fun onLost(network: Network) { + Log.d(TAG, "onLost: $network") + } + } + } + + fun bindNetworkListener() { + if (isListenerBound) return + Log.v(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) + } else { + connectivityManager.requestNetwork(networkRequest, networkCallback) + } + isListenerBound = true + } + + fun unbindNetworkListener() { + if (!isListenerBound) return + Log.v(TAG, "Unbind network listener") + connectivityManager.unregisterNetworkCallback(networkCallback) + isListenerBound = false + currentNetwork = null + validated = false + } +} diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 227de63e..5bf84139 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -173,4 +173,8 @@ open class Wireguard : Protocol() { GoBackend.wgTurnOff(handleToClose) state.value = DISCONNECTED } + + override fun reconnectVpn(vpnBuilder: Builder) { + state.value = CONNECTED + } } diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index fbcc3f01..384ef131 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -67,6 +67,14 @@ AndroidController::AndroidController() : QObject() }, Qt::QueuedConnection); + connect( + this, &AndroidController::vpnReconnecting, this, + [this]() { + qDebug() << "Android event: VPN reconnecting"; + emit connectionStateChanged(Vpn::ConnectionState::Reconnecting); + }, + Qt::QueuedConnection); + connect( this, &AndroidController::configImported, this, []() { @@ -101,6 +109,7 @@ bool AndroidController::initialize() {"onVpnPermissionRejected", "()V", reinterpret_cast(onVpnPermissionRejected)}, {"onVpnConnected", "()V", reinterpret_cast(onVpnConnected)}, {"onVpnDisconnected", "()V", reinterpret_cast(onVpnDisconnected)}, + {"onVpnReconnecting", "()V", reinterpret_cast(onVpnReconnecting)}, {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, {"onConfigImported", "()V", reinterpret_cast(onConfigImported)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)} @@ -187,6 +196,7 @@ Vpn::ConnectionState AndroidController::convertState(AndroidController::Connecti case AndroidController::ConnectionState::CONNECTING: return Vpn::ConnectionState::Connecting; case AndroidController::ConnectionState::DISCONNECTED: return Vpn::ConnectionState::Disconnected; case AndroidController::ConnectionState::DISCONNECTING: return Vpn::ConnectionState::Disconnecting; + case AndroidController::ConnectionState::RECONNECTING: return Vpn::ConnectionState::Reconnecting; case AndroidController::ConnectionState::UNKNOWN: return Vpn::ConnectionState::Unknown; } } @@ -199,6 +209,7 @@ QString AndroidController::textConnectionState(AndroidController::ConnectionStat case AndroidController::ConnectionState::CONNECTING: return "CONNECTING"; case AndroidController::ConnectionState::DISCONNECTED: return "DISCONNECTED"; case AndroidController::ConnectionState::DISCONNECTING: return "DISCONNECTING"; + case AndroidController::ConnectionState::RECONNECTING: return "RECONNECTING"; case AndroidController::ConnectionState::UNKNOWN: return "UNKNOWN"; } } @@ -260,6 +271,15 @@ void AndroidController::onVpnDisconnected(JNIEnv *env, jobject thiz) emit AndroidController::instance()->vpnDisconnected(); } +// static +void AndroidController::onVpnReconnecting(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->vpnReconnecting(); +} + // static void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes) { diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index a4bfc890..d902398d 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -23,6 +23,7 @@ public: CONNECTING, DISCONNECTED, DISCONNECTING, + RECONNECTING, UNKNOWN }; @@ -40,6 +41,7 @@ signals: void vpnPermissionRejected(); void vpnConnected(); void vpnDisconnected(); + void vpnReconnecting(); void statisticsUpdated(quint64 rxBytes, quint64 txBytes); void configImported(); void importConfigFromOutside(QString &data); @@ -60,6 +62,7 @@ private: static void onVpnPermissionRejected(JNIEnv *env, jobject thiz); static void onVpnConnected(JNIEnv *env, jobject thiz); static void onVpnDisconnected(JNIEnv *env, jobject thiz); + static void onVpnReconnecting(JNIEnv *env, jobject thiz); static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onConfigImported(JNIEnv *env, jobject thiz); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);