summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-03-06 09:10:00 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-03-25 17:17:00 +0100
commitdb66c85daab1ba708b282b5243d7835beb615b3b (patch)
treefe1b75670efe95c0bcb5f17491161249b8acda21
parentb582c60162fac40a831d0ef56089c94e491e959b (diff)
downloadmullvadvpn-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>
-rw-r--r--CHANGELOG.md1
-rw-r--r--android/CHANGELOG.md2
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/talpid/util/ConnectivityManagerUtilKtTest.kt32
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt4
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/Connectivity.kt24
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/IpAvailability.kt7
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/ConnectivityManagerUtil.kt11
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/UnderlyingConnectivityStatusResolver.kt4
-rw-r--r--mullvad-daemon/src/tunnel.rs12
-rw-r--r--mullvad-jni/src/classes.rs4
-rw-r--r--mullvad-relay-selector/src/error.rs3
-rw-r--r--mullvad-relay-selector/src/lib.rs3
-rw-r--r--mullvad-relay-selector/src/relay_selector/mod.rs93
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs91
-rw-r--r--talpid-core/src/connectivity_listener.rs18
-rw-r--r--talpid-core/src/offline/linux.rs4
-rw-r--r--talpid-core/src/offline/macos.rs5
-rw-r--r--talpid-core/src/offline/windows.rs10
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs27
-rw-r--r--talpid-core/src/tunnel_state_machine/mod.rs4
-rw-r--r--talpid-types/src/net/mod.rs55
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,
+ }
}
}