diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml
index 4a8f519ba..19e31c3c1 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 d363fbebf..ba7df01a2 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 c02a4360a..c716a9708 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 57bded467..ce3c13f55 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 df74206a4..7050e79f6 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 977ef284d..080690fa3 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 df70e1025..cc2cee256 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 1682dbcef..094874c7c 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 c0183b45f..c9d2bd3f9 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 7b896bc31..000000000
--- 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 72f2c83c1..30102a4a3 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 000000000..42d7baee9
--- /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 227de63e1..5bf841395 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 fbcc3f012..384ef1311 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 a4bfc8901..d902398df 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);