From 3e379c2d57d569b1fdb5f862c91cb2cc3572156a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Mon, 13 Jan 2025 11:51:09 +0100 Subject: [PATCH] Replace old waitForTunnelUp function After invoking VpnService.establish() we will get a tunnel file descriptor that corresponds to the interface that was created. However, this has no guarantee of the routing table beeing up to date, and we might thus send traffic outside the tunnel. Previously this was done through looking at the tunFd to see that traffic is sent to verify that the routing table has changed. If no traffic is seen some traffic is induced to a random IP address to ensure traffic can be seen. This new implementation is slower but won't risk sending UDP traffic to a random public address at the internet. --- .../lib/common/util/VpnServiceUtils.kt | 46 +++- .../mullvad/talpid/ConnectivityListener.kt | 33 ++- .../net/mullvad/talpid/TalpidVpnService.kt | 204 ++++++++++-------- .../mullvad/talpid/model/CreateTunResult.kt | 45 ++-- .../talpid/util/ConnectivityManagerUtil.kt | 111 ++++++---- mullvad-jni/src/classes.rs | 3 +- mullvad-jni/src/lib.rs | 3 +- mullvad-jni/src/talpid_vpn_service.rs | 181 ---------------- talpid-tunnel/src/tun_provider/android/mod.rs | 6 +- 9 files changed, 280 insertions(+), 352 deletions(-) delete mode 100644 mullvad-jni/src/talpid_vpn_service.rs diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt index 59833cb3961d..a3e98e88bb08 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt @@ -2,10 +2,14 @@ package net.mullvad.mullvadvpn.lib.common.util import android.content.Context import android.content.Intent +import android.net.VpnService import android.net.VpnService.prepare +import android.os.ParcelFileDescriptor import arrow.core.Either -import arrow.core.flatten +import arrow.core.flatMap import arrow.core.left +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull import arrow.core.right import co.touchlab.kermit.Logger import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.getInstalledPackagesList @@ -13,6 +17,8 @@ import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.mullvadvpn.lib.model.Prepared /** + * Safely prepare to establish a VPN connection. + * * Invoking VpnService.prepare() can result in 3 out comes: * 1. IllegalStateException - There is a legacy VPN profile marked as always on * 2. Intent @@ -34,7 +40,7 @@ fun Context.prepareVpnSafe(): Either = else -> throw it } } - .map { intent -> + .flatMap { intent -> if (intent == null) { Prepared.right() } else { @@ -46,7 +52,6 @@ fun Context.prepareVpnSafe(): Either = } } } - .flatten() fun Context.getAlwaysOnVpnAppName(): String? { return resolveAlwaysOnVpnPackageName() @@ -59,3 +64,38 @@ fun Context.getAlwaysOnVpnAppName(): String? { ?.loadLabel(packageManager) ?.toString() } + +/** + * Establish a VPN connection safely. + * + * This function wraps the [VpnService.Builder.establish] function and catches any exceptions that + * may be thrown and type them to a more specific error. + * + * @return [ParcelFileDescriptor] if successful, [EstablishError] otherwise + */ +fun VpnService.Builder.establishSafe(): Either = either { + val vpnInterfaceFd = + Either.catch { establish() } + .mapLeft { + when (it) { + is IllegalStateException -> EstablishError.ParameterNotApplied(it) + is IllegalArgumentException -> EstablishError.ParameterNotAccepted(it) + else -> EstablishError.UnknownError(it) + } + } + .bind() + + ensureNotNull(vpnInterfaceFd) { EstablishError.NullVpnInterface } + + vpnInterfaceFd +} + +sealed interface EstablishError { + data class ParameterNotApplied(val exception: IllegalStateException) : EstablishError + + data class ParameterNotAccepted(val exception: IllegalArgumentException) : EstablishError + + data object NullVpnInterface : EstablishError + + data class UnknownError(val error: Throwable) : EstablishError +} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt index 86b27e3ba83d..2ef560ed34db 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn import net.mullvad.talpid.util.NetworkEvent -import net.mullvad.talpid.util.defaultNetworkFlow +import net.mullvad.talpid.util.NetworkState +import net.mullvad.talpid.util.defaultNetworkStateFlow import net.mullvad.talpid.util.networkFlow class ConnectivityListener(val connectivityManager: ConnectivityManager) { @@ -27,14 +27,23 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) { val isConnected get() = _isConnected.value - private lateinit var _currentDnsServers: StateFlow> + private lateinit var _currentNetworkState: StateFlow + val currentNetworkState + get() = _currentNetworkState + // Used by JNI val currentDnsServers - get() = ArrayList(_currentDnsServers.value) + get() = + ArrayList( + _currentNetworkState.value?.linkProperties?.dnsServersWithoutFallback() + ?: emptyList() + ) fun register(scope: CoroutineScope) { - _currentDnsServers = - dnsServerChanges().stateIn(scope, SharingStarted.Eagerly, currentDnsServers()) + _currentNetworkState = + connectivityManager + .defaultNetworkStateFlow() + .stateIn(scope, SharingStarted.Eagerly, null) _isConnected = hasInternetCapability() @@ -42,18 +51,6 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) { .stateIn(scope, SharingStarted.Eagerly, false) } - private fun dnsServerChanges(): Flow> = - connectivityManager - .defaultNetworkFlow() - .filterIsInstance() - .onEach { Logger.d("Link properties changed") } - .map { it.linkProperties.dnsServersWithoutFallback() } - - private fun currentDnsServers(): List = - connectivityManager - .getLinkProperties(connectivityManager.activeNetwork) - ?.dnsServersWithoutFallback() ?: emptyList() - private fun LinkProperties.dnsServersWithoutFallback(): List = dnsServers.filter { it.hostAddress != TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt index 74d44005cd7c..2cf46b4d173e 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt @@ -1,18 +1,39 @@ package net.mullvad.talpid import android.net.ConnectivityManager +import android.net.LinkProperties import android.os.ParcelFileDescriptor import androidx.annotation.CallSuper import androidx.core.content.getSystemService import androidx.lifecycle.lifecycleScope +import arrow.core.Either +import arrow.core.flattenOrAccumulate +import arrow.core.merge +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull import co.touchlab.kermit.Logger import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress import kotlin.properties.Delegates.observable +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import net.mullvad.mullvadvpn.lib.common.util.establishSafe import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.talpid.model.CreateTunResult +import net.mullvad.talpid.model.CreateTunResult.EstablishError +import net.mullvad.talpid.model.CreateTunResult.InvalidDnsServers +import net.mullvad.talpid.model.CreateTunResult.NotPrepared +import net.mullvad.talpid.model.CreateTunResult.OtherAlwaysOnApp +import net.mullvad.talpid.model.CreateTunResult.OtherLegacyAlwaysOnVpn +import net.mullvad.talpid.model.CreateTunResult.RoutesTimedOutError +import net.mullvad.talpid.model.InetNetwork import net.mullvad.talpid.model.TunConfig import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported @@ -22,7 +43,7 @@ open class TalpidVpnService : LifecycleVpnService() { val oldTunFd = when (oldTunStatus) { is CreateTunResult.Success -> oldTunStatus.tunFd - is CreateTunResult.InvalidDnsServers -> oldTunStatus.tunFd + is InvalidDnsServers -> oldTunStatus.tunFd else -> null } @@ -43,26 +64,30 @@ open class TalpidVpnService : LifecycleVpnService() { connectivityListener.register(lifecycleScope) } - fun openTun(config: TunConfig): CreateTunResult { + // Used by JNI + fun openTun(config: TunConfig): CreateTunResult = synchronized(this) { val tunStatus = activeTunStatus if (config == currentTunConfig && tunStatus != null && tunStatus.isOpen) { - return tunStatus + tunStatus } else { - return openTunImpl(config) + openTunImpl(config) } } - } - fun openTunForced(config: TunConfig): CreateTunResult { - synchronized(this) { - return openTunImpl(config) - } - } + // Used by JNI + fun openTunForced(config: TunConfig): CreateTunResult = + synchronized(this) { openTunImpl(config) } + + // Used by JNI + fun closeTun(): Unit = synchronized(this) { activeTunStatus = null } + + // Used by JNI + fun bypass(socket: Int): Boolean = protect(socket) private fun openTunImpl(config: TunConfig): CreateTunResult { - val newTunStatus = createTun(config) + val newTunStatus = createTun(config).merge() currentTunConfig = config activeTunStatus = newTunStatus @@ -70,94 +95,100 @@ open class TalpidVpnService : LifecycleVpnService() { return newTunStatus } - fun closeTun() { - synchronized(this) { activeTunStatus = null } - } - - // DROID-1407 - // Function is to be cleaned up and lint suppression to be removed. - @Suppress("ReturnCount") - private fun createTun(config: TunConfig): CreateTunResult { - prepareVpnSafe() - .mapLeft { it.toCreateTunResult() } - .onLeft { - return it - } - - val invalidDnsServerAddresses = ArrayList() - - val builder = - Builder().apply { - for (address in config.addresses) { - addAddress(address, address.prefixLength()) - } - - for (dnsServer in config.dnsServers) { - try { - addDnsServer(dnsServer) - } catch (exception: IllegalArgumentException) { - invalidDnsServerAddresses.add(dnsServer) + private fun createTun( + config: TunConfig + ): Either = either { + prepareVpnSafe().mapLeft { it.toCreateTunError() }.bind() + + val builder = Builder() + builder.setMtu(config.mtu) + builder.setBlocking(false) + builder.setMeteredIfSupported(false) + + config.addresses.forEach { builder.addAddress(it, it.prefixLength()) } + config.routes.forEach { builder.addRoute(it.address, it.prefixLength.toInt()) } + config.excludedPackages.forEach { app -> builder.addDisallowedApplication(app) } + + // We don't care if this fails at this point, since we can still create a tunnel and + // then notify daemon to later enter blocked state. + val dnsConfigureResult = + config.dnsServers + .map { builder.addDnsServerE(it) } + .flattenOrAccumulate() + .onLeft { + // Avoid creating a tunnel with no DNS servers or if all DNS servers was + // invalid, since apps then may leak DNS requests. + // https://issuetracker.google.com/issues/337961996 + if (it.size == config.dnsServers.size) { + Logger.w( + "All DNS servers invalid or non set, using fallback DNS server to " + + "minimize leaks, dnsServers.isEmpty(): ${config.dnsServers.isEmpty()}" + ) + builder.addDnsServer(FALLBACK_DUMMY_DNS_SERVER) } } - - // Avoids creating a tunnel with no DNS servers or if all DNS servers was invalid, - // since apps then may leak DNS requests. - // https://issuetracker.google.com/issues/337961996 - if (invalidDnsServerAddresses.size == config.dnsServers.size) { - Logger.w( - "All DNS servers invalid or non set, using fallback DNS server to " + - "minimize leaks, dnsServers.isEmpty(): ${config.dnsServers.isEmpty()}" - ) - addDnsServer(FALLBACK_DUMMY_DNS_SERVER) - } - - for (route in config.routes) { - addRoute(route.address, route.prefixLength.toInt()) - } - - config.excludedPackages.forEach { app -> addDisallowedApplication(app) } - setMtu(config.mtu) - setBlocking(false) - setMeteredIfSupported(false) - } + .map { /* Ignore right */ } val vpnInterfaceFd = - try { - builder.establish() - } catch (e: IllegalStateException) { - Logger.e("Failed to establish, a parameter could not be applied", e) - return CreateTunResult.TunnelDeviceError - } catch (e: IllegalArgumentException) { - Logger.e("Failed to establish a parameter was not accepted", e) - return CreateTunResult.TunnelDeviceError - } + builder + .establishSafe() + .onLeft { Logger.w("Failed to establish tunnel $it") } + .mapLeft { EstablishError } + .bind() - if (vpnInterfaceFd == null) { - Logger.e("VpnInterface returned null") - return CreateTunResult.TunnelDeviceError - } + // Wait for android OS to respond back to us that the routes are setup so we don't + // send traffic before the routes are set up. Otherwise we might send traffic + // through the wrong interface + runBlocking { waitForRoutesWithTimeout(config) }.bind() val tunFd = vpnInterfaceFd.detachFd() - waitForTunnelUp(tunFd, config.routes.any { route -> route.isIpv6 }) + dnsConfigureResult.mapLeft { InvalidDnsServers(it, tunFd) }.bind() - if (invalidDnsServerAddresses.isNotEmpty()) { - return CreateTunResult.InvalidDnsServers(invalidDnsServerAddresses, tunFd) + CreateTunResult.Success(tunFd) + } + + private fun PrepareError.toCreateTunError() = + when (this) { + is PrepareError.OtherLegacyAlwaysOnVpn -> OtherLegacyAlwaysOnVpn + is PrepareError.NotPrepared -> NotPrepared + is PrepareError.OtherAlwaysOnApp -> OtherAlwaysOnApp(appName) } - return CreateTunResult.Success(tunFd) - } + private fun Builder.addDnsServerE(dnsServer: InetAddress) = + Either.catch { addDnsServer(dnsServer) } + .mapLeft { + when (it) { + is IllegalArgumentException -> dnsServer + else -> throw it + } + } + + private suspend fun waitForRoutesWithTimeout( + config: TunConfig, + timeout: Duration = ROUTES_SETUP_TIMEOUT, + ): Either = either { + // Wait for routes to match our expectations + val result = + withTimeoutOrNull(timeout = timeout) { + connectivityListener.currentNetworkState + .map { it?.linkProperties } + .distinctUntilChanged() + .first { linkProps -> linkProps?.containsAll(config.routes) == true } + } - fun bypass(socket: Int): Boolean { - return protect(socket) + ensureNotNull(result) { RoutesTimedOutError } } - private fun PrepareError.toCreateTunResult() = - when (this) { - is PrepareError.OtherLegacyAlwaysOnVpn -> CreateTunResult.OtherLegacyAlwaysOnVpn - is PrepareError.NotPrepared -> CreateTunResult.NotPrepared - is PrepareError.OtherAlwaysOnApp -> CreateTunResult.OtherAlwaysOnApp(appName) - } + private fun LinkProperties.containsAll(configRoutes: List): Boolean { + // Current routes on the link + val linkRoutes = + routes.map { + InetNetwork(it.destination.address, it.destination.prefixLength.toShort()) + } + + return linkRoutes.containsAll(configRoutes) + } private fun InetAddress.prefixLength(): Int = when (this) { @@ -166,10 +197,9 @@ open class TalpidVpnService : LifecycleVpnService() { else -> throw IllegalArgumentException("Invalid IP address (not IPv4 nor IPv6)") } - private external fun waitForTunnelUp(tunFd: Int, isIpv6Enabled: Boolean) - companion object { const val FALLBACK_DUMMY_DNS_SERVER = "192.0.2.1" + private val ROUTES_SETUP_TIMEOUT = 5000.milliseconds private const val IPV4_PREFIX_LENGTH = 32 private const val IPV6_PREFIX_LENGTH = 128 diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt index 3cd73685f715..af0e6ba688b4 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt @@ -1,29 +1,44 @@ package net.mullvad.talpid.model import java.net.InetAddress +import java.util.ArrayList +import net.mullvad.talpid.model.CreateTunResult.Error -sealed class CreateTunResult { - open val isOpen - get() = false +sealed interface CreateTunResult { + val isOpen: Boolean - class Success(val tunFd: Int) : CreateTunResult() { - override val isOpen - get() = true + data class Success(val tunFd: Int) : CreateTunResult { + override val isOpen = true } - class InvalidDnsServers(val addresses: ArrayList, val tunFd: Int) : - CreateTunResult() { - override val isOpen - get() = true + sealed interface Error : CreateTunResult + + // Prepare errors + data object OtherLegacyAlwaysOnVpn : Error { + override val isOpen: Boolean = false + } + + data class OtherAlwaysOnApp(val appName: String) : Error { + override val isOpen: Boolean = false + } + + data object NotPrepared : Error { + override val isOpen: Boolean = false } // Establish error - data object TunnelDeviceError : CreateTunResult() + data object EstablishError : Error { + override val isOpen: Boolean = false + } - // Prepare errors - data object OtherLegacyAlwaysOnVpn : CreateTunResult() + // Routes never got setup in time + data object RoutesTimedOutError : Error { + override val isOpen: Boolean = true + } - data class OtherAlwaysOnApp(val appName: String) : CreateTunResult() + data class InvalidDnsServers(val addresses: ArrayList, val tunFd: Int) : Error { + constructor(address: List, tunFd: Int) : this(ArrayList(address), tunFd) - data object NotPrepared : CreateTunResult() + override val isOpen = true + } } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/ConnectivityManagerUtil.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/ConnectivityManagerUtil.kt index daf155c6e8ef..37d982abb0e6 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/ConnectivityManagerUtil.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/ConnectivityManagerUtil.kt @@ -10,59 +10,76 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.scan + +fun ConnectivityManager.defaultNetworkFlow(): Flow = callbackFlow { + val callback = + object : NetworkCallback() { + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + super.onLinkPropertiesChanged(network, linkProperties) + trySendBlocking(NetworkEvent.LinkPropertiesChanged(network, linkProperties)) + } -fun ConnectivityManager.defaultNetworkFlow(): Flow = - callbackFlow { - val callback = - object : NetworkCallback() { - override fun onLinkPropertiesChanged( - network: Network, - linkProperties: LinkProperties, - ) { - super.onLinkPropertiesChanged(network, linkProperties) - trySendBlocking(NetworkEvent.LinkPropertiesChanged(network, linkProperties)) - } - - override fun onAvailable(network: Network) { - super.onAvailable(network) - trySendBlocking(NetworkEvent.Available(network)) - } + override fun onAvailable(network: Network) { + super.onAvailable(network) + trySendBlocking(NetworkEvent.Available(network)) + } - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities, - ) { - super.onCapabilitiesChanged(network, networkCapabilities) - trySendBlocking(NetworkEvent.CapabilitiesChanged(network, networkCapabilities)) - } + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities, + ) { + super.onCapabilitiesChanged(network, networkCapabilities) + trySendBlocking(NetworkEvent.CapabilitiesChanged(network, networkCapabilities)) + } - override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { - super.onBlockedStatusChanged(network, blocked) - trySendBlocking(NetworkEvent.BlockedStatusChanged(network, blocked)) - } + override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { + super.onBlockedStatusChanged(network, blocked) + trySendBlocking(NetworkEvent.BlockedStatusChanged(network, blocked)) + } - override fun onLosing(network: Network, maxMsToLive: Int) { - super.onLosing(network, maxMsToLive) - trySendBlocking(NetworkEvent.Losing(network, maxMsToLive)) - } + override fun onLosing(network: Network, maxMsToLive: Int) { + super.onLosing(network, maxMsToLive) + trySendBlocking(NetworkEvent.Losing(network, maxMsToLive)) + } - override fun onLost(network: Network) { - super.onLost(network) - trySendBlocking(NetworkEvent.Lost(network)) - } + override fun onLost(network: Network) { + super.onLost(network) + trySendBlocking(NetworkEvent.Lost(network)) + } - override fun onUnavailable() { - super.onUnavailable() - trySendBlocking(NetworkEvent.Unavailable) - } + override fun onUnavailable() { + super.onUnavailable() + trySendBlocking(NetworkEvent.Unavailable) } - registerDefaultNetworkCallback(callback) + } + registerDefaultNetworkCallback(callback) - awaitClose { unregisterNetworkCallback(callback) } - } + awaitClose { unregisterNetworkCallback(callback) } +} + +fun ConnectivityManager.defaultNetworkStateFlow(): Flow = + defaultNetworkFlow() + .scan( + null as NetworkState?, + { state, event -> + return@scan when (event) { + is NetworkEvent.Available -> NetworkState(network = event.network) + is NetworkEvent.BlockedStatusChanged -> + state?.copy(blockedStatus = event.blocked) + is NetworkEvent.CapabilitiesChanged -> + state?.copy(networkCapabilities = event.networkCapabilities) + is NetworkEvent.LinkPropertiesChanged -> + state?.copy(linkProperties = event.linkProperties) + is NetworkEvent.Losing -> state?.copy(maxMsToLive = event.maxMsToLive) + is NetworkEvent.Lost -> null + NetworkEvent.Unavailable -> null + } + }, + ) fun ConnectivityManager.networkFlow(networkRequest: NetworkRequest): Flow = - callbackFlow { + callbackFlow { val callback = object : NetworkCallback() { override fun onLinkPropertiesChanged( @@ -130,3 +147,11 @@ sealed interface NetworkEvent { data class Lost(val network: Network) : NetworkEvent } + +data class NetworkState( + val network: Network, + val linkProperties: LinkProperties? = null, + val networkCapabilities: NetworkCapabilities? = null, + val blockedStatus: Boolean = false, + val maxMsToLive: Int? = null, +) diff --git a/mullvad-jni/src/classes.rs b/mullvad-jni/src/classes.rs index 8312657efb1e..f2bdeb1b78a2 100644 --- a/mullvad-jni/src/classes.rs +++ b/mullvad-jni/src/classes.rs @@ -12,7 +12,8 @@ pub const CLASSES: &[&str] = &[ "net/mullvad/talpid/model/CreateTunResult$OtherLegacyAlwaysOnVpn", "net/mullvad/talpid/model/CreateTunResult$OtherAlwaysOnApp", "net/mullvad/talpid/model/CreateTunResult$NotPrepared", - "net/mullvad/talpid/model/CreateTunResult$TunnelDeviceError", + "net/mullvad/talpid/model/CreateTunResult$EstablishError", + "net/mullvad/talpid/model/CreateTunResult$RoutesTimedOutError", "net/mullvad/talpid/ConnectivityListener", "net/mullvad/talpid/TalpidVpnService", "net/mullvad/mullvadvpn/lib/endpoint/ApiEndpointOverride", diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index fd35396fd00f..852d52cb6ee2 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -3,7 +3,6 @@ mod api; mod classes; mod problem_report; -mod talpid_vpn_service; use jnix::{ jni::{ @@ -134,7 +133,7 @@ fn start( rpc_socket: PathBuf, files_dir: PathBuf, cache_dir: PathBuf, - api_endpoint: Option, + api_endpoint: Option, ) -> Result { start_logging(&files_dir).map_err(Error::InitializeLogging)?; version::log_version(); diff --git a/mullvad-jni/src/talpid_vpn_service.rs b/mullvad-jni/src/talpid_vpn_service.rs deleted file mode 100644 index ea6928538a86..000000000000 --- a/mullvad-jni/src/talpid_vpn_service.rs +++ /dev/null @@ -1,181 +0,0 @@ -use ipnetwork::IpNetwork; -use jnix::jni::{ - objects::JObject, - sys::{jboolean, jint, JNI_FALSE}, - JNIEnv, -}; -use nix::sys::{ - select::{pselect, FdSet}, - time::{TimeSpec, TimeValLike}, -}; -use rand::{thread_rng, Rng}; -use std::{ - io, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, - os::unix::io::RawFd, - time::{Duration, Instant}, -}; -use talpid_types::ErrorExt; - -#[derive(Debug, thiserror::Error)] -enum Error { - #[error("Failed to verify the tunnel device")] - VerifyTunDevice(#[from] SendRandomDataError), - - #[error("Failed to select() on tunnel device")] - Select(#[from] nix::Error), - - #[error("Timed out while waiting for tunnel device to receive data")] - TunnelDeviceTimeout, -} - -#[no_mangle] -#[allow(non_snake_case)] -pub extern "system" fn Java_net_mullvad_talpid_TalpidVpnService_waitForTunnelUp( - _: JNIEnv<'_>, - _this: JObject<'_>, - tunFd: jint, - isIpv6Enabled: jboolean, -) { - let tun_fd = tunFd as RawFd; - let is_ipv6_enabled = isIpv6Enabled != JNI_FALSE; - - if let Err(error) = wait_for_tunnel_up(tun_fd, is_ipv6_enabled) { - log::error!( - "{}", - error.display_chain_with_msg("Failed to wait for tunnel device to be usable") - ); - } -} - -fn wait_for_tunnel_up(tun_fd: RawFd, is_ipv6_enabled: bool) -> Result<(), Error> { - let mut fd_set = FdSet::new(); - fd_set.insert(tun_fd); - let timeout = TimeSpec::microseconds(300); - const TIMEOUT: Duration = Duration::from_secs(60); - let start = Instant::now(); - while start.elapsed() < TIMEOUT { - // if tunnel device is ready to be read from, traffic is being routed through it - if pselect(None, Some(&mut fd_set), None, None, Some(&timeout), None)? > 0 { - return Ok(()); - } - // have to add tun_fd back into the bitset - fd_set.insert(tun_fd); - try_sending_random_udp(is_ipv6_enabled)?; - } - - Err(Error::TunnelDeviceTimeout) -} - -#[derive(Debug, thiserror::Error)] -enum SendRandomDataError { - #[error("Failed to bind an UDP socket")] - BindUdpSocket(#[source] io::Error), - - #[error("Failed to send random data through UDP socket")] - SendToUdpSocket(#[source] io::Error), -} - -fn try_sending_random_udp(is_ipv6_enabled: bool) -> Result<(), SendRandomDataError> { - let mut tried_ipv6 = false; - const TIMEOUT: Duration = Duration::from_millis(300); - let start = Instant::now(); - - while start.elapsed() < TIMEOUT { - // TODO: if we are to allow LAN on Android by changing the routes that are stuffed in - // TunConfig, then this should be revisited to be fair between IPv4 and IPv6 - let should_generate_ipv4 = !is_ipv6_enabled || tried_ipv6 || thread_rng().gen(); - let (bound_addr, random_public_addr) = random_socket_addrs(should_generate_ipv4); - - tried_ipv6 |= random_public_addr.ip().is_ipv6(); - - let socket = UdpSocket::bind(bound_addr).map_err(SendRandomDataError::BindUdpSocket)?; - match socket.send_to(&random_data(), random_public_addr) { - Ok(_) => return Ok(()), - // Always retry on IPv6 errors - Err(_) if random_public_addr.ip().is_ipv6() => continue, - Err(_err) if matches!(_err.raw_os_error(), Some(22) | Some(101)) => { - // Error code 101 - specified network is unreachable - // Error code 22 - specified address is not usable - continue; - } - Err(err) => return Err(SendRandomDataError::SendToUdpSocket(err)), - } - } - Ok(()) -} - -fn random_data() -> Vec { - let mut buf = vec![0u8; thread_rng().gen_range(17..214)]; - thread_rng().fill(buf.as_mut_slice()); - buf -} - -/// Returns a random local and public destination socket address. -/// If `ipv4` is true, then IPv4 addresses will be returned. Otherwise, IPv6 addresses will be -/// returned. -fn random_socket_addrs(ipv4: bool) -> (SocketAddr, SocketAddr) { - loop { - let rand_port = thread_rng().gen(); - let (local_addr, rand_dest_addr) = if ipv4 { - let mut ipv4_bytes = [0u8; 4]; - thread_rng().fill(&mut ipv4_bytes); - ( - SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0), - SocketAddr::new(IpAddr::from(ipv4_bytes), rand_port), - ) - } else { - let mut ipv6_bytes = [0u8; 16]; - thread_rng().fill(&mut ipv6_bytes); - ( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0), - SocketAddr::new(IpAddr::from(ipv6_bytes), rand_port), - ) - }; - - // TODO: once https://github.com/rust-lang/rust/issues/27709 is resolved, please use - // `is_global()` to check if a new address should be attempted. - if !is_public_ip(rand_dest_addr.ip()) { - continue; - } - - return (local_addr, rand_dest_addr); - } -} - -fn is_public_ip(addr: IpAddr) -> bool { - match addr { - IpAddr::V4(ipv4) => { - // 0.x.x.x is not a publicly routable address - if ipv4.octets()[0] == 0u8 { - return false; - } - } - IpAddr::V6(ipv6) => { - if ipv6.segments()[0] == 0u16 { - return false; - } - } - } - // A non-exhaustive list of non-public subnets - let publicly_unroutable_subnets: Vec = vec![ - // IPv4 local networks - "10.0.0.0/8".parse().unwrap(), - "172.16.0.0/12".parse().unwrap(), - "192.168.0.0/16".parse().unwrap(), - // IPv4 non-forwardable network - "169.254.0.0/16".parse().unwrap(), - "192.0.0.0/8".parse().unwrap(), - // Documentation networks - "192.0.2.0/24".parse().unwrap(), - "198.51.100.0/24".parse().unwrap(), - "203.0.113.0/24".parse().unwrap(), - // IPv6 publicly unroutable networks - "fc00::/7".parse().unwrap(), - "fe80::/10".parse().unwrap(), - ]; - - !publicly_unroutable_subnets - .iter() - .any(|net| net.contains(addr)) -} diff --git a/talpid-tunnel/src/tun_provider/android/mod.rs b/talpid-tunnel/src/tun_provider/android/mod.rs index 3d356e50d328..4b416edbccd3 100644 --- a/talpid-tunnel/src/tun_provider/android/mod.rs +++ b/talpid-tunnel/src/tun_provider/android/mod.rs @@ -381,7 +381,8 @@ impl AsRawFd for VpnServiceTun { enum CreateTunResult { Success { tun_fd: i32 }, InvalidDnsServers { addresses: Vec }, - TunnelDeviceError, + EstablishError, + RoutesTimedOutError, OtherLegacyAlwaysOnVpn, OtherAlwaysOnApp { app_name: String }, NotPrepared, @@ -394,7 +395,8 @@ impl From for Result { CreateTunResult::InvalidDnsServers { addresses } => { Err(Error::InvalidDnsServers(addresses)) } - CreateTunResult::TunnelDeviceError => Err(Error::TunnelDeviceError), + CreateTunResult::RoutesTimedOutError => Err(Error::TunnelDeviceError), + CreateTunResult::EstablishError => Err(Error::TunnelDeviceError), CreateTunResult::OtherLegacyAlwaysOnVpn => Err(Error::OtherLegacyAlwaysOnVpn), CreateTunResult::OtherAlwaysOnApp { app_name } => { Err(Error::OtherAlwaysOnApp { app_name })