Skip to content

Commit a0efdda

Browse files
dlonPururun
andcommitted
Track IPv6 connectivity on Android
Co-authored-by: Jonatan Rhoidn <jonatan.rhodin@mullvad.net>
1 parent 5025db7 commit a0efdda

File tree

8 files changed

+151
-58
lines changed

8 files changed

+151
-58
lines changed

android/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Line wrap the file at 100 chars. Th
2929
### Removed
3030
- Remove Google's resolvers from encrypted DNS proxy.
3131

32+
### Fixed
33+
- Will no longer try to connect over IPv6 if IPv6 is not available.
34+
3235

3336
## [android/2024.10-beta2] - 2024-12-20
3437

android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt

+59-7
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,41 @@ import android.net.LinkProperties
55
import android.net.NetworkCapabilities
66
import android.net.NetworkRequest
77
import co.touchlab.kermit.Logger
8+
import java.net.DatagramSocket
9+
import java.net.Inet4Address
10+
import java.net.Inet6Address
811
import java.net.InetAddress
912
import kotlin.collections.ArrayList
1013
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Dispatchers
1115
import kotlinx.coroutines.channels.Channel
1216
import kotlinx.coroutines.flow.Flow
1317
import kotlinx.coroutines.flow.MutableStateFlow
1418
import kotlinx.coroutines.flow.SharingStarted
1519
import kotlinx.coroutines.flow.StateFlow
20+
import kotlinx.coroutines.flow.combine
21+
import kotlinx.coroutines.flow.distinctUntilChanged
1622
import kotlinx.coroutines.flow.map
1723
import kotlinx.coroutines.flow.merge
1824
import kotlinx.coroutines.flow.onEach
1925
import kotlinx.coroutines.flow.receiveAsFlow
2026
import kotlinx.coroutines.flow.scan
2127
import kotlinx.coroutines.flow.stateIn
2228
import kotlinx.coroutines.launch
29+
import kotlinx.coroutines.plus
30+
import net.mullvad.talpid.model.Connectivity
2331
import net.mullvad.talpid.model.NetworkState
32+
import net.mullvad.talpid.util.IPAvailabilityUtils
2433
import net.mullvad.talpid.util.NetworkEvent
2534
import net.mullvad.talpid.util.RawNetworkState
2635
import net.mullvad.talpid.util.defaultRawNetworkStateFlow
2736
import net.mullvad.talpid.util.networkEvents
2837

29-
class ConnectivityListener(private val connectivityManager: ConnectivityManager) {
30-
private lateinit var _isConnected: StateFlow<Boolean>
38+
class ConnectivityListener(
39+
val connectivityManager: ConnectivityManager,
40+
val protect: (socket: DatagramSocket) -> Boolean,
41+
) {
42+
private lateinit var _isConnected: StateFlow<Connectivity>
3143
// Used by JNI
3244
val isConnected
3345
get() = _isConnected.value
@@ -58,12 +70,49 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
5870
}
5971

6072
_isConnected =
61-
hasInternetCapability()
62-
.onEach { notifyConnectivityChange(it) }
73+
combine(connectivityManager.defaultRawNetworkStateFlow(), hasInternetCapability()) {
74+
rawNetworkState,
75+
hasInternetCapability: Boolean ->
76+
if (hasInternetCapability) {
77+
if (rawNetworkState.isUnderlyingNetwork()) {
78+
// If the default network is not a VPN we can check the addresses
79+
// directly
80+
Connectivity.Status(
81+
ipv4 =
82+
rawNetworkState?.linkProperties?.routes?.any {
83+
it.destination.address is Inet4Address
84+
} == true,
85+
ipv6 =
86+
rawNetworkState?.linkProperties?.routes?.any {
87+
it.destination.address is Inet6Address
88+
} == true,
89+
)
90+
} else {
91+
// If the default network is a VPN we need to use a socket to check
92+
// the underlying network
93+
Connectivity.Status(
94+
IPAvailabilityUtils.isIPv4Available(protect = { protect(it) }),
95+
IPAvailabilityUtils.isIPv6Available(protect = { protect(it) }),
96+
)
97+
}
98+
// If we have internet, but both IPv4 and IPv6 are not available, we
99+
// assume something is wrong and instead will return presume online.
100+
.takeUnless { !it.ipv4 && !it.ipv6 } ?: Connectivity.PresumeOnline
101+
} else {
102+
Connectivity.Status(false, false)
103+
}
104+
}
105+
.distinctUntilChanged()
106+
.onEach {
107+
when (it) {
108+
Connectivity.PresumeOnline -> notifyConnectivityChange(true, true)
109+
is Connectivity.Status -> notifyConnectivityChange(it.ipv4, it.ipv6)
110+
}
111+
}
63112
.stateIn(
64-
scope,
113+
scope + Dispatchers.IO,
65114
SharingStarted.Eagerly,
66-
true, // Assume we have internet until we know otherwise
115+
Connectivity.PresumeOnline, // Assume we have internet until we know otherwise
67116
)
68117
}
69118

