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 13, 2025
1 parent a861ac7 commit 283e460
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 287 deletions.
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,27 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) {
val isConnected
get() = _isConnected.value

private lateinit var _currentDnsServers: StateFlow<List<InetAddress>>
lateinit var _currentNetworkState: StateFlow<NetworkState?>
// 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
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
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.merge
import arrow.core.raise.either
import arrow.core.raise.ensure
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.ExperimentalCoroutinesApi
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.prepareVpnSafe
import net.mullvad.mullvadvpn.lib.model.PrepareError
import net.mullvad.talpid.model.CreateTunResult
import net.mullvad.talpid.model.InetNetwork
import net.mullvad.talpid.model.TunConfig
import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported

Expand Down Expand Up @@ -74,90 +89,140 @@ open class TalpidVpnService : LifecycleVpnService() {
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<InetAddress>()
private fun createTun(config: TunConfig): CreateTunResult =
either<CreateTunResult.Error, CreateTunResult.Success> {
Logger.d("Creating tunnel with config: $config")
prepareVpnSafe().mapLeft { it.toCreateTunError() }.bind()

val invalidDnsServerAddresses = ArrayList<InetAddress>()

val builder =
Builder().apply {
for (address in config.addresses) {
addAddress(address, address.prefixLength())
}

for (dnsServer in config.dnsServers) {
try {
addDnsServer(dnsServer)
} catch (_: IllegalArgumentException) {
invalidDnsServerAddresses.add(dnsServer)
}
}

// 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)
}

val builder =
Builder().apply {
for (address in config.addresses) {
addAddress(address, address.prefixLength())
}
val vpnInterfaceFd = builder.establishE().bind()

for (dnsServer in config.dnsServers) {
try {
addDnsServer(dnsServer)
} catch (exception: IllegalArgumentException) {
invalidDnsServerAddresses.add(dnsServer)
}
}
// 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
waitForRoutesWithTimeout(config).bind()

// 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)
}
val tunFd = vpnInterfaceFd.detachFd()

for (route in config.routes) {
addRoute(route.address, route.prefixLength.toInt())
ensure(invalidDnsServerAddresses.isEmpty()) {
CreateTunResult.InvalidDnsServers(invalidDnsServerAddresses, tunFd)
}

config.excludedPackages.forEach { app -> addDisallowedApplication(app) }
setMtu(config.mtu)
setBlocking(false)
setMeteredIfSupported(false)
CreateTunResult.Success(tunFd)
}
.merge()
.also { Logger.d("TunnelResult: $it") }

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
}
fun bypass(socket: Int): Boolean {
return protect(socket)
}

if (vpnInterfaceFd == null) {
Logger.e("VpnInterface returned null")
return CreateTunResult.TunnelDeviceError
private fun PrepareError.toCreateTunError() =
when (this) {
is PrepareError.OtherLegacyAlwaysOnVpn -> CreateTunResult.OtherLegacyAlwaysOnVpn
is PrepareError.NotPrepared -> CreateTunResult.NotPrepared
is PrepareError.OtherAlwaysOnApp -> CreateTunResult.OtherAlwaysOnApp(appName)
}

val tunFd = vpnInterfaceFd.detachFd()
private fun Builder.establishE(): Either<CreateTunResult.EstablishError, ParcelFileDescriptor> =
either {
val vpnInterfaceFd =
Either.catch { establish() }
.mapLeft { error ->
when (error) {
is IllegalStateException -> {
Logger.e(
"Failed to establish, a parameter could not be applied",
error,
)
CreateTunResult.EstablishError
}
is IllegalArgumentException -> {
Logger.e("Failed to establish a parameter was not accepted", error)
CreateTunResult.EstablishError
}
else -> throw error
}
}
.bind()

waitForTunnelUp(tunFd, config.routes.any { route -> route.isIpv6 })
ensureNotNull(vpnInterfaceFd) {
Logger.e("VpnInterface returned null")
CreateTunResult.EstablishError
}

if (invalidDnsServerAddresses.isNotEmpty()) {
return CreateTunResult.InvalidDnsServers(invalidDnsServerAddresses, tunFd)
vpnInterfaceFd
}

return CreateTunResult.Success(tunFd)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun waitForRoutesWithTimeout(
config: TunConfig,
timeout: Duration = ROUTES_SETUP_TIMEOUT,
): Either<CreateTunResult.RoutesTimedOutError, Unit> = either {
// Wait for routes to match our expectations
val result = runBlocking {
withTimeoutOrNull(timeout = timeout) {
connectivityListener._currentNetworkState
.map { it?.linkProperties }
.distinctUntilChanged()
.first { it?.matches(config) == true }
}
}

fun bypass(socket: Int): Boolean {
return protect(socket)
ensureNotNull(result) { CreateTunResult.RoutesTimedOutError }
}

private fun PrepareError.toCreateTunResult() =
when (this) {
is PrepareError.OtherLegacyAlwaysOnVpn -> CreateTunResult.OtherLegacyAlwaysOnVpn
is PrepareError.NotPrepared -> CreateTunResult.NotPrepared
is PrepareError.OtherAlwaysOnApp -> CreateTunResult.OtherAlwaysOnApp(appName)
}
// return true if LinkProperties matches the TunConfig
private fun LinkProperties.matches(tunConfig: TunConfig): Boolean {
// Compare routes:

val linkRoutes =
routes
.map { InetNetwork(it.destination.address, it.destination.prefixLength.toShort()) }
.toSet()

val missingRoutes = tunConfig.routes - linkRoutes
// If there are no missing routes we consider it ready for use.
return missingRoutes.isEmpty()
}

private fun InetAddress.prefixLength(): Int =
when (this) {
Expand All @@ -166,10 +231,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 = 3000.milliseconds

private const val IPV4_PREFIX_LENGTH = 32
private const val IPV6_PREFIX_LENGTH = 128
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,39 @@ package net.mullvad.talpid.model

import java.net.InetAddress

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<InetAddress>, val tunFd: Int) :
CreateTunResult() {
override val isOpen
get() = true
sealed interface Error : CreateTunResult

// Prepare errors
data object OtherLegacyAlwaysOnVpn : Error {
override val isOpen: Boolean = false
}

// Establish error
data object TunnelDeviceError : CreateTunResult()
data class OtherAlwaysOnApp(val appName: String) : Error {
override val isOpen: Boolean = false
}

// Prepare errors
data object OtherLegacyAlwaysOnVpn : CreateTunResult()
data object NotPrepared : Error {
override val isOpen: Boolean = false
}

data class OtherAlwaysOnApp(val appName: String) : CreateTunResult()
// Establish error
data object EstablishError : Error {
override val isOpen: Boolean = false
}

data object NotPrepared : CreateTunResult()
// Routes never got setup in time
data object RoutesTimedOutError : Error {
override val isOpen: Boolean = true
}

data class InvalidDnsServers(val addresses: ArrayList<InetAddress>, val tunFd: Int) : Error {
override val isOpen = true
}
}
Loading

0 comments on commit 283e460

Please sign in to comment.