Skip to content

Commit 1396839

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

File tree

8 files changed

+150
-58
lines changed

8 files changed

+150
-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

+58-7
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ import android.net.Network
66
import android.net.NetworkCapabilities
77
import android.net.NetworkRequest
88
import co.touchlab.kermit.Logger
9+
import java.net.DatagramSocket
10+
import java.net.Inet4Address
11+
import java.net.Inet6Address
912
import java.net.InetAddress
1013
import kotlin.collections.ArrayList
1114
import kotlin.time.Duration.Companion.seconds
1215
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
1317
import kotlinx.coroutines.channels.Channel
1418
import kotlinx.coroutines.flow.Flow
1519
import kotlinx.coroutines.flow.MutableStateFlow
1620
import kotlinx.coroutines.flow.SharingStarted
1721
import kotlinx.coroutines.flow.StateFlow
22+
import kotlinx.coroutines.flow.combine
1823
import kotlinx.coroutines.flow.distinctUntilChanged
1924
import kotlinx.coroutines.flow.filter
2025
import kotlinx.coroutines.flow.map
@@ -25,15 +30,21 @@ import kotlinx.coroutines.flow.receiveAsFlow
2530
import kotlinx.coroutines.flow.scan
2631
import kotlinx.coroutines.flow.stateIn
2732
import kotlinx.coroutines.launch
33+
import kotlinx.coroutines.plus
2834
import net.mullvad.mullvadvpn.lib.common.util.debounceFirst
35+
import net.mullvad.talpid.model.Connectivity
2936
import net.mullvad.talpid.model.NetworkState
37+
import net.mullvad.talpid.util.IPAvailabilityUtils
3038
import net.mullvad.talpid.util.NetworkEvent
3139
import net.mullvad.talpid.util.RawNetworkState
3240
import net.mullvad.talpid.util.defaultRawNetworkStateFlow
3341
import net.mullvad.talpid.util.networkEvents
3442

35-
class ConnectivityListener(private val connectivityManager: ConnectivityManager) {
36-
private lateinit var _isConnected: StateFlow<Boolean>
43+
class ConnectivityListener(
44+
val connectivityManager: ConnectivityManager,
45+
val protect: (socket: DatagramSocket) -> Boolean,
46+
) {
47+
private lateinit var _isConnected: StateFlow<Connectivity>
3748
// Used by JNI
3849
val isConnected
3950
get() = _isConnected.value
@@ -64,12 +75,49 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
6475
}
6576

6677
_isConnected =
67-
hasInternetConnectivity()
68-
.onEach { notifyConnectivityChange(it) }
78+
combine(connectivityManager.defaultRawNetworkStateFlow(), hasInternetConnectivity()) {
79+
rawNetworkState,
80+
hasInternetCapability: Boolean ->
81+
if (hasInternetCapability) {
82+
if (rawNetworkState.isNotVpn()) {
83+
// If the default network is not a VPN we can check the addresses
84+
// directly
85+
Connectivity.Status(
86+
ipv4 =
87+
rawNetworkState?.linkProperties?.routes?.any {
88+
it.destination.address is Inet4Address
89+
} == true,
90+
ipv6 =
91+
rawNetworkState?.linkProperties?.routes?.any {
92+
it.destination.address is Inet6Address
93+
} == true,
94+
)
95+
} else {
96+
// If the default network is a VPN we need to use a socket to check
97+
// the underlying network
98+
Connectivity.Status(
99+
IPAvailabilityUtils.isIPv4Available(protect = { protect(it) }),
100+
IPAvailabilityUtils.isIPv6Available(protect = { protect(it) }),
101+
)
102+
}
103+
// If we have internet, but both IPv4 and IPv6 are not available, we
104+
// assume something is wrong and instead will return presume online.
105+
.takeUnless { !it.ipv4 && !it.ipv6 } ?: Connectivity.PresumeOnline
106+
} else {
107+
Connectivity.Status(false, false)
108+
}
109+
}
110+
.distinctUntilChanged()
111+
.onEach {
112+
when (it) {
113+
Connectivity.PresumeOnline -> notifyConnectivityChange(true, true)
114+
is Connectivity.Status -> notifyConnectivityChange(it.ipv4, it.ipv6)
115+
}
116+
}
69117
.stateIn(
70-
scope,
118+
scope + Dispatchers.IO,
71119
SharingStarted.Eagerly,
72-
true, // Assume we have internet until we know otherwise
120+
Connectivity.PresumeOnline, // Assume we have internet until we know otherwise
73121
)
74122
}
75123

@@ -142,14 +190,17 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
142190
}
143191
.toSet()
144192

193+
private fun RawNetworkState?.isNotVpn(): Boolean =
194+
this?.networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) == true
195+
145196
private fun RawNetworkState.toNetworkState(): NetworkState =
146197
NetworkState(
147198
network.networkHandle,
148199
linkProperties?.routes,
149200
linkProperties?.dnsServersWithoutFallback(),
150201
)
151202

152-
private external fun notifyConnectivityChange(isConnected: Boolean)
203+
private external fun notifyConnectivityChange(isIPv4: Boolean, isIPv6: Boolean)
153204

154205
private external fun notifyDefaultNetworkChange(networkState: NetworkState?)
155206
}

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)