From 730feb1c85c5241d0abf826e7dcddd0b24e554ce Mon Sep 17 00:00:00 2001 From: Markus Pettersson Date: Mon, 30 Sep 2024 13:40:13 +0200 Subject: [PATCH] Make the daemon aware if ipv4 and/or ipv6 is available --- .../mullvad/talpid/ConnectivityListener.kt | 51 ++++++++---- .../net/mullvad/talpid/model/NetworkInfo.kt | 5 ++ mullvad-daemon/src/lib.rs | 13 +++ mullvad-daemon/src/tunnel.rs | 6 +- .../src/relay_selector/mod.rs | 43 +++++++++- .../tests/relay_selector.rs | 8 +- talpid-core/src/offline/android.rs | 79 ++++++++++++------- talpid-core/src/offline/mod.rs | 4 +- .../tunnel_state_machine/connecting_state.rs | 12 +-- talpid-core/src/tunnel_state_machine/mod.rs | 1 + talpid-types/src/net/mod.rs | 32 ++------ 11 files changed, 170 insertions(+), 84 deletions(-) create mode 100644 android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkInfo.kt 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 edeec9a6fe9d..f8d85dec975e 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 @@ -6,38 +6,55 @@ import android.net.ConnectivityManager.NetworkCallback import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import kotlin.properties.Delegates.observable +import co.touchlab.kermit.Logger +import java.net.Inet4Address +import java.net.Inet6Address +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import net.mullvad.talpid.model.NetworkInfo class ConnectivityListener { - private val availableNetworks = HashSet() + private val availableNetworks = MutableStateFlow(emptySet()) private val callback = object : NetworkCallback() { override fun onAvailable(network: Network) { - availableNetworks.add(network) - isConnected = true + availableNetworks.update { it + network } + val info = availableNetworks.value.info() + notifyConnectivityChange(info.hasIpV4, info.hasIpV6, senderAddress) } override fun onLost(network: Network) { - availableNetworks.remove(network) - isConnected = availableNetworks.isNotEmpty() + availableNetworks.update { it - network } + val info = availableNetworks.value.info() + notifyConnectivityChange(info.hasIpV4, info.hasIpV6, senderAddress) } } private lateinit var connectivityManager: ConnectivityManager - // Used by JNI - var isConnected by - observable(false) { _, oldValue, newValue -> - if (newValue != oldValue) { - if (senderAddress != 0L) { - notifyConnectivityChange(newValue, senderAddress) - } - } - } - var senderAddress = 0L + // Used by jni + @Suppress("unused") fun isConnected(): Boolean = availableNetworks.value.info().isConnected + + fun Set.info(): NetworkInfo { + return this.map { network -> + val addresses = + connectivityManager.getLinkProperties(network)?.linkAddresses ?: emptyList() + NetworkInfo( + hasIpV4 = addresses.any { it.address is Inet4Address }, + hasIpV6 = addresses.any { it.address is Inet6Address }, + ) + } + .reduceOrNull { acc, networkInfo -> + NetworkInfo( + hasIpV4 = acc.hasIpV4 || networkInfo.hasIpV4, + hasIpV6 = acc.hasIpV6 || networkInfo.hasIpV6, + ) + } ?: NetworkInfo(hasIpV4 = false, hasIpV6 = false) + } + fun register(context: Context) { val request = NetworkRequest.Builder() @@ -64,7 +81,7 @@ class ConnectivityListener { senderAddress = 0L } - private external fun notifyConnectivityChange(isConnected: Boolean, senderAddress: Long) + private external fun notifyConnectivityChange(ipv4: Boolean, ipv6: Boolean, senderAddress: Long) private external fun destroySender(senderAddress: Long) } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkInfo.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkInfo.kt new file mode 100644 index 000000000000..76e1528a47a1 --- /dev/null +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/NetworkInfo.kt @@ -0,0 +1,5 @@ +package net.mullvad.talpid.model + +data class NetworkInfo(val hasIpV4: Boolean, val hasIpV6: Boolean) { + val isConnected = hasIpV4 || hasIpV6 +} diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 08c523ffd760..efd9ce4f70d2 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -380,6 +380,13 @@ pub enum DaemonCommand { ExportJsonSettings(ResponseTx), /// Request the current feature indicators. GetFeatureIndicators(oneshot::Sender), + /// Update the IPv4 and IPv6 connectivity status. + #[cfg(target_os = "android")] + UpdateNetworkAvailability( + ResponseTx<(), Error>, + bool, // IPv4 + bool, // IPv6 + ), } /// All events that can happen in the daemon. Sent from various threads and exposed interfaces. @@ -1336,6 +1343,12 @@ impl Daemon { ApplyJsonSettings(tx, blob) => self.on_apply_json_settings(tx, blob).await, ExportJsonSettings(tx) => self.on_export_json_settings(tx), GetFeatureIndicators(tx) => self.on_get_feature_indicators(tx), + #[cfg(target_os = "android")] + UpdateNetworkAvailability( + _tx, + _ipv4, // IPv4 + _ipv6, // IPv6 + ) => {} } } diff --git a/mullvad-daemon/src/tunnel.rs b/mullvad-daemon/src/tunnel.rs index a9c59662853f..2eddeb42bd0c 100644 --- a/mullvad-daemon/src/tunnel.rs +++ b/mullvad-daemon/src/tunnel.rs @@ -160,12 +160,13 @@ impl InnerParametersGenerator { async fn generate( &mut self, retry_attempt: u32, + ipv4: bool, ipv6: bool, ) -> Result { let data = self.device().await?; let selected_relay = self .relay_selector - .get_relay(retry_attempt as usize, RuntimeParameters { ipv6 })?; + .get_relay(retry_attempt as usize, RuntimeParameters { ipv4, ipv6 })?; match selected_relay { #[cfg(not(target_os = "android"))] @@ -299,13 +300,14 @@ impl TunnelParametersGenerator for ParametersGenerator { fn generate( &mut self, retry_attempt: u32, + ipv4: bool, ipv6: bool, ) -> Pin>>> { let generator = self.0.clone(); Box::pin(async move { let mut inner = generator.lock().await; inner - .generate(retry_attempt, ipv6) + .generate(retry_attempt, ipv4, ipv6) .await .inspect_err(|error| { log::error!( diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index e7a69227272e..02d242d42ea4 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -140,6 +140,8 @@ pub struct AdditionalWireguardConstraints { /// Values which affect the choice of relay but are only known at runtime. #[derive(Clone, Debug)] pub struct RuntimeParameters { + /// Whether IPv4 is available + pub ipv4: bool, /// Whether IPv6 is available pub ipv6: bool, } @@ -147,6 +149,19 @@ pub struct RuntimeParameters { impl RuntimeParameters { /// Return whether a given [query][`RelayQuery`] is valid given the current runtime parameters pub fn compatible(&self, query: &RelayQuery) -> bool { + if !self.ipv4 { + let must_use_ipv4 = matches!( + query.wireguard_constraints().ip_version, + Constraint::Only(talpid_types::net::IpVersion::V4) + ); + if must_use_ipv4 { + log::trace!( + "{query:?} is incompatible with {self:?} due to IPv4 not being available" + ); + return false; + } + } + if !self.ipv6 { let must_use_ipv6 = matches!( query.wireguard_constraints().ip_version, @@ -168,7 +183,10 @@ impl RuntimeParameters { #[allow(clippy::derivable_impls)] impl Default for RuntimeParameters { fn default() -> Self { - RuntimeParameters { ipv6: false } + RuntimeParameters { + ipv4: false, + ipv6: false, + } } } @@ -528,13 +546,32 @@ impl RelaySelector { SpecializedSelectorConfig::Normal(normal_config) => { let parsed_relays = &self.parsed_relays.lock().unwrap(); // Merge user preferences with the relay selector's default preferences. - let query = Self::pick_and_merge_query( + let mut query = Self::pick_and_merge_query( retry_attempt, retry_order, - runtime_params, + runtime_params.clone(), &normal_config, parsed_relays, )?; + // TODO: Remove + // Dirty hack to set IPv4 or IPv6 if one or the other is available. + dbg!(&query); + #[cfg(target_os = "android")] + { + let mut wireguard_constraints = query.wireguard_constraints().clone(); + if wireguard_constraints.ip_version.is_any() { + if runtime_params.ipv4 && !runtime_params.ipv6 { + wireguard_constraints.ip_version = + Constraint::Only(talpid_types::net::IpVersion::V4) + } + if runtime_params.ipv6 && !runtime_params.ipv4 { + wireguard_constraints.ip_version = + Constraint::Only(talpid_types::net::IpVersion::V6) + } + } + query.set_wireguard_constraints(wireguard_constraints); + } + dbg!(&query); Self::get_relay_inner(&query, parsed_relays, normal_config.custom_lists) } } diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 6908d468de11..3719c7188c0f 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -370,7 +370,13 @@ fn test_retry_order() { let relay_selector = default_relay_selector(); for (retry_attempt, query) in RETRY_ORDER.iter().enumerate() { let relay = relay_selector - .get_relay(retry_attempt, RuntimeParameters { ipv6: true }) + .get_relay( + retry_attempt, + RuntimeParameters { + ipv4: true, + ipv6: true, + }, + ) .unwrap_or_else(|_| panic!("Retry attempt {retry_attempt} did not yield any relay")); // For each relay, cross-check that the it has the expected tunnel protocol let tunnel_type = tunnel_type(&unwrap_relay(relay.clone())); diff --git a/talpid-core/src/offline/android.rs b/talpid-core/src/offline/android.rs index 7dc8389ed39b..d3beeec57bd4 100644 --- a/talpid-core/src/offline/android.rs +++ b/talpid-core/src/offline/android.rs @@ -94,33 +94,37 @@ impl MonitorHandle { #[allow(clippy::unused_async)] pub async fn connectivity(&self) -> Connectivity { self.get_is_connected() - .map(|connected| Connectivity::Status { connected }) - .unwrap_or_else(|error| { + .map(|connected| Connectivity::Status { + ipv4: connected, + ipv6: connected, + }) + .inspect_err(|error| { log::error!( "{}", error.display_chain_with_msg("Failed to check connectivity status") ); - Connectivity::PresumeOnline }) + .unwrap_or(Connectivity::PresumeOnline) } fn get_is_connected(&self) -> Result { - let is_connected = self.call_method( - "isConnected", - "()Z", - &[], - JavaType::Primitive(Primitive::Boolean), - )?; - - match is_connected { - JValue::Bool(JNI_TRUE) => Ok(true), - JValue::Bool(_) => Ok(false), - value => Err(Error::InvalidMethodResult( - "ConnectivityListener", - "isConnected", - format!("{:?}", value), - )), - } + Ok(true) + // let is_connected = self.call_method( + // "isConnected", + // "()Z", + // &[], + // JavaType::Primitive(Primitive::Boolean), + // )?; + + // match is_connected { + // JValue::Bool(JNI_TRUE) => Ok(true), + // JValue::Bool(_) => Ok(false), + // value => Err(Error::InvalidMethodResult( + // "ConnectivityListener", + // "isConnected", + // format!("{:?}", value), + // )), + // } } fn set_sender(&self, sender: Weak>) -> Result<(), Error> { @@ -172,17 +176,26 @@ impl MonitorHandle { pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyConnectivityChange( _: JNIEnv<'_>, _: JObject<'_>, - connected: jboolean, + ipv4: jboolean, + ipv6: jboolean, sender_address: jlong, ) { - let connected = JNI_TRUE == connected; - let sender_ref = Box::leak(unsafe { get_sender_from_address(sender_address) }); - if let Some(sender) = sender_ref.upgrade() { - if sender - .unbounded_send(Connectivity::Status { connected }) - .is_err() - { - log::warn!("Failed to send offline change event"); + let ipv4 = JNI_TRUE == ipv4; + let ipv6 = JNI_TRUE == ipv6; + let sender_ptr = unsafe { get_sender_from_address(sender_address) }.map(Box::leak); + match sender_ptr { + Some(sender_ref) => { + if let Some(sender) = sender_ref.upgrade() { + if sender + .unbounded_send(Connectivity::Status { ipv4, ipv6 }) + .is_err() + { + log::warn!("Failed to send offline change event"); + } + } + } + None => { + log::error!("sender was null pointer"); } } } @@ -198,8 +211,14 @@ pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_destroySende let _ = unsafe { get_sender_from_address(sender_address) }; } -unsafe fn get_sender_from_address(address: jlong) -> Box>> { - Box::from_raw(address as *mut Weak>) +unsafe fn get_sender_from_address( + address: jlong, +) -> Option>>> { + let raw = address as *mut Weak>; + if raw.is_null() { + return None; + } + Some(Box::from_raw(raw)) } #[allow(clippy::unused_async)] diff --git a/talpid-core/src/offline/mod.rs b/talpid-core/src/offline/mod.rs index 0e1d55c27375..f0b74aa37abc 100644 --- a/talpid-core/src/offline/mod.rs +++ b/talpid-core/src/offline/mod.rs @@ -41,7 +41,7 @@ impl MonitorHandle { } pub async fn spawn_monitor( - sender: UnboundedSender, + offline_tx: UnboundedSender, #[cfg(not(target_os = "android"))] route_manager: RouteManagerHandle, #[cfg(target_os = "linux")] fwmark: Option, #[cfg(target_os = "android")] android_context: AndroidContext, @@ -50,7 +50,7 @@ pub async fn spawn_monitor( None } else { imp::spawn_monitor( - sender, + offline_tx, #[cfg(not(target_os = "android"))] route_manager, #[cfg(target_os = "linux")] diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index bc3840114778..c694d0a057bc 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -77,11 +77,13 @@ impl ConnectingState { } return ErrorState::enter(shared_values, ErrorStateCause::IsOffline); } - match shared_values.runtime.block_on( - shared_values - .tunnel_parameters_generator - .generate(retry_attempt, shared_values.connectivity.has_ipv6()), - ) { + match shared_values + .runtime + .block_on(shared_values.tunnel_parameters_generator.generate( + retry_attempt, + shared_values.connectivity.has_ipv4(), + shared_values.connectivity.has_ipv6(), + )) { Err(err) => { ErrorState::enter(shared_values, ErrorStateCause::TunnelParameterError(err)) } diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index e0ef07850d6d..d046cf122f58 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -440,6 +440,7 @@ pub trait TunnelParametersGenerator: Send + 'static { fn generate( &mut self, retry_attempt: u32, + ipv4: bool, ipv6: bool, ) -> Pin>>>; } diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index 1ec8ba46c574..709854d76b41 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -572,18 +572,12 @@ pub fn all_of_the_internet() -> Vec { /// configured IPv4 and/or IPv6. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Connectivity { - #[cfg(not(target_os = "android"))] Status { /// Whether IPv4 connectivity seems to be available on the host. ipv4: bool, /// Whether IPv6 connectivity seems to be available on the host. ipv6: bool, }, - #[cfg(target_os = "android")] - Status { - /// Whether _any_ connectivity seems to be available on the host. - connected: bool, - }, /// On/offline status could not be verified, but we have no particular /// reason to believe that the host is offline. PresumeOnline, @@ -591,14 +585,13 @@ pub enum Connectivity { impl Connectivity { /// Inverse of [`Connectivity::is_offline`]. - pub fn is_online(&self) -> bool { + pub const fn is_online(&self) -> bool { !self.is_offline() } /// If no IP4 nor IPv6 routes exist, we have no way of reaching the internet /// so we consider ourselves offline. - #[cfg(not(target_os = "android"))] - pub fn is_offline(&self) -> bool { + pub const fn is_offline(&self) -> bool { matches!( self, Connectivity::Status { @@ -608,26 +601,17 @@ impl Connectivity { ) } - /// Whether IPv6 connectivity seems to be available on the host. + /// Whether IPv4 connectivity seems to be available on the host. /// - /// If IPv6 status is unknown, `false` is returned. - #[cfg(not(target_os = "android"))] - pub fn has_ipv6(&self) -> bool { - matches!(self, Connectivity::Status { ipv6: true, .. }) + /// If IPv4 status is unknown, `false` is returned. + pub const fn has_ipv4(&self) -> bool { + matches!(self, Connectivity::Status { ipv4: true, .. }) } /// Whether IPv6 connectivity seems to be available on the host. /// /// If IPv6 status is unknown, `false` is returned. - #[cfg(target_os = "android")] - pub fn has_ipv6(&self) -> bool { - self.is_online() - } - - /// If the host does not have configured IPv6 routes, we have no way of - /// reaching the internet so we consider ourselves offline. - #[cfg(target_os = "android")] - pub fn is_offline(&self) -> bool { - matches!(self, Connectivity::Status { connected: false }) + pub const fn has_ipv6(&self) -> bool { + matches!(self, Connectivity::Status { ipv6: true, .. }) } }