summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-01-13 11:51:09 +0100
committerDavid Göransson <david.goransson@mullvad.net>2025-02-06 11:02:59 +0100
commit341c10ba38752bc36151b8998064e706f70d9ea6 (patch)
treeafb60c53e267eda0b033f346b64afd9035d7495a
parent612aad8d8d2ae779a4e5e01e85b2848b4fc7de3c (diff)
downloadmullvadvpn-341c10ba38752bc36151b8998064e706f70d9ea6.tar.xz
mullvadvpn-341c10ba38752bc36151b8998064e706f70d9ea6.zip
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.
-rw-r--r--Cargo.lock25
-rw-r--r--android/app/src/test/kotlin/net/mullvad/talpid/TalpidVpnServiceFallbackDnsTest.kt146
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt46
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt10
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt55
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt168
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt41
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkState.kt19
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/RouteInfo.kt18
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/ConnectivityManagerUtil.kt111
-rw-r--r--mullvad-jni/src/classes.rs4
-rw-r--r--mullvad-jni/src/lib.rs16
-rw-r--r--mullvad-jni/src/talpid_vpn_service.rs181
-rw-r--r--talpid-core/src/connectivity_listener.rs1
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs1
-rw-r--r--talpid-routing/Cargo.toml5
-rw-r--r--talpid-routing/src/lib.rs34
-rw-r--r--talpid-routing/src/unix/android.rs154
-rw-r--r--talpid-routing/src/unix/linux.rs1
-rw-r--r--talpid-routing/src/unix/macos/mod.rs3
-rw-r--r--talpid-routing/src/unix/mod.rs47
-rw-r--r--talpid-tunnel/src/tun_provider/android/mod.rs7
-rw-r--r--talpid-types/src/android/mod.rs53
-rw-r--r--talpid-wireguard/src/lib.rs8
24 files changed, 745 insertions, 409 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e7a426717d..d280ba649b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -431,9 +431,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.7.1"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
+checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "c2rust-bitfields"
@@ -1248,9 +1248,9 @@ dependencies = [
[[package]]
name = "futures"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
@@ -1279,9 +1279,9 @@ checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
@@ -2107,9 +2107,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jnix"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd797d41e48568eb956ded20d7e5e3f2df1c02980d9e5b9aab9b47bd3a9f599"
+checksum = "542b2072131a62ec940ee161ff0a01e7a1c2a129796b30143efc952cb6e0f28f"
dependencies = [
"jni",
"jnix-macros",
@@ -2196,9 +2196,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.158"
+version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libdbus-sys"
@@ -2888,9 +2888,9 @@ dependencies = [
[[package]]
name = "netlink-sys"
-version = "0.8.6"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "416060d346fbaf1f23f9512963e3e878f1a78e707cb699ba9215761754244307"
+checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
dependencies = [
"bytes",
"futures",
@@ -4789,6 +4789,7 @@ dependencies = [
"bitflags 2.6.0",
"futures",
"ipnetwork",
+ "jnix",
"libc",
"log",
"netlink-packet-route",
diff --git a/android/app/src/test/kotlin/net/mullvad/talpid/TalpidVpnServiceFallbackDnsTest.kt b/android/app/src/test/kotlin/net/mullvad/talpid/TalpidVpnServiceFallbackDnsTest.kt
new file mode 100644
index 0000000000..27e7658a11
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/talpid/TalpidVpnServiceFallbackDnsTest.kt
@@ -0,0 +1,146 @@
+package net.mullvad.talpid
+
+import android.net.VpnService
+import android.os.ParcelFileDescriptor
+import arrow.core.right
+import io.mockk.MockKAnnotations
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkConstructor
+import io.mockk.mockkStatic
+import io.mockk.spyk
+import java.net.InetAddress
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe
+import net.mullvad.mullvadvpn.lib.model.Prepared
+import net.mullvad.talpid.model.CreateTunResult
+import net.mullvad.talpid.model.InetNetwork
+import net.mullvad.talpid.model.TunConfig
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertInstanceOf
+
+class TalpidVpnServiceFallbackDnsTest {
+ lateinit var talpidVpnService: TalpidVpnService
+ var builderMockk = mockk<VpnService.Builder>()
+
+ @BeforeEach
+ fun setup() {
+ MockKAnnotations.init(this)
+ mockkStatic(VPN_SERVICE_EXTENSION)
+
+ talpidVpnService = spyk<TalpidVpnService>(recordPrivateCalls = true)
+ every { talpidVpnService.prepareVpnSafe() } returns Prepared.right()
+ builderMockk = mockk<VpnService.Builder>()
+
+ mockkConstructor(VpnService.Builder::class)
+ every { anyConstructed<VpnService.Builder>().setMtu(any()) } returns builderMockk
+ every { anyConstructed<VpnService.Builder>().setBlocking(any()) } returns builderMockk
+ every { anyConstructed<VpnService.Builder>().addAddress(any<InetAddress>(), any()) } returns
+ builderMockk
+ every { anyConstructed<VpnService.Builder>().addRoute(any<InetAddress>(), any()) } returns
+ builderMockk
+ every {
+ anyConstructed<VpnService.Builder>()
+ .addDnsServer(TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER)
+ } returns builderMockk
+ val parcelFileDescriptor: ParcelFileDescriptor = mockk()
+ every { anyConstructed<VpnService.Builder>().establish() } returns parcelFileDescriptor
+ every { parcelFileDescriptor.detachFd() } returns 1
+ }
+
+ @Test
+ fun `opening tun with no DnsServers should add fallback DNS server`() {
+ val tunConfig = baseTunConfig.copy(dnsServers = arrayListOf())
+
+ val result = talpidVpnService.openTun(tunConfig)
+
+ assertInstanceOf<CreateTunResult.Success>(result)
+
+ // Fallback DNS server should be added if no DNS servers are provided
+ coVerify(exactly = 1) {
+ anyConstructed<VpnService.Builder>()
+ .addDnsServer(TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER)
+ }
+ }
+
+ @Test
+ fun `opening tun with all bad DnsServers should return InvalidDnsServers and add fallback`() {
+ val badDns1 = InetAddress.getByName("0.0.0.0")
+ val badDns2 = InetAddress.getByName("255.255.255.255")
+ every { anyConstructed<VpnService.Builder>().addDnsServer(badDns1) } throws
+ IllegalArgumentException()
+ every { anyConstructed<VpnService.Builder>().addDnsServer(badDns2) } throws
+ IllegalArgumentException()
+
+ val tunConfig = baseTunConfig.copy(dnsServers = arrayListOf(badDns1, badDns2))
+ val result = talpidVpnService.openTun(tunConfig)
+
+ assertInstanceOf<CreateTunResult.InvalidDnsServers>(result)
+ assertLists(tunConfig.dnsServers, result.addresses)
+ // Fallback DNS server should be added if no valid DNS servers are provided
+ coVerify(exactly = 1) {
+ anyConstructed<VpnService.Builder>()
+ .addDnsServer(TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER)
+ }
+ }
+
+ @Test
+ fun `opening tun with 1 good and 1 bad DnsServers should return InvalidDnsServers`() {
+ val goodDnsServer = InetAddress.getByName("1.1.1.1")
+ val badDns = InetAddress.getByName("255.255.255.255")
+ every { anyConstructed<VpnService.Builder>().addDnsServer(goodDnsServer) } returns
+ builderMockk
+ every { anyConstructed<VpnService.Builder>().addDnsServer(badDns) } throws
+ IllegalArgumentException()
+
+ val tunConfig = baseTunConfig.copy(dnsServers = arrayListOf(goodDnsServer, badDns))
+ val result = talpidVpnService.openTun(tunConfig)
+
+ assertInstanceOf<CreateTunResult.InvalidDnsServers>(result)
+ assertLists(arrayListOf(badDns), result.addresses)
+
+ // Fallback DNS server should not be added since we have 1 good DNS server
+ coVerify(exactly = 0) {
+ anyConstructed<VpnService.Builder>()
+ .addDnsServer(TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER)
+ }
+ }
+
+ @Test
+ fun `providing good dns servers should not add the fallback dns and return success`() {
+ val goodDnsServer = InetAddress.getByName("1.1.1.1")
+ every { anyConstructed<VpnService.Builder>().addDnsServer(goodDnsServer) } returns
+ builderMockk
+
+ val tunConfig = baseTunConfig.copy(dnsServers = arrayListOf(goodDnsServer))
+ val result = talpidVpnService.openTun(tunConfig)
+
+ assertInstanceOf<CreateTunResult.Success>(result)
+
+ // Fallback DNS server should not be added since we have good DNS servers.
+ coVerify(exactly = 0) {
+ anyConstructed<VpnService.Builder>()
+ .addDnsServer(TalpidVpnService.FALLBACK_DUMMY_DNS_SERVER)
+ }
+ }
+
+ companion object {
+ private const val VPN_SERVICE_EXTENSION =
+ "net.mullvad.mullvadvpn.lib.common.util.VpnServiceUtilsKt"
+
+ val baseTunConfig =
+ TunConfig(
+ addresses = arrayListOf(InetAddress.getByName("45.83.223.209")),
+ dnsServers = arrayListOf(),
+ routes =
+ arrayListOf(
+ InetNetwork(InetAddress.getByName("0.0.0.0"), 0),
+ InetNetwork(InetAddress.getByName("::"), 0),
+ ),
+ mtu = 1280,
+ excludedPackages = arrayListOf(),
+ )
+ }
+}
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 59833cb396..06c862936b 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
/**
+ * Prepare to establish a VPN connection safely.
+ *
* 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<PrepareError, Prepared> =
else -> throw it
}
}
- .map { intent ->
+ .flatMap { intent ->
if (intent == null) {
Prepared.right()
} else {
@@ -46,7 +52,6 @@ fun Context.prepareVpnSafe(): Either<PrepareError, Prepared> =
}
}
}
- .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<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
+}
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
index daa04fc8d9..fe4cf11881 100644
--- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
@@ -36,9 +36,6 @@ import net.mullvad.mullvadvpn.lib.model.DnsState
import net.mullvad.mullvadvpn.lib.model.Endpoint
import net.mullvad.mullvadvpn.lib.model.ErrorState
import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
-import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.AuthFailed
-import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.OtherAlwaysOnApp
-import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.TunnelParameterError
import net.mullvad.mullvadvpn.lib.model.FeatureIndicator
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
@@ -125,7 +122,7 @@ private fun ManagementInterface.TunnelState.Error.toDomain(): TunnelState.Error
val otherAlwaysOnAppError =
errorState.let {
if (it.hasOtherAlwaysOnAppError()) {
- OtherAlwaysOnApp(it.otherAlwaysOnAppError.appName)
+ ErrorStateCause.OtherAlwaysOnApp(it.otherAlwaysOnAppError.appName)
} else {
null
}
@@ -238,7 +235,7 @@ internal fun ManagementInterface.ErrorState.toDomain(
cause =
when (cause!!) {
ManagementInterface.ErrorState.Cause.AUTH_FAILED ->
- AuthFailed(authFailedError.toDomain())
+ ErrorStateCause.AuthFailed(authFailedError.toDomain())
ManagementInterface.ErrorState.Cause.IPV6_UNAVAILABLE ->
ErrorStateCause.Ipv6Unavailable
ManagementInterface.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR ->
@@ -247,7 +244,7 @@ internal fun ManagementInterface.ErrorState.toDomain(
ManagementInterface.ErrorState.Cause.START_TUNNEL_ERROR ->
ErrorStateCause.StartTunnelError
ManagementInterface.ErrorState.Cause.TUNNEL_PARAMETER_ERROR ->
- TunnelParameterError(parameterError.toDomain())
+ ErrorStateCause.TunnelParameterError(parameterError.toDomain())
ManagementInterface.ErrorState.Cause.IS_OFFLINE -> ErrorStateCause.IsOffline
ManagementInterface.ErrorState.Cause.SPLIT_TUNNEL_ERROR ->
ErrorStateCause.StartTunnelError
@@ -255,7 +252,6 @@ internal fun ManagementInterface.ErrorState.toDomain(
ManagementInterface.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS,
ManagementInterface.ErrorState.Cause.CREATE_TUNNEL_DEVICE ->
throw IllegalArgumentException("Unrecognized error state cause")
-
ManagementInterface.ErrorState.Cause.NOT_PREPARED -> ErrorStateCause.NotPrepared
ManagementInterface.ErrorState.Cause.OTHER_ALWAYS_ON_APP -> otherAlwaysOnApp!!
ManagementInterface.ErrorState.Cause.OTHER_LEGACY_ALWAYS_ON_VPN ->
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 86b27e3ba8..fdee5039ad 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
@@ -7,34 +7,48 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest
import co.touchlab.kermit.Logger
import java.net.InetAddress
+import kotlin.collections.ArrayList
import kotlinx.coroutines.CoroutineScope
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.model.NetworkState
import net.mullvad.talpid.util.NetworkEvent
-import net.mullvad.talpid.util.defaultNetworkFlow
-import net.mullvad.talpid.util.networkFlow
+import net.mullvad.talpid.util.RawNetworkState
+import net.mullvad.talpid.util.defaultRawNetworkStateFlow
+import net.mullvad.talpid.util.networkEvents
-class ConnectivityListener(val connectivityManager: ConnectivityManager) {
+class ConnectivityListener(private val connectivityManager: ConnectivityManager) {
private lateinit var _isConnected: StateFlow<Boolean>
// Used by JNI
val isConnected
get() = _isConnected.value
- private lateinit var _currentDnsServers: StateFlow<List<InetAddress>>
+ private lateinit var _currentNetworkState: StateFlow<NetworkState?>
+
+ // Used by JNI
+ val currentDefaultNetworkState: NetworkState?
+ get() = _currentNetworkState.value
+
// Used by JNI
- val currentDnsServers
- get() = ArrayList(_currentDnsServers.value)
+ val currentDnsServers: ArrayList<InetAddress>
+ get() = _currentNetworkState.value?.dnsServers ?: ArrayList()
fun register(scope: CoroutineScope) {
- _currentDnsServers =
- dnsServerChanges().stateIn(scope, SharingStarted.Eagerly, currentDnsServers())
+ // Consider implementing retry logic for the flows below, because registering a listener on
+ // the default network may fail if the network on Android 11
+ // https://issuetracker.google.com/issues/175055271?pli=1
+ _currentNetworkState =
+ connectivityManager
+ .defaultRawNetworkStateFlow()
+ .map { it?.toNetworkState() }
+ .onEach { notifyDefaultNetworkChange(it) }
+ .stateIn(scope, SharingStarted.Eagerly, null)
_isConnected =
hasInternetCapability()
@@ -42,18 +56,6 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) {
.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 }
@@ -65,7 +67,7 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) {
.build()
return connectivityManager
- .networkFlow(request)
+ .networkEvents(request)
.scan(setOf<Network>()) { networks, event ->
when (event) {
is NetworkEvent.Available -> {
@@ -87,5 +89,14 @@ class ConnectivityListener(val connectivityManager: ConnectivityManager) {
.distinctUntilChanged()
}
+ private fun RawNetworkState.toNetworkState(): NetworkState =
+ NetworkState(
+ network.networkHandle,
+ linkProperties?.routes,
+ linkProperties?.dnsServersWithoutFallback(),
+ )
+
private external fun notifyConnectivityChange(isConnected: Boolean)
+
+ private external fun notifyDefaultNetworkChange(networkState: NetworkState?)
}
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 74d44005cd..a143df6132 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,29 @@
package net.mullvad.talpid
import android.net.ConnectivityManager
+import android.net.VpnService
import android.os.ParcelFileDescriptor
import androidx.annotation.CallSuper
import androidx.core.content.getSystemService
import androidx.lifecycle.lifecycleScope
+import arrow.core.Either
+import arrow.core.mapOrAccumulate
+import arrow.core.merge
+import arrow.core.raise.either
import co.touchlab.kermit.Logger
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import kotlin.properties.Delegates.observable
+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.TunConfig
import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported
@@ -22,7 +33,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 +54,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,95 +85,76 @@ 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<InetAddress>()
+ private fun createTun(
+ config: TunConfig
+ ): Either<CreateTunResult.Error, CreateTunResult.Success> = either {
+ prepareVpnSafe().mapLeft { it.toCreateTunError() }.bind()
- val builder =
- Builder().apply {
- for (address in config.addresses) {
- addAddress(address, address.prefixLength())
- }
+ val builder = Builder()
+ builder.setMtu(config.mtu)
+ builder.setBlocking(false)
+ builder.setMeteredIfSupported(false)
- for (dnsServer in config.dnsServers) {
- try {
- addDnsServer(dnsServer)
- } catch (exception: IllegalArgumentException) {
- invalidDnsServerAddresses.add(dnsServer)
- }
- }
+ 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) }
- // 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 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
+ // We don't care if adding DNS servers fails at this point, since we can still create a
+ // tunnel to consume traffic and then notify daemon to later enter blocked state.
+ val dnsConfigureResult =
+ config.dnsServers.mapOrAccumulate {
+ builder.addDnsServerSafe(it).bind()
+ Unit
}
- if (vpnInterfaceFd == null) {
- Logger.e("VpnInterface returned null")
- return CreateTunResult.TunnelDeviceError
+ // Never create a tunnel where all DNS servers are invalid or if none was ever set, since
+ // apps then may leak DNS requests.
+ // https://issuetracker.google.com/issues/337961996
+ val shouldAddFallbackDns =
+ dnsConfigureResult.fold(
+ { invalidDnsServers -> invalidDnsServers.size == config.dnsServers.size },
+ { addedDnsServers -> addedDnsServers.isEmpty() },
+ )
+ if (shouldAddFallbackDns) {
+ 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)
}
- val tunFd = vpnInterfaceFd.detachFd()
-
- waitForTunnelUp(tunFd, config.routes.any { route -> route.isIpv6 })
+ val vpnInterfaceFd =
+ builder
+ .establishSafe()
+ .onLeft { Logger.w("Failed to establish tunnel $it") }
+ .mapLeft { EstablishError }
+ .bind()
- if (invalidDnsServerAddresses.isNotEmpty()) {
- return CreateTunResult.InvalidDnsServers(invalidDnsServerAddresses, tunFd)
- }
+ val tunFd = vpnInterfaceFd.detachFd()
- return CreateTunResult.Success(tunFd)
- }
+ dnsConfigureResult.mapLeft { InvalidDnsServers(it, tunFd) }.bind()
- fun bypass(socket: Int): Boolean {
- return protect(socket)
+ CreateTunResult.Success(tunFd)
}
- private fun PrepareError.toCreateTunResult() =
+ private fun PrepareError.toCreateTunError() =
when (this) {
- is PrepareError.OtherLegacyAlwaysOnVpn -> CreateTunResult.OtherLegacyAlwaysOnVpn
- is PrepareError.NotPrepared -> CreateTunResult.NotPrepared
- is PrepareError.OtherAlwaysOnApp -> CreateTunResult.OtherAlwaysOnApp(appName)
+ is PrepareError.OtherLegacyAlwaysOnVpn -> OtherLegacyAlwaysOnVpn
+ is PrepareError.NotPrepared -> NotPrepared
+ is PrepareError.OtherAlwaysOnApp -> OtherAlwaysOnApp(appName)
}
+ private fun Builder.addDnsServerSafe(
+ dnsServer: InetAddress
+ ): Either<InetAddress, VpnService.Builder> =
+ Either.catch { addDnsServer(dnsServer) }
+ .mapLeft {
+ when (it) {
+ is IllegalArgumentException -> dnsServer
+ else -> throw it
+ }
+ }
+
private fun InetAddress.prefixLength(): Int =
when (this) {
is Inet4Address -> IPV4_PREFIX_LENGTH
@@ -166,8 +162,6 @@ 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"
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 3cd73685f7..ef10dcd2f3 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,38 @@
package net.mullvad.talpid.model
import java.net.InetAddress
+import java.util.ArrayList
-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()
+ data class InvalidDnsServers(val addresses: ArrayList<InetAddress>, val tunFd: Int) : Error {
+ constructor(address: List<InetAddress>, tunFd: Int) : this(ArrayList(address), tunFd)
+
+ override val isOpen = true
+ }
}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkState.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkState.kt
new file mode 100644
index 0000000000..ca0b6db7e2
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkState.kt
@@ -0,0 +1,19 @@
+package net.mullvad.talpid.model
+
+import java.net.InetAddress
+
+data class NetworkState(
+ val networkHandle: Long,
+ val routes: ArrayList<RouteInfo>?,
+ val dnsServers: ArrayList<InetAddress>?,
+) {
+ constructor(
+ networkHandle: Long,
+ routes: List<AndroidRouteInfo>?,
+ dnsServers: List<InetAddress>?,
+ ) : this(
+ networkHandle = networkHandle,
+ routes = routes?.map { it.toRoute() }?.let { ArrayList(it) },
+ dnsServers = dnsServers?.let { ArrayList(it) },
+ )
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/RouteInfo.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/RouteInfo.kt
new file mode 100644
index 0000000000..a2b63b3ca7
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/RouteInfo.kt
@@ -0,0 +1,18 @@
+package net.mullvad.talpid.model
+
+import java.net.InetAddress
+
+typealias AndroidRouteInfo = android.net.RouteInfo
+
+data class RouteInfo(
+ val destination: InetNetwork,
+ val gateway: InetAddress?,
+ val interfaceName: String?,
+)
+
+fun AndroidRouteInfo.toRoute() =
+ RouteInfo(
+ destination = InetNetwork(destination.address, destination.prefixLength.toShort()),
+ gateway = gateway,
+ interfaceName = `interface`,
+ )
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 daf155c6e8..fddaa6fb88 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,56 @@ 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<NetworkEvent> =
- callbackFlow<NetworkEvent> {
- val callback =
- object : NetworkCallback() {
- override fun onLinkPropertiesChanged(
- network: Network,
- linkProperties: LinkProperties,
- ) {
- super.onLinkPropertiesChanged(network, linkProperties)
- trySendBlocking(NetworkEvent.LinkPropertiesChanged(network, linkProperties))
- }
+internal fun ConnectivityManager.defaultNetworkEvents(): Flow<NetworkEvent> = 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.networkFlow(networkRequest: NetworkRequest): Flow<NetworkEvent> =
- callbackFlow<NetworkEvent> {
+fun ConnectivityManager.networkEvents(networkRequest: NetworkRequest): Flow<NetworkEvent> =
+ callbackFlow {
val callback =
object : NetworkCallback() {
override fun onLinkPropertiesChanged(
@@ -111,6 +108,26 @@ fun ConnectivityManager.networkFlow(networkRequest: NetworkRequest): Flow<Networ
awaitClose { unregisterNetworkCallback(callback) }
}
+internal fun ConnectivityManager.defaultRawNetworkStateFlow(): Flow<RawNetworkState?> =
+ defaultNetworkEvents()
+ .scan(
+ null as RawNetworkState?,
+ { state, event ->
+ return@scan when (event) {
+ is NetworkEvent.Available -> RawNetworkState(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
+ }
+ },
+ )
+
sealed interface NetworkEvent {
data class Available(val network: Network) : NetworkEvent
@@ -130,3 +147,11 @@ sealed interface NetworkEvent {
data class Lost(val network: Network) : NetworkEvent
}
+
+internal data class RawNetworkState(
+ 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 8312657efb..f773d3adca 100644
--- a/mullvad-jni/src/classes.rs
+++ b/mullvad-jni/src/classes.rs
@@ -7,12 +7,14 @@ pub const CLASSES: &[&str] = &[
"net/mullvad/mullvadvpn/service/MullvadVpnService",
"net/mullvad/talpid/model/InetNetwork",
"net/mullvad/talpid/model/TunConfig",
+ "net/mullvad/talpid/model/NetworkState",
+ "net/mullvad/talpid/model/RouteInfo",
"net/mullvad/talpid/model/CreateTunResult$Success",
"net/mullvad/talpid/model/CreateTunResult$InvalidDnsServers",
"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/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 fd35396fd0..8b1018d926 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::{
@@ -88,17 +87,25 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_initial
assert!(ctx.is_none(), "multiple calls to MullvadDaemon.initialize");
let env = JnixEnv::from(env);
+ let files_dir = pathbuf_from_java(&env, files_directory);
+ start_logging(&files_dir)
+ .map_err(Error::InitializeLogging)
+ .unwrap();
+ version::log_version();
+ log::info!("Pre-loading classes!");
LOAD_CLASSES.call_once(|| env.preload_classes(classes::CLASSES.iter().cloned()));
+ log::info!("Done loading classes");
let rpc_socket = pathbuf_from_java(&env, rpc_socket_path);
- let files_dir = pathbuf_from_java(&env, files_directory);
let cache_dir = pathbuf_from_java(&env, cache_directory);
let android_context = ok_or_throw!(&env, create_android_context(&env, vpn_service));
+ log::info!("Created Android Context");
let api_endpoint = api::api_endpoint_from_java(&env, api_endpoint);
+ log::info!("Starting daemon");
let daemon = ok_or_throw!(
&env,
start(
@@ -134,11 +141,8 @@ fn start(
rpc_socket: PathBuf,
files_dir: PathBuf,
cache_dir: PathBuf,
- api_endpoint: Option<mullvad_api::ApiEndpoint>,
+ api_endpoint: Option<ApiEndpoint>,
) -> Result<DaemonContext, Error> {
- start_logging(&files_dir).map_err(Error::InitializeLogging)?;
- version::log_version();
-
#[cfg(not(feature = "api-override"))]
if api_endpoint.is_some() {
log::warn!("api_endpoint will be ignored since 'api-override' is not enabled");
diff --git a/mullvad-jni/src/talpid_vpn_service.rs b/mullvad-jni/src/talpid_vpn_service.rs
deleted file mode 100644
index ea6928538a..0000000000
--- 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<u8> {
- 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<IpNetwork> = 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-core/src/connectivity_listener.rs b/talpid-core/src/connectivity_listener.rs
index 9bdf4bf87a..033d918e16 100644
--- a/talpid-core/src/connectivity_listener.rs
+++ b/talpid-core/src/connectivity_listener.rs
@@ -47,6 +47,7 @@ pub struct ConnectivityListener {
android_listener: GlobalRef,
}
+// Clean this up
static CONNECTIVITY_TX: Mutex<Option<UnboundedSender<Connectivity>>> = Mutex::new(None);
impl ConnectivityListener {
diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs
index 7c7637cd20..9060787536 100644
--- a/talpid-core/src/tunnel_state_machine/connecting_state.rs
+++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs
@@ -138,6 +138,7 @@ impl ConnectingState {
&shared_values.route_manager,
retry_attempt,
);
+
let params = connecting_state.tunnel_parameters.clone();
(
Box::new(connecting_state),
diff --git a/talpid-routing/Cargo.toml b/talpid-routing/Cargo.toml
index 14f30b8333..b4d3e2a747 100644
--- a/talpid-routing/Cargo.toml
+++ b/talpid-routing/Cargo.toml
@@ -16,10 +16,11 @@ futures = { workspace = true }
ipnetwork = { workspace = true }
log = { workspace = true }
tokio = { workspace = true, features = ["process", "rt-multi-thread", "net", "io-util", "time"] }
-
-[target.'cfg(not(target_os="android"))'.dependencies]
talpid-types = { path = "../talpid-types" }
+[target.'cfg(target_os = "android")'.dependencies]
+jnix = { version = "0.5.2", features = ["derive"] }
+
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
rtnetlink = "0.11"
diff --git a/talpid-routing/src/lib.rs b/talpid-routing/src/lib.rs
index b80f96ccdc..89effbdd46 100644
--- a/talpid-routing/src/lib.rs
+++ b/talpid-routing/src/lib.rs
@@ -24,7 +24,10 @@ mod imp;
use netlink_packet_route::rtnl::constants::RT_TABLE_MAIN;
#[cfg(target_os = "macos")]
-pub use imp::{imp::RouteError, DefaultRouteEvent, PlatformError};
+pub use imp::{
+ imp::{DefaultRouteEvent, RouteError},
+ PlatformError,
+};
pub use imp::{Error, RouteManagerHandle};
@@ -70,6 +73,7 @@ pub struct Gateway {
}
/// A network route with a specific network node, destination and an optional metric.
+#[cfg(not(target_os = "android"))]
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct Route {
node: Node,
@@ -81,8 +85,14 @@ pub struct Route {
mtu: Option<u32>,
}
+/// A network route with a specific network node, destination and an optional metric.
+#[cfg(target_os = "android")]
+#[derive(Debug, Hash, Eq, PartialEq, Clone)]
+pub struct Route(IpNetwork);
+
impl Route {
/// Construct a new Route
+ #[cfg(not(target_os = "android"))]
pub fn new(node: Node, prefix: IpNetwork) -> Self {
Self {
node,
@@ -95,6 +105,12 @@ impl Route {
}
}
+ /// Construct a new Route
+ #[cfg(target_os = "android")]
+ pub fn new(prefix: IpNetwork) -> Self {
+ Self(prefix)
+ }
+
#[cfg(target_os = "linux")]
fn table(mut self, new_id: u32) -> Self {
self.table_id = new_id;
@@ -102,11 +118,13 @@ impl Route {
}
/// Returns the network node of the route.
+ #[cfg(target_os = "linux")]
pub fn get_node(&self) -> &Node {
&self.node
}
}
+#[cfg(target_os = "linux")]
impl fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} via {}", self.prefix, self.node)?;
@@ -123,9 +141,22 @@ impl fmt::Display for Route {
}
}
+#[cfg(target_os = "android")]
+impl From<&talpid_types::android::RouteInfo> for Route {
+ fn from(route_info: &talpid_types::android::RouteInfo) -> Self {
+ let network = IpNetwork::new(
+ route_info.destination.address,
+ route_info.destination.prefix_length as u8,
+ )
+ .unwrap();
+ Self::new(network)
+ }
+}
+
/// A network route that should be applied by the route manager.
/// It can either be routed through a specific network node or it can be routed through the current
/// default route.
+#[cfg(not(target_os = "android"))]
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct RequiredRoute {
/// Route's prefix
@@ -139,6 +170,7 @@ pub struct RequiredRoute {
mtu: Option<u16>,
}
+#[cfg(not(target_os = "android"))]
impl RequiredRoute {
/// Constructs a new required route.
pub fn new(prefix: IpNetwork, node: impl Into<NetNode>) -> Self {
diff --git a/talpid-routing/src/unix/android.rs b/talpid-routing/src/unix/android.rs
index 8abb23859b..bea2f0d33d 100644
--- a/talpid-routing/src/unix/android.rs
+++ b/talpid-routing/src/unix/android.rs
@@ -1,37 +1,159 @@
-use crate::imp::RouteManagerCommand;
-use futures::{channel::mpsc, stream::StreamExt};
+use std::collections::HashSet;
+use std::ops::{ControlFlow, Not};
+use std::sync::Mutex;
+
+use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender};
+use futures::channel::oneshot;
+use futures::future::FutureExt;
+use futures::select_biased;
+use futures::stream::StreamExt;
+use jnix::jni::{objects::JObject, JNIEnv};
+use jnix::{FromJava, JnixEnv};
+
+use talpid_types::android::NetworkState;
+
+use crate::{imp::RouteManagerCommand, Route};
/// Stub error type for routing errors on Android.
+/// Errors that occur while setting up VpnService tunnel.
#[derive(Debug, thiserror::Error)]
-#[error("Failed to send shutdown result")]
-pub struct Error;
+pub enum Error {
+ /// Timed out when waiting for network routes.
+ #[error("Timed out when waiting for network routes")]
+ RoutesTimedOut,
+}
+
+/// The sender used by [Java_net_mullvad_talpid_ConnectivityListener_notifyDefaultNetworkChange]
+/// to notify the route manager of changes to the network.
+static ROUTE_UPDATES_TX: Mutex<Option<UnboundedSender<Option<NetworkState>>>> = Mutex::new(None);
-/// Stub route manager for Android
-pub struct RouteManagerImpl {}
+/// Android route manager actor.
+#[derive(Debug)]
+pub struct RouteManagerImpl {
+ /// The receiving channel for updates on changes to the network.
+ network_state_updates: UnboundedReceiver<Option<NetworkState>>,
+
+ /// Cached [NetworkState]. If no update events have been received yet, this value will be [None].
+ last_state: Option<NetworkState>,
+
+ /// Clients waiting on response to [RouteManagerCommand::WaitForRoutes].
+ waiting_for_routes: Vec<oneshot::Sender<()>>,
+}
impl RouteManagerImpl {
#[allow(clippy::unused_async)]
pub async fn new() -> Result<Self, Error> {
- Ok(RouteManagerImpl {})
+ // Create a channel between the kotlin client and route manager
+ let (tx, rx) = futures::channel::mpsc::unbounded();
+
+ *ROUTE_UPDATES_TX.lock().unwrap() = Some(tx);
+
+ let route_manager = RouteManagerImpl {
+ network_state_updates: rx,
+ last_state: Default::default(),
+ waiting_for_routes: Default::default(),
+ };
+
+ Ok(route_manager)
}
pub(crate) async fn run(
- self,
+ mut self,
manage_rx: mpsc::UnboundedReceiver<RouteManagerCommand>,
) -> Result<(), Error> {
let mut manage_rx = manage_rx.fuse();
- while let Some(command) = manage_rx.next().await {
- match command {
- RouteManagerCommand::Shutdown(tx) => {
- tx.send(()).map_err(|()| Error)?;
- break;
+
+ loop {
+ select_biased! {
+ command = manage_rx.next().fuse() => {
+ let Some(command) = command else { break };
+ if self.handle_command(command).is_break() {
+ break;
+ }
}
- RouteManagerCommand::AddRoutes(_routes, tx) => {
- let _ = tx.send(Ok(()));
+
+ network_state_update = self.network_state_updates.next().fuse() => {
+ // None means that the sender was dropped
+ let Some(network_state) = network_state_update else { break };
+ // update the last known NetworkState
+ self.last_state = network_state;
+
+ if has_routes(self.last_state.as_ref()) {
+ // notify waiting clients that routes exist
+ for client in self.waiting_for_routes.drain(..) {
+ let _ = client.send(());
+ }
+ }
}
- RouteManagerCommand::ClearRoutes => (),
}
}
+
+ log::debug!("RouteManager exited");
+
Ok(())
}
+
+ fn handle_command(&mut self, command: RouteManagerCommand) -> ControlFlow<()> {
+ match command {
+ RouteManagerCommand::Shutdown(tx) => {
+ let _ = tx.send(());
+ return ControlFlow::Break(());
+ }
+ RouteManagerCommand::WaitForRoutes(response_tx) => {
+ // check if routes have already been configured on the Android system.
+ // otherwise, register a listener for network state changes.
+ // routes may come in at any moment in the future.
+ if has_routes(self.last_state.as_ref()) {
+ let _ = response_tx.send(());
+ } else {
+ self.waiting_for_routes.push(response_tx);
+ }
+ }
+ }
+
+ ControlFlow::Continue(())
+ }
+}
+
+/// Check whether the [NetworkState] contains any routes.
+///
+/// Since we are the ones telling Android what routes to set, we make the assumption that:
+/// If any routes exist whatsoever, they are the the routes we specified.
+fn has_routes(state: Option<&NetworkState>) -> bool {
+ let Some(network_state) = state else {
+ return false;
+ };
+ configured_routes(network_state).is_empty().not()
+}
+
+fn configured_routes(state: &NetworkState) -> HashSet<Route> {
+ match &state.routes {
+ None => Default::default(),
+ Some(route_info) => route_info.iter().map(Route::from).collect(),
+ }
+}
+
+/// Entry point for Android Java code to notify the current default network state.
+#[no_mangle]
+#[allow(non_snake_case)]
+pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyDefaultNetworkChange(
+ env: JNIEnv<'_>,
+ _: JObject<'_>,
+ network_state: JObject<'_>,
+) {
+ let env = JnixEnv::from(env);
+
+ let network_state: Option<NetworkState> = FromJava::from_java(&env, network_state);
+
+ let Some(tx) = &*ROUTE_UPDATES_TX.lock().unwrap() else {
+ // No sender has been registered
+ log::error!("Received routes notification wíth no channel");
+ return;
+ };
+
+ log::trace!("Received network state update {:#?}", network_state);
+
+ if tx.unbounded_send(network_state).is_err() {
+ log::warn!("Failed to send offline change event");
+ }
}
diff --git a/talpid-routing/src/unix/linux.rs b/talpid-routing/src/unix/linux.rs
index 92b4513301..a43f0690bc 100644
--- a/talpid-routing/src/unix/linux.rs
+++ b/talpid-routing/src/unix/linux.rs
@@ -86,6 +86,7 @@ pub type Result<T> = std::result::Result<T, Error>;
/// Errors that can happen in the Linux routing integration
#[derive(thiserror::Error, Debug)]
+#[allow(missing_docs)]
pub enum Error {
#[error("Failed to open a netlink connection")]
Connect(#[source] io::Error),
diff --git a/talpid-routing/src/unix/macos/mod.rs b/talpid-routing/src/unix/macos/mod.rs
index 85a020ba79..df89767e38 100644
--- a/talpid-routing/src/unix/macos/mod.rs
+++ b/talpid-routing/src/unix/macos/mod.rs
@@ -16,9 +16,10 @@ use std::{
use talpid_types::ErrorExt;
use watch::RoutingTable;
-use super::{DefaultRouteEvent, RouteManagerCommand};
+use super::RouteManagerCommand;
use data::{Destination, RouteDestination, RouteMessage, RouteSocketMessage};
+pub use super::DefaultRouteEvent;
pub use interface::DefaultRoute;
mod data;
diff --git a/talpid-routing/src/unix/mod.rs b/talpid-routing/src/unix/mod.rs
index 34d2570137..300ccff918 100644
--- a/talpid-routing/src/unix/mod.rs
+++ b/talpid-routing/src/unix/mod.rs
@@ -1,18 +1,21 @@
-#[cfg(target_os = "linux")]
-use crate::Route;
#[cfg(target_os = "macos")]
pub use crate::{imp::imp::DefaultRoute, Gateway};
+#[cfg(any(target_os = "linux", target_os = "macos"))]
use super::RequiredRoute;
+#[cfg(target_os = "linux")]
+use super::Route;
use futures::channel::{
mpsc::{self, UnboundedSender},
oneshot,
};
-use std::{collections::HashSet, sync::Arc};
+use std::sync::Arc;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use futures::stream::Stream;
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use std::collections::HashSet;
#[cfg(target_os = "linux")]
use std::net::IpAddr;
@@ -32,6 +35,7 @@ mod imp;
#[path = "android.rs"]
mod imp;
+#[cfg(any(target_os = "macos", target_os = "linux"))]
pub use imp::Error as PlatformError;
/// Errors that can be encountered whilst interacting with a [RouteManagerHandle].
@@ -97,11 +101,7 @@ pub(crate) enum RouteManagerCommand {
#[cfg(target_os = "android")]
#[derive(Debug)]
pub(crate) enum RouteManagerCommand {
- AddRoutes(
- HashSet<RequiredRoute>,
- oneshot::Sender<Result<(), PlatformError>>,
- ),
- ClearRoutes,
+ WaitForRoutes(oneshot::Sender<()>),
Shutdown(oneshot::Sender<()>),
}
@@ -192,6 +192,7 @@ impl RouteManagerHandle {
}
/// Applies the given routes until they are cleared
+ #[cfg(not(target_os = "android"))]
pub async fn add_routes(&self, routes: HashSet<RequiredRoute>) -> Result<(), Error> {
let (result_tx, result_rx) = oneshot::channel();
self.tx
@@ -204,13 +205,43 @@ impl RouteManagerHandle {
.map_err(Error::PlatformError)
}
+ /// Wait for routes to come up.
+ ///
+ /// This function is guaranteed to *not* wait for longer than 2 seconds.
+ /// Please, see the implementation of this function for further details.
+ #[cfg(target_os = "android")]
+ pub async fn wait_for_routes(&self) -> Result<(), Error> {
+ use std::time::Duration;
+ use tokio::time::timeout;
+ /// Maximum time to wait for routes to come up. The expected mean time is low (~200 ms), but
+ /// we add some additional margin to give some slack to slower hardware primarily.
+ const WAIT_FOR_ROUTES_TIMEOUT: Duration = Duration::from_secs(2);
+
+ let (result_tx, result_rx) = oneshot::channel();
+ self.tx
+ .unbounded_send(RouteManagerCommand::WaitForRoutes(result_tx))
+ .map_err(|_| Error::RouteManagerDown)?;
+
+ timeout(WAIT_FOR_ROUTES_TIMEOUT, result_rx)
+ .await
+ .map_err(|_error| Error::PlatformError(imp::Error::RoutesTimedOut))?
+ .map_err(|_| Error::ManagerChannelDown)
+ }
+
/// Removes all routes previously applied in [`RouteManagerHandle::add_routes`].
+ #[cfg(not(target_os = "android"))]
pub fn clear_routes(&self) -> Result<(), Error> {
self.tx
.unbounded_send(RouteManagerCommand::ClearRoutes)
.map_err(|_| Error::RouteManagerDown)
}
+ /// (Android) This is a noop since we don't directly control the routes on Android.
+ #[cfg(target_os = "android")]
+ pub fn clear_routes(&self) -> Result<(), Error> {
+ Ok(())
+ }
+
/// Listen for non-tunnel default route changes.
#[cfg(target_os = "macos")]
pub async fn default_route_listener(
diff --git a/talpid-tunnel/src/tun_provider/android/mod.rs b/talpid-tunnel/src/tun_provider/android/mod.rs
index 3d356e50d3..f285b4a64c 100644
--- a/talpid-tunnel/src/tun_provider/android/mod.rs
+++ b/talpid-tunnel/src/tun_provider/android/mod.rs
@@ -46,6 +46,9 @@ pub enum Error {
#[error("Failed to create tunnel device")]
TunnelDeviceError,
+ #[error("Routes timed out")]
+ RoutesTimedOut,
+
#[error("Profile for VPN has not been setup")]
NotPrepared,
@@ -381,7 +384,7 @@ impl AsRawFd for VpnServiceTun {
enum CreateTunResult {
Success { tun_fd: i32 },
InvalidDnsServers { addresses: Vec<IpAddr> },
- TunnelDeviceError,
+ EstablishError,
OtherLegacyAlwaysOnVpn,
OtherAlwaysOnApp { app_name: String },
NotPrepared,
@@ -394,7 +397,7 @@ impl From<CreateTunResult> for Result<RawFd, Error> {
CreateTunResult::InvalidDnsServers { addresses } => {
Err(Error::InvalidDnsServers(addresses))
}
- CreateTunResult::TunnelDeviceError => Err(Error::TunnelDeviceError),
+ CreateTunResult::EstablishError => Err(Error::TunnelDeviceError),
CreateTunResult::OtherLegacyAlwaysOnVpn => Err(Error::OtherLegacyAlwaysOnVpn),
CreateTunResult::OtherAlwaysOnApp { app_name } => {
Err(Error::OtherAlwaysOnApp { app_name })
diff --git a/talpid-types/src/android/mod.rs b/talpid-types/src/android/mod.rs
index 4169216f3b..e4326fe81f 100644
--- a/talpid-types/src/android/mod.rs
+++ b/talpid-types/src/android/mod.rs
@@ -1,8 +1,61 @@
+use ipnetwork::{IpNetwork, IpNetworkError, Ipv4Network, Ipv6Network};
use jnix::jni::{objects::GlobalRef, JavaVM};
+use jnix::{FromJava, IntoJava};
+use std::net::IpAddr;
use std::sync::Arc;
+/// What Java calls an [IpAddr]
+pub type InetAddress = IpAddr;
+
#[derive(Clone)]
pub struct AndroidContext {
pub jvm: Arc<JavaVM>,
pub vpn_service: GlobalRef,
}
+
+/// A Java-compatible variant of [IpNetwork]
+#[derive(Clone, Debug, Eq, PartialEq, Hash, IntoJava, FromJava)]
+#[jnix(package = "net.mullvad.talpid.model")]
+pub struct InetNetwork {
+ pub address: IpAddr,
+ pub prefix_length: i16,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash, IntoJava, FromJava)]
+#[jnix(package = "net.mullvad.talpid.model")]
+pub struct RouteInfo {
+ pub destination: InetNetwork,
+ pub gateway: Option<InetAddress>,
+ pub interface_name: Option<String>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash, IntoJava, FromJava)]
+#[jnix(package = "net.mullvad.talpid.model")]
+pub struct NetworkState {
+ pub network_handle: i64,
+ pub routes: Option<Vec<RouteInfo>>,
+ pub dns_servers: Option<Vec<InetAddress>>,
+}
+
+impl From<IpNetwork> for InetNetwork {
+ fn from(ip_network: IpNetwork) -> Self {
+ InetNetwork {
+ address: ip_network.ip(),
+ prefix_length: ip_network.prefix() as i16,
+ }
+ }
+}
+
+impl TryFrom<InetNetwork> for IpNetwork {
+ type Error = IpNetworkError;
+ fn try_from(inet_network: InetNetwork) -> Result<Self, Self::Error> {
+ Ok(match inet_network.address {
+ IpAddr::V4(addr) => {
+ IpNetwork::V4(Ipv4Network::new(addr, inet_network.prefix_length as u8)?)
+ }
+ IpAddr::V6(addr) => {
+ IpNetwork::V6(Ipv6Network::new(addr, inet_network.prefix_length as u8)?)
+ }
+ })
+ }
+}
diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs
index af8e2da79e..fe1a848e9a 100644
--- a/talpid-wireguard/src/lib.rs
+++ b/talpid-wireguard/src/lib.rs
@@ -403,7 +403,6 @@ impl WireguardMonitor {
let desired_mtu = get_desired_mtu(params);
let mut config =
Config::from_parameters(params, desired_mtu).map_err(Error::WireguardConfigError)?;
-
let (close_obfs_sender, close_obfs_listener) = sync_mpsc::channel();
// Start obfuscation server and patch the WireGuard config to point the endpoint to it.
let obfuscator = args
@@ -466,6 +465,13 @@ impl WireguardMonitor {
.on_event(TunnelEvent::InterfaceUp(metadata.clone(), allowed_traffic))
.await;
+ // Wait for routes to come up
+ args.route_manager
+ .wait_for_routes()
+ .await
+ .map_err(Error::SetupRoutingError)
+ .map_err(CloseMsg::SetupError)?;
+
if should_negotiate_ephemeral_peer {
let ephemeral_obfs_sender = close_obfs_sender.clone();