diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-03-06 09:10:00 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-03-25 17:17:00 +0100 |
| commit | db66c85daab1ba708b282b5243d7835beb615b3b (patch) | |
| tree | fe1b75670efe95c0bcb5f17491161249b8acda21 | |
| parent | b582c60162fac40a831d0ef56089c94e491e959b (diff) | |
| download | mullvadvpn-db66c85daab1ba708b282b5243d7835beb615b3b.tar.xz mullvadvpn-db66c85daab1ba708b282b5243d7835beb615b3b.zip | |
Avoid using an unavailable ip version to connect to a relay
Co-authored-by: Sebastian Holmin <sebastian.holmin@mullvad.net>
21 files changed, 257 insertions, 157 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b21dda16..5eaa8d53ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th ### Fixed - Fix `mullvad-cli` panicking if it tried to write to a closed pipe on Linux and macOS. - Fix bug where new users are not forwarded to the main view after payment. +- Will no longer try to connect over IPv4 if IPv4 is not available. #### Windows - Fix error setting up tunnel when MTU was incorrectly set to a value below 1280 for IPv6. diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 2bbb8e5f91..9d7c4963a8 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -37,7 +37,7 @@ Line wrap the file at 100 chars. Th - Remove Google's resolvers from encrypted DNS proxy. ### Fixed -- Will no longer try to connect over IPv6 if IPv6 is not available. +- Will no longer try to connect using an IP version if that IP version is not available. ## [android/2025.1-beta1] - 2025-03-05 diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/talpid/util/ConnectivityManagerUtilKtTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/talpid/util/ConnectivityManagerUtilKtTest.kt index 354b6f585d..39df5de05b 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/talpid/util/ConnectivityManagerUtilKtTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/talpid/util/ConnectivityManagerUtilKtTest.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.test.runTest import net.mullvad.talpid.model.Connectivity +import net.mullvad.talpid.model.IpAvailability import net.mullvad.talpid.util.NetworkEvent import net.mullvad.talpid.util.UnderlyingConnectivityStatusResolver import net.mullvad.talpid.util.defaultNetworkEvents @@ -53,7 +54,7 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // Since initial state and listener both return `true` within debounce we only see one // event - assertEquals(Connectivity.Status(true, true), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4AndIpv6), awaitItem()) expectNoEvents() } } @@ -66,7 +67,7 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // Initially offline and no network events, so we should get a single `false` event - assertEquals(Connectivity.Status(false, false), awaitItem()) + assertEquals(Connectivity.Offline, awaitItem()) expectNoEvents() } } @@ -88,8 +89,8 @@ class ConnectivityManagerUtilKtTest { } connectivityManager.hasInternetConnectivity(mockResolver).test { - assertEquals(Connectivity.Status(false, false), awaitItem()) - assertEquals(Connectivity.Status(true, true), awaitItem()) + assertEquals(Connectivity.Offline, awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4AndIpv6), awaitItem()) expectNoEvents() } } @@ -113,8 +114,8 @@ class ConnectivityManagerUtilKtTest { } connectivityManager.hasInternetConnectivity(mockResolver).test { - assertEquals(Connectivity.Status(true, true), awaitItem()) - assertEquals(Connectivity.Status(false, false), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4AndIpv6), awaitItem()) + assertEquals(Connectivity.Offline, awaitItem()) expectNoEvents() } } @@ -150,7 +151,7 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // We should always only see us being online - assertEquals(Connectivity.Status(ipv4 = true, ipv6 = false), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4), awaitItem()) expectNoEvents() } } @@ -184,7 +185,7 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // We should always only see us being online, small offline state is caught by debounce - assertEquals(Connectivity.Status(ipv4 = true, ipv6 = false), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4), awaitItem()) expectNoEvents() } } @@ -218,11 +219,11 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // Wifi is online - assertEquals(Connectivity.Status(false, true), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv6), awaitItem()) // We didn't get any network within debounce time, so we are offline - assertEquals(Connectivity.Status(false, false), awaitItem()) + assertEquals(Connectivity.Offline, awaitItem()) // Cellular network is online - assertEquals(Connectivity.Status(false, true), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv6), awaitItem()) expectNoEvents() } } @@ -250,9 +251,9 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // Ipv6 network is online - assertEquals(Connectivity.Status(false, true), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv6), awaitItem()) // Ipv4 network is online - assertEquals(Connectivity.Status(true, false), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4), awaitItem()) expectNoEvents() } } @@ -265,7 +266,8 @@ class ConnectivityManagerUtilKtTest { every { capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) } returns false val mockResolver = mockk<UnderlyingConnectivityStatusResolver>() - every { mockResolver.currentStatus() } returns Connectivity.Status(true, true) + every { mockResolver.currentStatus() } returns + Connectivity.Online(IpAvailability.Ipv4AndIpv6) every { connectivityManager.defaultNetworkEvents() } returns callbackFlow { @@ -276,7 +278,7 @@ class ConnectivityManagerUtilKtTest { connectivityManager.hasInternetConnectivity(mockResolver).test { // Network is online - assertEquals(Connectivity.Status(true, true), awaitItem()) + assertEquals(Connectivity.Online(IpAvailability.Ipv4AndIpv6), awaitItem()) } verify(exactly = 1) { mockResolver.currentStatus() } 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 ede883a837..460dac8f68 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 @@ -66,7 +66,7 @@ class ConnectivityListener( _isConnected = connectivityManager .hasInternetConnectivity(resolver) - .onEach { notifyConnectivityChange(it.ipv4, it.ipv6) } + .onEach { notifyConnectivityChange(it) } .stateIn( scope + Dispatchers.IO, SharingStarted.Eagerly, @@ -99,7 +99,7 @@ class ConnectivityListener( linkProperties?.dnsServersWithoutFallback(), ) - private external fun notifyConnectivityChange(isIPv4: Boolean, isIPv6: Boolean) + private external fun notifyConnectivityChange(connectivity: Connectivity) private external fun notifyDefaultNetworkChange(networkState: NetworkState?) } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/Connectivity.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/Connectivity.kt index b87eaaacc8..46195636bc 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/Connectivity.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/Connectivity.kt @@ -1,8 +1,30 @@ package net.mullvad.talpid.model +import android.net.LinkAddress +import java.net.Inet4Address +import java.net.Inet6Address + sealed class Connectivity { - data class Status(val ipv4: Boolean, val ipv6: Boolean) : Connectivity() + data class Online(val ipAvailability: IpAvailability) : Connectivity() + + data object Offline : Connectivity() // Required by jni data object PresumeOnline : Connectivity() + + companion object { + fun fromIpAvailability(ipv4: Boolean, ipv6: Boolean) = + when { + ipv4 && ipv6 -> Online(IpAvailability.Ipv4AndIpv6) + ipv4 -> Online(IpAvailability.Ipv4) + ipv6 -> Online(IpAvailability.Ipv6) + else -> Offline + } + + fun fromLinkAddresses(linkAddresses: List<LinkAddress>): Connectivity { + val ipv4 = linkAddresses.any { it.address is Inet4Address } + val ipv6 = linkAddresses.any { it.address is Inet6Address } + return fromIpAvailability(ipv4, ipv6) + } + } } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/IpAvailability.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/IpAvailability.kt new file mode 100644 index 0000000000..916f9d0a88 --- /dev/null +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/IpAvailability.kt @@ -0,0 +1,7 @@ +package net.mullvad.talpid.model + +enum class IpAvailability { + Ipv4, + Ipv6, + Ipv4AndIpv6, +} 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 7a0208eaa1..5409bc2f99 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 @@ -6,8 +6,6 @@ import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import co.touchlab.kermit.Logger -import java.net.Inet4Address -import java.net.Inet6Address import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose @@ -137,7 +135,7 @@ internal fun ConnectivityManager.activeRawNetworkState(): RawNetworkState? = @OptIn(FlowPreview::class) fun ConnectivityManager.hasInternetConnectivity( resolver: UnderlyingConnectivityStatusResolver -): Flow<Connectivity.Status> = +): Flow<Connectivity> = this.defaultRawNetworkStateFlow() .debounce(CONNECTIVITY_DEBOUNCE) .map { resolveConnectivityStatus(it, resolver) } @@ -146,7 +144,7 @@ fun ConnectivityManager.hasInternetConnectivity( internal fun resolveConnectivityStatus( currentRawNetworkState: RawNetworkState?, resolver: UnderlyingConnectivityStatusResolver, -): Connectivity.Status = +): Connectivity = if (currentRawNetworkState.isVpn()) { // If the default network is a VPN we need to use a socket to check // the underlying network @@ -158,10 +156,7 @@ internal fun resolveConnectivityStatus( } private fun RawNetworkState?.toConnectivityStatus() = - Connectivity.Status( - ipv4 = this?.linkProperties?.linkAddresses?.any { it.address is Inet4Address } == true, - ipv6 = this?.linkProperties?.linkAddresses?.any { it.address is Inet6Address } == true, - ) + Connectivity.fromLinkAddresses(this?.linkProperties?.linkAddresses ?: emptyList()) private fun RawNetworkState?.isVpn(): Boolean = this?.networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) == false diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/UnderlyingConnectivityStatusResolver.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/UnderlyingConnectivityStatusResolver.kt index 0024f63357..1a5cb33be4 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/UnderlyingConnectivityStatusResolver.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/UnderlyingConnectivityStatusResolver.kt @@ -14,8 +14,8 @@ import net.mullvad.talpid.model.Connectivity class UnderlyingConnectivityStatusResolver( private val protect: (socket: DatagramSocket) -> Boolean ) { - fun currentStatus(): Connectivity.Status = - Connectivity.Status(ipv4 = hasIpv4(), ipv6 = hasIpv6()) + fun currentStatus(): Connectivity = + Connectivity.fromIpAvailability(ipv4 = hasIpv4(), ipv6 = hasIpv6()) private fun hasIpv4(): Boolean = hasIpVersion(Inet4Address.getByName(PUBLIC_IPV4_ADDRESS), protect) diff --git a/mullvad-daemon/src/tunnel.rs b/mullvad-daemon/src/tunnel.rs index 3fd94d591d..4874ee167a 100644 --- a/mullvad-daemon/src/tunnel.rs +++ b/mullvad-daemon/src/tunnel.rs @@ -8,7 +8,7 @@ use std::{ use tokio::sync::Mutex; -use mullvad_relay_selector::{GetRelay, RelaySelector, RuntimeParameters, WireguardConfig}; +use mullvad_relay_selector::{GetRelay, RelaySelector, WireguardConfig}; use mullvad_types::{ endpoint::MullvadWireguardEndpoint, location::GeoIpLocation, relay_list::Relay, settings::TunnelOptions, @@ -23,7 +23,7 @@ use talpid_types::net::{ #[cfg(target_os = "android")] use talpid_types::net::{obfuscation::ObfuscatorConfig, wireguard, TunnelParameters}; -use talpid_types::{tunnel::ParameterGenerationError, ErrorExt}; +use talpid_types::{net::IpAvailability, tunnel::ParameterGenerationError, ErrorExt}; use crate::device::{AccountManagerHandle, Error as DeviceError, PrivateAccountAndDevice}; @@ -160,12 +160,12 @@ impl InnerParametersGenerator { async fn generate( &mut self, retry_attempt: u32, - ipv6: bool, + ip_availability: IpAvailability, ) -> Result<TunnelParameters, Error> { 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, ip_availability)?; match selected_relay { #[cfg(not(target_os = "android"))] @@ -299,13 +299,13 @@ impl TunnelParametersGenerator for ParametersGenerator { fn generate( &mut self, retry_attempt: u32, - ipv6: bool, + ip_availability: IpAvailability, ) -> Pin<Box<dyn Future<Output = Result<TunnelParameters, ParameterGenerationError>>>> { let generator = self.0.clone(); Box::pin(async move { let mut inner = generator.lock().await; inner - .generate(retry_attempt, ipv6) + .generate(retry_attempt, ip_availability) .await .inspect_err(|error| { log::error!( diff --git a/mullvad-jni/src/classes.rs b/mullvad-jni/src/classes.rs index 6245bd901f..780fcc9724 100644 --- a/mullvad-jni/src/classes.rs +++ b/mullvad-jni/src/classes.rs @@ -18,6 +18,8 @@ pub const CLASSES: &[&str] = &[ "net/mullvad/talpid/ConnectivityListener", "net/mullvad/talpid/TalpidVpnService", "net/mullvad/mullvadvpn/lib/endpoint/ApiEndpointOverride", - "net/mullvad/talpid/model/Connectivity$Status", + "net/mullvad/talpid/model/Connectivity$Online", + "net/mullvad/talpid/model/Connectivity$Offline", "net/mullvad/talpid/model/Connectivity$PresumeOnline", + "net/mullvad/talpid/model/IpAvailability", ]; diff --git a/mullvad-relay-selector/src/error.rs b/mullvad-relay-selector/src/error.rs index 9f7a50bda3..2df69074e4 100644 --- a/mullvad-relay-selector/src/error.rs +++ b/mullvad-relay-selector/src/error.rs @@ -36,6 +36,9 @@ pub enum Error { #[error("Invalid bridge settings")] InvalidBridgeSettings(#[from] MissingCustomBridgeSettings), + + #[error("The requested IP version is not available")] + IpVersionUnavailable, } /// Special type which only shows up in [`Error`]. This error variant signals that no valid diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index 32779f4431..7947885082 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -11,6 +11,5 @@ pub use error::Error; pub use relay_selector::{ detailer, matcher, matcher::filter_matching_relay_list, query, relays::WireguardConfig, AdditionalRelayConstraints, AdditionalWireguardConstraints, GetRelay, RelaySelector, - RuntimeParameters, SelectedBridge, SelectedObfuscator, SelectorConfig, OPENVPN_RETRY_ORDER, - WIREGUARD_RETRY_ORDER, + SelectedBridge, SelectedObfuscator, SelectorConfig, OPENVPN_RETRY_ORDER, WIREGUARD_RETRY_ORDER, }; diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index 7fef7c15c4..1468f608b3 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -28,7 +28,6 @@ use std::{ use chrono::{DateTime, Local}; use itertools::Itertools; - use mullvad_types::{ constraints::Constraint, custom_list::CustomListsSettings, @@ -48,13 +47,13 @@ use talpid_types::{ net::{ obfuscation::ObfuscatorConfig, proxy::{CustomProxy, Shadowsocks}, - Endpoint, TransportProtocol, TunnelType, + Endpoint, IpAvailability, IpVersion, TransportProtocol, TunnelType, }, ErrorExt, }; -/// [`WIREGUARD_RETRY_ORDER`] defines an ordered set of relay parameters which the relay selector should -/// prioritize on successive connection attempts. Note that these will *never* override user +/// [`WIREGUARD_RETRY_ORDER`] defines an ordered set of relay parameters which the relay selector +/// should prioritize on successive connection attempts. Note that these will *never* override user /// preferences. See [the documentation on `RelayQuery`][RelayQuery] for further details. /// /// This list should be kept in sync with the expected behavior defined in `docs/relay-selector.md` @@ -81,8 +80,8 @@ pub static WIREGUARD_RETRY_ORDER: LazyLock<Vec<RelayQuery>> = LazyLock::new(|| { ] }); -/// [`OPENVPN_RETRY_ORDER`] defines an ordered set of relay parameters which the relay selector should -/// prioritize on successive connection attempts. Note that these will *never* override user +/// [`OPENVPN_RETRY_ORDER`] defines an ordered set of relay parameters which the relay selector +/// should prioritize on successive connection attempts. Note that these will *never* override user /// preferences. See [the documentation on `RelayQuery`][RelayQuery] for further details. /// /// This list should be kept in sync with the expected behavior defined in `docs/relay-selector.md` @@ -179,41 +178,6 @@ pub struct AdditionalWireguardConstraints { pub quantum_resistant: QuantumResistantState, } -/// Values which affect the choice of relay but are only known at runtime. -#[derive(Clone, Debug)] -pub struct RuntimeParameters { - /// Whether IPv6 is available - pub ipv6: bool, -} - -impl RuntimeParameters { - /// Return whether a given [query][`RelayQuery`] is valid given the current runtime parameters - pub fn compatible(&self, query: &RelayQuery) -> bool { - if !self.ipv6 { - let must_use_ipv6 = matches!( - query.wireguard_constraints().ip_version, - Constraint::Only(talpid_types::net::IpVersion::V6) - ); - if must_use_ipv6 { - log::trace!( - "{query:?} is incompatible with {self:?} due to IPv6 not being available" - ); - return false; - } - } - true - } -} - -// Note: It is probably not a good idea to rely on derived default values to be correct for our use -// case. -#[allow(clippy::derivable_impls)] -impl Default for RuntimeParameters { - fn default() -> Self { - RuntimeParameters { ipv6: false } - } -} - /// This enum exists to separate the two types of [`SelectorConfig`] that exists. /// /// The first one is a "regular" config, where [`SelectorConfig::relay_settings`] is @@ -563,7 +527,7 @@ impl RelaySelector { pub fn get_relay( &self, retry_attempt: usize, - runtime_params: RuntimeParameters, + runtime_ip_availability: IpAvailability, ) -> Result<GetRelay, Error> { let config_guard = self.config.lock().unwrap(); let config = SpecializedSelectorConfig::from(&*config_guard); @@ -579,12 +543,12 @@ impl RelaySelector { TunnelType::Wireguard => self.get_relay_with_custom_params( retry_attempt, &WIREGUARD_RETRY_ORDER, - runtime_params, + runtime_ip_availability, ), TunnelType::OpenVpn => self.get_relay_with_custom_params( retry_attempt, &OPENVPN_RETRY_ORDER, - runtime_params, + runtime_ip_availability, ), } } @@ -597,7 +561,7 @@ impl RelaySelector { &self, retry_attempt: usize, retry_order: &[RelayQuery], - runtime_params: RuntimeParameters, + runtime_ip_availability: IpAvailability, ) -> Result<GetRelay, Error> { let config_guard = self.config.lock().unwrap(); let config = SpecializedSelectorConfig::from(&*config_guard); @@ -614,7 +578,7 @@ impl RelaySelector { let query = Self::pick_and_merge_query( retry_attempt, retry_order, - runtime_params, + runtime_ip_availability, &normal_config, &relay_list, )?; @@ -640,17 +604,15 @@ impl RelaySelector { fn pick_and_merge_query( retry_attempt: usize, retry_order: &[RelayQuery], - runtime_params: RuntimeParameters, + runtime_ip_availability: IpAvailability, user_config: &NormalSelectorConfig<'_>, parsed_relays: &RelayList, ) -> Result<RelayQuery, Error> { - let user_query = RelayQuery::try_from(user_config.clone())?; + let mut user_query = RelayQuery::try_from(user_config.clone())?; + apply_ip_availability(runtime_ip_availability, &mut user_query)?; log::trace!("Merging user preferences {user_query:?} with default retry strategy"); retry_order .iter() - // Remove candidate queries based on runtime parameters before trying to merge user - // settings - .filter(|query| runtime_params.compatible(query)) .filter_map(|query| query.clone().intersection(user_query.clone())) .filter(|query| Self::get_relay_inner(query, parsed_relays, user_config.custom_lists).is_ok()) .cycle() // If the above filters remove all relays, cycle will also return an empty iterator @@ -693,10 +655,10 @@ impl RelaySelector { parsed_relays: &RelayList, custom_lists: &CustomListsSettings, ) -> Result<GetRelay, Error> { - // FIXME: A bit of defensive programming - calling `get_wireguard_relay_inner` with a query that - // doesn't specify Wireguard as the desired tunnel type is not valid and will lead - // to unwanted behavior. This should be seen as a workaround, and it would be nicer - // to lift this invariant to be checked by the type system instead. + // FIXME: A bit of defensive programming - calling `get_wireguard_relay_inner` with a query + // that doesn't specify Wireguard as the desired tunnel type is not valid and will + // lead to unwanted behavior. This should be seen as a workaround, and it would be + // nicer to lift this invariant to be checked by the type system instead. let mut query = query.clone(); query.set_tunnel_protocol(TunnelType::Wireguard)?; Self::get_wireguard_relay_inner(&query, custom_lists, parsed_relays) @@ -1183,6 +1145,27 @@ impl RelaySelector { } } +fn apply_ip_availability( + runtime_ip_availability: IpAvailability, + user_query: &mut RelayQuery, +) -> Result<(), Error> { + let ip_version = match runtime_ip_availability { + IpAvailability::Ipv4 => Constraint::Only(IpVersion::V4), + IpAvailability::Ipv6 => Constraint::Only(IpVersion::V6), + IpAvailability::Ipv4AndIpv6 => Constraint::Any, + }; + let wireguard_constraints = user_query + .wireguard_constraints() + .to_owned() + .intersection(WireguardRelayQuery { + ip_version, + ..Default::default() + }) + .ok_or(Error::IpVersionUnavailable)?; + user_query.set_wireguard_constraints(wireguard_constraints)?; + Ok(()) +} + #[derive(Clone)] struct RelayWithDistance { distance: f64, diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 08f6f49870..9e66d6a1ba 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -15,8 +15,8 @@ use talpid_types::net::{ use mullvad_relay_selector::{ query::{builder::RelayQueryBuilder, BridgeQuery, ObfuscationQuery, OpenVpnRelayQuery}, - Error, GetRelay, RelaySelector, RuntimeParameters, SelectedObfuscator, SelectorConfig, - WireguardConfig, OPENVPN_RETRY_ORDER, WIREGUARD_RETRY_ORDER, + Error, GetRelay, RelaySelector, SelectedObfuscator, SelectorConfig, WireguardConfig, + OPENVPN_RETRY_ORDER, WIREGUARD_RETRY_ORDER, }; use mullvad_types::{ constraints::Constraint, @@ -379,7 +379,8 @@ fn assert_openvpn_retry_order() { ); } -/// Test whether the relay selector seems to respect the order as defined by [`WIREGUARD_RETRY_ORDER`]. +/// Test whether the relay selector seems to respect the order as defined by +/// [`WIREGUARD_RETRY_ORDER`]. #[test] fn test_wireguard_retry_order() { // In order to for the relay queries defined by `RETRY_ORDER` to always take precedence, @@ -390,7 +391,10 @@ fn test_wireguard_retry_order() { let relay_selector = default_relay_selector(); for (retry_attempt, query) in WIREGUARD_RETRY_ORDER.iter().enumerate() { let relay = relay_selector - .get_relay(retry_attempt, RuntimeParameters { ipv6: true }) + .get_relay( + retry_attempt, + talpid_types::net::IpAvailability::Ipv4AndIpv6, + ) .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())); @@ -429,7 +433,8 @@ fn test_wireguard_retry_order() { } } -/// Test whether the relay selector seems to respect the order as defined by [`OPENVPN_RETRY_ORDER`]. +/// Test whether the relay selector seems to respect the order as defined by +/// [`OPENVPN_RETRY_ORDER`]. #[test] fn test_openvpn_retry_order() { // In order to for the relay queries defined by `RETRY_ORDER` to always take precedence, @@ -448,7 +453,10 @@ fn test_openvpn_retry_order() { for (retry_attempt, query) in OPENVPN_RETRY_ORDER.iter().enumerate() { let relay = relay_selector - .get_relay(retry_attempt, RuntimeParameters { ipv6: true }) + .get_relay( + retry_attempt, + talpid_types::net::IpAvailability::Ipv4AndIpv6, + ) .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())); @@ -1154,7 +1162,11 @@ fn test_openvpn_auto_bridge() { .take(100 * retry_order.len()) { let relay = relay_selector - .get_relay_with_custom_params(retry_attempt, &retry_order, RuntimeParameters::default()) + .get_relay_with_custom_params( + retry_attempt, + &retry_order, + talpid_types::net::IpAvailability::Ipv4, + ) .unwrap(); match relay { GetRelay::OpenVpn { bridge, .. } => { @@ -1263,7 +1275,7 @@ fn test_include_in_country() { // If include_in_country is false for all relays, a relay must be selected anyway. let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list.clone()); assert!(relay_selector - .get_relay(0, RuntimeParameters::default()) + .get_relay(0, talpid_types::net::IpAvailability::Ipv4) .is_ok()); // If include_in_country is true for some relay, it must always be selected. @@ -1272,7 +1284,7 @@ fn test_include_in_country() { let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list); let relay = unwrap_relay( relay_selector - .get_relay(0, RuntimeParameters::default()) + .get_relay(0, talpid_types::net::IpAvailability::Ipv4) .expect("expected match"), ); @@ -1609,9 +1621,68 @@ fn valid_user_setting_should_yield_relay() { let user_result = relay_selector.get_relay_by_query(user_query.clone()); for retry_attempt in 0..WIREGUARD_RETRY_ORDER.len() { let post_unification_result = - relay_selector.get_relay(retry_attempt, RuntimeParameters::default()); + relay_selector.get_relay(retry_attempt, talpid_types::net::IpAvailability::Ipv4); if user_result.is_ok() { assert!(post_unification_result.is_ok(), "Expected Post-unification query to be valid because original query {:#?} yielded a connection configuration", user_query) } } } + +/// Check that if IPv4 is not available and shadowsocks obfuscation is requested +/// it should return a relay with IPv6 address. +#[test] +fn test_shadowsocks_runtime_ipv4_unavailable() { + // Make a valid user relay constraint + let (relay_constraints, _, _, obfs_settings) = RelayQueryBuilder::wireguard() + .shadowsocks() + .build() + .into_settings(); + + let config = SelectorConfig { + relay_settings: relay_constraints.into(), + obfuscation_settings: obfs_settings, + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + let runtime_parameters = talpid_types::net::IpAvailability::Ipv6; + let user_result = relay_selector.get_relay(0, runtime_parameters).unwrap(); + assert!( + matches!(user_result, GetRelay::Wireguard { + obfuscator: Some(SelectedObfuscator { + config: ObfuscatorConfig::Shadowsocks { + endpoint, + .. + }, + .. + }), + .. + } if endpoint.is_ipv6()), + "expected IPv6 endpoint for Shadowsocks, got {user_result:?}" + ); +} + +/// Check that if IPv4 is not available, a relay with an IPv6 endpoint is returned. +#[test] +fn test_runtime_ipv4_unavailable() { + // Make a valid user relay constraint + let (relay_constraints, ..) = RelayQueryBuilder::wireguard().build().into_settings(); + + let config = SelectorConfig { + relay_settings: relay_constraints.into(), + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + let runtime_parameters = talpid_types::net::IpAvailability::Ipv6; + let relay = relay_selector.get_relay(0, runtime_parameters).unwrap(); + match relay { + GetRelay::Wireguard { endpoint, .. } => { + assert!( + endpoint.peer.endpoint.is_ipv6(), + "expected IPv6 endpoint, got {endpoint:?}", + ); + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } +} diff --git a/talpid-core/src/connectivity_listener.rs b/talpid-core/src/connectivity_listener.rs index 1e6e504f7d..57015fc9fe 100644 --- a/talpid-core/src/connectivity_listener.rs +++ b/talpid-core/src/connectivity_listener.rs @@ -5,7 +5,6 @@ use jnix::{ jni::{ self, objects::{GlobalRef, JObject, JValue}, - sys::{jboolean, JNI_TRUE}, JNIEnv, JavaVM, }, FromJava, JnixEnv, @@ -165,10 +164,9 @@ impl ConnectivityListener { #[unsafe(no_mangle)] #[allow(non_snake_case)] pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyConnectivityChange( - _env: JNIEnv<'_>, + env: JNIEnv<'_>, _obj: JObject<'_>, - is_ipv4: jboolean, - is_ipv6: jboolean, + connectivity: JObject<'_>, ) { let Some(tx) = &*CONNECTIVITY_TX.lock().unwrap() else { // No sender has been registered @@ -176,16 +174,10 @@ pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyConnec return; }; - let is_ipv4 = JNI_TRUE == is_ipv4; - let is_ipv6 = JNI_TRUE == is_ipv6; + let env = JnixEnv::from(env); + let connectivity: Connectivity = FromJava::from_java(&env, connectivity); - if tx - .unbounded_send(Connectivity::Status { - ipv4: is_ipv4, - ipv6: is_ipv6, - }) - .is_err() - { + if tx.unbounded_send(connectivity).is_err() { log::warn!("Failed to send offline change event"); } } diff --git a/talpid-core/src/offline/linux.rs b/talpid-core/src/offline/linux.rs index b4f96ba8c5..6055bd7ccd 100644 --- a/talpid-core/src/offline/linux.rs +++ b/talpid-core/src/offline/linux.rs @@ -82,7 +82,7 @@ async fn check_connectivity(handle: &RouteManagerHandle, fwmark: Option<u32>) -> route_exists(PUBLIC_INTERNET_ADDRESS_V4).await, route_exists(PUBLIC_INTERNET_ADDRESS_V6).await, ) { - (Ok(ipv4), Ok(ipv6)) => Connectivity::Status { ipv4, ipv6 }, + (Ok(ipv4), Ok(ipv6)) => Connectivity::new(ipv4, ipv6), // If we fail to retrieve the IPv4 route, always assume we're connected (Err(err), _) => { log::error!( @@ -99,7 +99,7 @@ async fn check_connectivity(handle: &RouteManagerHandle, fwmark: Option<u32>) -> "Failed to infer offline state for IPv6. Assuming it's unavailable" ) ); - Connectivity::Status { ipv4, ipv6: false } + Connectivity::new(ipv4, false) } } } diff --git a/talpid-core/src/offline/macos.rs b/talpid-core/src/offline/macos.rs index daafecb052..71a799ef50 100644 --- a/talpid-core/src/offline/macos.rs +++ b/talpid-core/src/offline/macos.rs @@ -52,10 +52,7 @@ struct ConnectivityInner { impl ConnectivityInner { fn into_connectivity(self) -> Connectivity { - Connectivity::Status { - ipv4: self.ipv4, - ipv6: self.ipv6, - } + Connectivity::new(self.ipv4, self.ipv6) } fn is_online(&self) -> bool { diff --git a/talpid-core/src/offline/windows.rs b/talpid-core/src/offline/windows.rs index 56616cba51..319e12bde3 100644 --- a/talpid-core/src/offline/windows.rs +++ b/talpid-core/src/offline/windows.rs @@ -230,15 +230,9 @@ impl ConnectivityInner { /// `ipv4` and `ipv6` availability to `false`. fn into_connectivity(self) -> Connectivity { if self.suspended { - Connectivity::Status { - ipv4: false, - ipv6: false, - } + Connectivity::Offline } else { - Connectivity::Status { - ipv4: self.ipv4, - ipv6: self.ipv6, - } + Connectivity::new(self.ipv4, self.ipv6) } } diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index cb06540b0f..66534f6081 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -9,7 +9,9 @@ use futures::{FutureExt, StreamExt}; use talpid_routing::RouteManagerHandle; use talpid_tunnel::tun_provider::TunProvider; use talpid_tunnel::{EventHook, TunnelArgs, TunnelEvent, TunnelMetadata}; -use talpid_types::net::{AllowedClients, AllowedEndpoint, AllowedTunnelTraffic, TunnelParameters}; +use talpid_types::net::{ + AllowedClients, AllowedEndpoint, AllowedTunnelTraffic, IpAvailability, TunnelParameters, +}; use talpid_types::tunnel::{ErrorStateCause, FirewallPolicyError}; use talpid_types::ErrorExt; @@ -73,19 +75,24 @@ impl ConnectingState { }); } - if shared_values.connectivity.is_offline() { - // FIXME: Temporary: Nudge route manager to update the default interface - #[cfg(target_os = "macos")] - { - log::debug!("Poking route manager to update default routes"); - let _ = shared_values.route_manager.refresh_routes(); + let ip_availability = match shared_values.connectivity { + talpid_types::net::Connectivity::Offline => { + // FIXME: Temporary: Nudge route manager to update the default interface + #[cfg(target_os = "macos")] + { + log::debug!("Poking route manager to update default routes"); + let _ = shared_values.route_manager.refresh_routes(); + } + return ErrorState::enter(shared_values, ErrorStateCause::IsOffline); } - return ErrorState::enter(shared_values, ErrorStateCause::IsOffline); - } + talpid_types::net::Connectivity::PresumeOnline => IpAvailability::Ipv4, + talpid_types::net::Connectivity::Online(ip_availability) => ip_availability, + }; + match shared_values.runtime.block_on( shared_values .tunnel_parameters_generator - .generate(retry_attempt, shared_values.connectivity.has_ipv6()), + .generate(retry_attempt, ip_availability), ) { 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 cae7f2384b..a2446b0d84 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -46,7 +46,7 @@ use std::{ #[cfg(target_os = "android")] use talpid_types::{android::AndroidContext, ErrorExt}; use talpid_types::{ - net::{AllowedEndpoint, Connectivity, TunnelParameters}, + net::{AllowedEndpoint, Connectivity, IpAvailability, TunnelParameters}, tunnel::{ErrorStateCause, ParameterGenerationError, TunnelStateTransition}, }; @@ -452,7 +452,7 @@ pub trait TunnelParametersGenerator: Send + 'static { fn generate( &mut self, retry_attempt: u32, - ipv6: bool, + ip_availability: IpAvailability, ) -> Pin<Box<dyn Future<Output = Result<TunnelParameters, ParameterGenerationError>>>>; } diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index 19b04918e5..56934a10d3 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -1,3 +1,4 @@ +use self::proxy::{CustomProxy, Socks5Local}; use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; #[cfg(target_os = "android")] use jnix::FromJava; @@ -12,8 +13,6 @@ use std::{ sync::LazyLock, }; -use self::proxy::{CustomProxy, Socks5Local}; - pub mod obfuscation; pub mod openvpn; pub mod proxy; @@ -571,15 +570,22 @@ pub fn all_of_the_internet() -> Vec<ipnetwork::IpNetwork> { #[cfg_attr(target_os = "android", derive(FromJava))] #[cfg_attr(target_os = "android", jnix(package = "net.mullvad.talpid.model"))] pub enum Connectivity { - 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, - }, - /// On/offline status could not be verified, but we have no particular - /// reason to believe that the host is offline. + /// Host is offline + Offline, + /// The connectivity status is unknown, but presumed to be online PresumeOnline, + /// Host is online with the given IP versions available + Online(IpAvailability), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(target_os = "android", derive(FromJava))] +#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.talpid.model"))] +/// Available IP versions +pub enum IpAvailability { + Ipv4, + Ipv6, + Ipv4AndIpv6, } impl Connectivity { @@ -591,12 +597,18 @@ impl Connectivity { /// If no IP4 nor IPv6 routes exist, we have no way of reaching the internet /// so we consider ourselves offline. pub fn is_offline(&self) -> bool { + *self == Connectivity::Offline + } + + /// Whether IPv4 connectivity seems to be available on the host. + /// + /// If IPv4 status is unknown, `true` is returned. + pub fn has_ipv4(&self) -> bool { matches!( self, - Connectivity::Status { - ipv4: false, - ipv6: false - } + Connectivity::PresumeOnline + | Connectivity::Online(IpAvailability::Ipv4) + | Connectivity::Online(IpAvailability::Ipv4AndIpv6) ) } @@ -604,6 +616,19 @@ impl Connectivity { /// /// If IPv6 status is unknown, `false` is returned. pub fn has_ipv6(&self) -> bool { - matches!(self, Connectivity::Status { ipv6: true, .. }) + matches!( + self, + Connectivity::Online(IpAvailability::Ipv6) + | Connectivity::Online(IpAvailability::Ipv4AndIpv6) + ) + } + + pub fn new(ipv4: bool, ipv6: bool) -> Connectivity { + match (ipv4, ipv6) { + (true, true) => Connectivity::Online(IpAvailability::Ipv4AndIpv6), + (true, false) => Connectivity::Online(IpAvailability::Ipv4), + (false, true) => Connectivity::Online(IpAvailability::Ipv6), + (false, false) => Connectivity::Offline, + } } } |