@@ -112,14 +161,17 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
112161
private fun NetworkCapabilities?.hasInternetCapability(): Boolean =
113162
this?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
114163

164+
private fun RawNetworkState?.isUnderlyingNetwork(): Boolean =
165+
this?.networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) == true
166+
115167
private fun RawNetworkState.toNetworkState(): NetworkState =
116168
NetworkState(
117169
network.networkHandle,
118170
linkProperties?.routes,
119171
linkProperties?.dnsServersWithoutFallback(),
120172
)
121173

122-
private external fun notifyConnectivityChange(isConnected: Boolean)
174+
private external fun notifyConnectivityChange(isIPv4: Boolean, isIPv6: Boolean)
123175

124176
private external fun notifyDefaultNetworkChange(networkState: NetworkState?)
125177
}

android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ open class TalpidVpnService : LifecycleVpnService() {
4848
@CallSuper
4949
override fun onCreate() {
5050
super.onCreate()
51-
connectivityListener = ConnectivityListener(getSystemService<ConnectivityManager>()!!)
51+
connectivityListener =
52+
ConnectivityListener(getSystemService<ConnectivityManager>()!!, ::protect)
5253
connectivityListener.register(lifecycleScope)
5354
}
5455

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.mullvad.talpid.model
2+
3+
sealed class Connectivity {
4+
data class Status(val ipv4: Boolean, val ipv6: Boolean) : Connectivity()
5+
6+
data object PresumeOnline : Connectivity()
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package net.mullvad.talpid.util
2+
3+
import co.touchlab.kermit.Logger
4+
import java.net.DatagramSocket
5+
import java.net.InetAddress
6+
import java.net.InetSocketAddress
7+
import java.net.SocketException
8+
9+
object IPAvailabilityUtils {
10+
fun isIPv4Available(protect: (socket: DatagramSocket) -> Boolean): Boolean =
11+
isIPAvailable(InetAddress.getByName(PUBLIC_IPV4_ADDRESS), protect)
12+
13+
fun isIPv6Available(protect: (socket: DatagramSocket) -> Boolean): Boolean =
14+
isIPAvailable(InetAddress.getByName(PUBLIC_IPV6_ADDRESS), protect)
15+
16+
// Fake a connection to a public ip address using a UDP socket.
17+
// We don't care about the result of the connection, only that it is possible to create.
18+
// This is done this way since otherwise there is not way to check the availability of an ip
19+
// version on the underlying network if the VPN is turned on.
20+
// Since we are protecting the socket it will use the underlying network regardless
21+
// if the VPN is turned on or not.
22+
// If the ip version is not supported on the underlying network it will trigger a socket
23+
// exception. Otherwise we assume it is available.
24+
private inline fun <reified T : InetAddress> isIPAvailable(
25+
ip: T,
26+
protect: (socket: DatagramSocket) -> Boolean,
27+
): Boolean {
28+
val socket = DatagramSocket()
29+
if (!protect(socket)) {
30+
Logger.e("Unable to protect the socket VPN is not set up correctly")
31+
return false
32+
}
33+
return try {
34+
socket.connect(InetSocketAddress(ip, 1))
35+
true
36+
} catch (_: SocketException) {
37+
Logger.e("Socket could not be set up")
38+
false
39+
} finally {
40+
socket.close()
41+
}
42+
}
43+
44+
private const val PUBLIC_IPV4_ADDRESS = "1.1.1.1"
45+
private const val PUBLIC_IPV6_ADDRESS = "2606:4700:4700::1001"
46+
}

mullvad-jni/src/classes.rs

+2
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ pub const CLASSES: &[&str] = &[
1818
"net/mullvad/talpid/ConnectivityListener",
1919
"net/mullvad/talpid/TalpidVpnService",
2020
"net/mullvad/mullvadvpn/lib/endpoint/ApiEndpointOverride",
21+
"net/mullvad/talpid/model/Connectivity$Status",
22+
"net/mullvad/talpid/model/Connectivity$PresumeOnline",
2123
];

talpid-core/src/connectivity_listener.rs

+29-26
Original file line numberDiff line numberDiff line change
@@ -98,36 +98,34 @@ impl ConnectivityListener {
9898

9999
/// Return the current offline/connectivity state
100100
pub fn connectivity(&self) -> Connectivity {
101-
self.get_is_connected()
102-
.map(|connected| Connectivity::Status { connected })
103-
.unwrap_or_else(|error| {
104-
log::error!(
105-
"{}",
106-
error.display_chain_with_msg("Failed to check connectivity status")
107-
);
108-
Connectivity::PresumeOnline
109-
})
101+
self.get_is_connected().unwrap_or_else(|error| {
102+
log::error!(
103+
"{}",
104+
error.display_chain_with_msg("Failed to check connectivity status")
105+
);
106+
Connectivity::PresumeOnline
107+
})
110108
}
111109

112-
fn get_is_connected(&self) -> Result<bool, Error> {
110+
fn get_is_connected(&self) -> Result<Connectivity, Error> {
113111
let env = JnixEnv::from(
114112
self.jvm
115113
.attach_current_thread_as_daemon()
116114
.map_err(Error::AttachJvmToThread)?,
117115
);
118116

119-
let is_connected =
120-
env.call_method(self.android_listener.as_obj(), "isConnected", "()Z", &[]);
121-
122-
match is_connected {
123-
Ok(JValue::Bool(JNI_TRUE)) => Ok(true),
124-
Ok(JValue::Bool(_)) => Ok(false),
125-
value => Err(Error::InvalidMethodResult(
126-
"ConnectivityListener",
117+
let is_connected = env
118+
.call_method(
119+
self.android_listener.as_obj(),
127120
"isConnected",
128-
format!("{:?}", value),
129-
)),
130-
}
121+
"()Lnet/mullvad/talpid/model/Connectivity;",
122+
&[],
123+
)
124+
.expect("Missing isConnected")
125+
.l()
126+
.expect("isConnected is not an object");
127+
128+
Ok(Connectivity::from_java(&env, is_connected))
131129
}
132130

133131
/// Return the current DNS servers according to Android
@@ -160,20 +158,25 @@ impl ConnectivityListener {
160158
#[unsafe(no_mangle)]
161159
#[allow(non_snake_case)]
162160
pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyConnectivityChange(
163-
_: JNIEnv<'_>,
164-
_: JObject<'_>,
165-
connected: jboolean,
161+
_env: JNIEnv<'_>,
162+
_obj: JObject<'_>,
163+
is_ipv4: jboolean,
164+
is_ipv6: jboolean,
166165
) {
167166
let Some(tx) = &*CONNECTIVITY_TX.lock().unwrap() else {
168167
// No sender has been registered
169168
log::trace!("Received connectivity notification wíth no channel");
170169
return;
171170
};
172171

173-
let connected = JNI_TRUE == connected;
172+
let is_ipv4 = JNI_TRUE == is_ipv4;
173+
let is_ipv6 = JNI_TRUE == is_ipv6;
174174

175175
if tx
176-
.unbounded_send(Connectivity::Status { connected })
176+
.unbounded_send(Connectivity::Status {
177+
ipv4: is_ipv4,
178+
ipv6: is_ipv6,
179+
})
177180
.is_err()
178181
{
179182
log::warn!("Failed to send offline change event");

talpid-types/src/net/mod.rs

+3-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
2+
use jnix::FromJava;
23
use obfuscation::ObfuscatorConfig;
34
use serde::{Deserialize, Serialize};
45
#[cfg(windows)]
@@ -564,20 +565,15 @@ pub fn all_of_the_internet() -> Vec<ipnetwork::IpNetwork> {
564565
///
565566
/// Information about the host's connectivity, such as the preesence of
566567
/// configured IPv4 and/or IPv6.
567-
#[derive(Debug, Clone, Copy, PartialEq)]
568+
#[derive(Debug, Clone, Copy, PartialEq, FromJava)]
569+
#[jnix(package = "net.mullvad.talpid.model")]
568570
pub enum Connectivity {
569-
#[cfg(not(target_os = "android"))]
570571
Status {
571572
/// Whether IPv4 connectivity seems to be available on the host.
572573
ipv4: bool,
573574
/// Whether IPv6 connectivity seems to be available on the host.
574575
ipv6: bool,
575576
},
576-
#[cfg(target_os = "android")]
577-
Status {
578-
/// Whether _any_ connectivity seems to be available on the host.
579-
connected: bool,
580-
},
581577
/// On/offline status could not be verified, but we have no particular
582578
/// reason to believe that the host is offline.
583579
PresumeOnline,
@@ -591,7 +587,6 @@ impl Connectivity {
591587

592588
/// If no IP4 nor IPv6 routes exist, we have no way of reaching the internet
593589
/// so we consider ourselves offline.
594-
#[cfg(not(target_os = "android"))]
595590
pub fn is_offline(&self) -> bool {
596591
matches!(
597592
self,
@@ -605,23 +600,7 @@ impl Connectivity {
605600
/// Whether IPv6 connectivity seems to be available on the host.
606601
///
607602
/// If IPv6 status is unknown, `false` is returned.
608-
#[cfg(not(target_os = "android"))]
609603
pub fn has_ipv6(&self) -> bool {
610604
matches!(self, Connectivity::Status { ipv6: true, .. })
611605
}
612-
613-
/// Whether IPv6 connectivity seems to be available on the host.
614-
///
615-
/// If IPv6 status is unknown, `false` is returned.
616-
#[cfg(target_os = "android")]
617-
pub fn has_ipv6(&self) -> bool {
618-
self.is_online()
619-
}
620-
621-
/// If the host does not have configured IPv6 routes, we have no way of
622-
/// reaching the internet so we consider ourselves offline.
623-
#[cfg(target_os = "android")]
624-
pub fn is_offline(&self) -> bool {
625-
matches!(self, Connectivity::Status { connected: false })
626-
}
627606
}

0 commit comments

Comments
 (0)