Skip to content

Commit

Permalink
Replace old waitForTunnelUp function
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Rawa committed Jan 15, 2025
1 parent 7fdaade commit 3e379c2
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 352 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ 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
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
Expand All @@ -34,7 +40,7 @@ fun Context.prepareVpnSafe(): Either<PrepareError, Prepared> =
else -> throw it
}
}
.map { intent ->
.flatMap { intent ->
if (intent == null) {
Prepared.right()
} else {
Expand All @@ -46,7 +52,6 @@ fun Context.prepareVpnSafe(): Either<PrepareError, Prepared> =
}
}
}
.flatten()

fun Context.getAlwaysOnVpnAppName(): String? {
return resolveAlwaysOnVpnPackageName()
Expand All @@ -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<EstablishError, ParcelFileDescriptor> = 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -27,33 +27,30 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) {
val isConnected
get() = _isConnected.value

private lateinit var _currentDnsServers: StateFlow<List<InetAddress>>
private lateinit var _currentNetworkState: StateFlow<NetworkState?>
val currentNetworkState
get() = _currentNetworkState

// Used by JNI
val currentDnsServers
get() = ArrayList(_currentDnsServers.value)
get() =
ArrayList(
_currentNetworkState.value?.linkProperties?.dnsServersWithoutFallback()
?: emptyList<InetAddress>()
)

fun register(scope: CoroutineScope) {
_currentDnsServers =
dnsServerChanges().stateIn(scope, SharingStarted.Eagerly, currentDnsServers())
_currentNetworkState =
connectivityManager
.defaultNetworkStateFlow()
.stateIn(scope, SharingStarted.Eagerly, null)

_isConnected =
hasInternetCapability()
.onEach { notifyConnectivityChange(it) }
.stateIn(scope, SharingStarted.Eagerly, false)
}

private fun dnsServerChanges(): Flow<List<InetAddress>> =
connectivityManager
.defaultNetworkFlow()
.filterIsInstance<NetworkEvent.LinkPropertiesChanged>()
.onEach { Logger.d("Link properties changed") }
.map { it.linkProperties.dnsServersWithoutFallback() }

private fun currentDnsServers(): List<InetAddress> =
connectivityManager
.getLinkProperties(connectivityManager.activeNetwork)
?.dnsServersWithoutFallback() ?: emptyList()

private fun LinkProperties.dnsServersWithoutFallback(): List<InetAddress> =
dnsServers.filter { it.hostAddress != TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER }

Expand Down
Loading

0 comments on commit 3e379c2

Please sign in to comment.