diff --git a/mullvad-daemon/src/tunnel.rs b/mullvad-daemon/src/tunnel.rs index 27cb6287382c..f7fb282d27a2 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, RuntimeIpAvailability, WireguardConfig}; use mullvad_types::{ endpoint::MullvadWireguardEndpoint, location::GeoIpLocation, relay_list::Relay, settings::TunnelOptions, @@ -164,9 +164,10 @@ impl InnerParametersGenerator { ipv6: bool, ) -> Result<TunnelParameters, Error> { let data = self.device().await?; - let selected_relay = self - .relay_selector - .get_relay(retry_attempt as usize, RuntimeParameters::new(ipv4, ipv6))?; + let selected_relay = self.relay_selector.get_relay( + retry_attempt as usize, + RuntimeIpAvailability::new(ipv4, ipv6), + )?; match selected_relay { #[cfg(not(target_os = "android"))] diff --git a/mullvad-relay-selector/src/error.rs b/mullvad-relay-selector/src/error.rs index 9f7a50bda36c..2df69074e40a 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 bf0b3385ae60..8ea4d58b7ec5 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -10,7 +10,7 @@ mod relay_selector; pub use error::Error; pub use relay_selector::{ detailer, matcher, matcher::filter_matching_relay_list, query, relays::WireguardConfig, - AdditionalRelayConstraints, AdditionalWireguardConstraints, GetRelay, IpAvailability, - RelaySelector, RuntimeParameters, SelectedBridge, SelectedObfuscator, SelectorConfig, - OPENVPN_RETRY_ORDER, WIREGUARD_RETRY_ORDER, + AdditionalRelayConstraints, AdditionalWireguardConstraints, GetRelay, RelaySelector, + RuntimeIpAvailability, 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 0a3f0afe505c..448c8edbf511 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -47,13 +47,13 @@ use talpid_types::{ net::{ obfuscation::ObfuscatorConfig, proxy::{CustomProxy, Shadowsocks}, - Endpoint, TransportProtocol, TunnelType, + Endpoint, 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` @@ -80,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` @@ -178,64 +178,26 @@ pub struct AdditionalWireguardConstraints { pub quantum_resistant: QuantumResistantState, } -/// Values which affect the choice of relay but are only known at runtime. +/// Whether IPv4 and IPv6 is available at runtime. +/// +/// `None` means that no IP version is available, `Some(Any)` means that both are available, #[derive(Clone, Debug)] -pub struct RuntimeParameters { - /// Whether IPv4, IPv6 or both is available - pub ip_availability: IpAvailability, -} - -impl RuntimeParameters { - /// Return whether a given [query][`RelayQuery`] is valid given the current runtime parameters - pub fn compatible(&self, query: &RelayQuery) -> bool { - match query.wireguard_constraints().ip_version { - Constraint::Any => true, - Constraint::Only(talpid_types::net::IpVersion::V4) => self.ip_availability.has_ipv4(), - Constraint::Only(talpid_types::net::IpVersion::V6) => self.ip_availability.has_ipv6(), - } - } - - pub fn new(ipv4: bool, ipv6: bool) -> RuntimeParameters { - if ipv4 && ipv6 { - RuntimeParameters { - ip_availability: IpAvailability::All, - } - } else if !ipv6 { - RuntimeParameters { - ip_availability: IpAvailability::Ipv4, - } - } else if !ipv4 { - RuntimeParameters { - ip_availability: IpAvailability::Ipv6, - } - } else { - panic!("Device is offline!") - } +pub struct RuntimeIpAvailability(Option<Constraint<IpVersion>>); + +impl RuntimeIpAvailability { + pub fn new(ipv4: bool, ipv6: bool) -> RuntimeIpAvailability { + RuntimeIpAvailability(match (ipv4, ipv6) { + (true, true) => Some(Constraint::Any), + (false, true) => Some(Constraint::Only(IpVersion::V6)), + (true, false) => Some(Constraint::Only(IpVersion::V4)), + (false, false) => None, + }) } } -impl Default for RuntimeParameters { +impl Default for RuntimeIpAvailability { fn default() -> Self { - RuntimeParameters { - ip_availability: IpAvailability::Ipv4, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum IpAvailability { - Ipv4, - Ipv6, - All, -} - -impl IpAvailability { - fn has_ipv4(&self) -> bool { - self.clone() == IpAvailability::Ipv4 || self.clone() == IpAvailability::All - } - - fn has_ipv6(&self) -> bool { - self.clone() == IpAvailability::Ipv6 || self.clone() == IpAvailability::All + RuntimeIpAvailability(Some(Constraint::Only(IpVersion::V4))) } } @@ -588,7 +550,7 @@ impl RelaySelector { pub fn get_relay( &self, retry_attempt: usize, - runtime_params: RuntimeParameters, + runtime_ip_availability: RuntimeIpAvailability, ) -> Result<GetRelay, Error> { let config_guard = self.config.lock().unwrap(); let config = SpecializedSelectorConfig::from(&*config_guard); @@ -604,12 +566,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, ), } } @@ -622,7 +584,7 @@ impl RelaySelector { &self, retry_attempt: usize, retry_order: &[RelayQuery], - runtime_params: RuntimeParameters, + runtime_ip_availability: RuntimeIpAvailability, ) -> Result<GetRelay, Error> { let config_guard = self.config.lock().unwrap(); let config = SpecializedSelectorConfig::from(&*config_guard); @@ -639,7 +601,7 @@ impl RelaySelector { let query = Self::pick_and_merge_query( retry_attempt, retry_order, - runtime_params, + runtime_ip_availability, &normal_config, &relay_list, )?; @@ -665,23 +627,16 @@ impl RelaySelector { fn pick_and_merge_query( retry_attempt: usize, retry_order: &[RelayQuery], - runtime_params: RuntimeParameters, + runtime_ip_availability: RuntimeIpAvailability, 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)) - // Check if the user query aligns with the runtime parameters so that if the user - // has selected an ip version that is not available it will return an error - .filter(|_query| runtime_params.compatible(&user_query)) .filter_map(|query| query.clone().intersection(user_query.clone())) - // Resolve query ip version set to Any based on runtime ip availability - .map(|query| resolve_valid_ip_version(&query, &runtime_params)) .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 .nth(retry_attempt) @@ -723,10 +678,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) @@ -1213,24 +1168,22 @@ impl RelaySelector { } } -/// If the selected ip version is Any we want to resolve that to an Only ip version if only -/// one ip version is available on the network. This is to avoid situations where in other parts -/// of the relay selector that Any is resolved to IPv4 and IPv4 is not available. -fn resolve_valid_ip_version(query: &RelayQuery, runtime_params: &RuntimeParameters) -> RelayQuery { - let mut wireguard_constraints = query.wireguard_constraints().clone(); - if wireguard_constraints.ip_version.is_any() { - if runtime_params.ip_availability == IpAvailability::Ipv4 { - wireguard_constraints.ip_version = Constraint::Only(talpid_types::net::IpVersion::V4) - } - if runtime_params.ip_availability == IpAvailability::Ipv6 { - wireguard_constraints.ip_version = Constraint::Only(talpid_types::net::IpVersion::V6) - } - } - let mut ret = query.clone(); - match ret.set_wireguard_constraints(wireguard_constraints) { - Ok(()) => ret, - _error => query.clone(), - } +fn apply_ip_availability( + runtime_ip_availability: RuntimeIpAvailability, + user_query: &mut RelayQuery, +) -> Result<(), Error> { + let wireguard_constraints = user_query + .wireguard_constraints() + .to_owned() + .intersection(WireguardRelayQuery { + ip_version: runtime_ip_availability + .0 + .ok_or(Error::IpVersionUnavailable)?, + ..Default::default() + }) + .ok_or(Error::IpVersionUnavailable)?; + user_query.set_wireguard_constraints(wireguard_constraints)?; + Ok(()) } #[derive(Clone)] diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 2f1ec6e776e7..db5264be76d0 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, IpAvailability, RelaySelector, RuntimeParameters, SelectedObfuscator, - SelectorConfig, WireguardConfig, OPENVPN_RETRY_ORDER, WIREGUARD_RETRY_ORDER, + Error, GetRelay, RelaySelector, RuntimeIpAvailability, 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,12 +391,7 @@ 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 { - ip_availability: IpAvailability::All, - }, - ) + .get_relay(retry_attempt, RuntimeIpAvailability::new(false, 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())); @@ -434,7 +430,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, @@ -453,12 +450,7 @@ fn test_openvpn_retry_order() { for (retry_attempt, query) in OPENVPN_RETRY_ORDER.iter().enumerate() { let relay = relay_selector - .get_relay( - retry_attempt, - RuntimeParameters { - ip_availability: IpAvailability::All, - }, - ) + .get_relay(retry_attempt, RuntimeIpAvailability::new(false, 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())); @@ -1164,7 +1156,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, + RuntimeIpAvailability::default(), + ) .unwrap(); match relay { GetRelay::OpenVpn { bridge, .. } => { @@ -1273,7 +1269,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, RuntimeIpAvailability::default()) .is_ok()); // If include_in_country is true for some relay, it must always be selected. @@ -1282,7 +1278,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, RuntimeIpAvailability::default()) .expect("expected match"), ); @@ -1619,7 +1615,7 @@ 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, RuntimeIpAvailability::default()); 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) } @@ -1642,9 +1638,7 @@ fn test_shadowsocks_runtime_ipv4_unavailable() { ..SelectorConfig::default() }; let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); - let runtime_parameters = RuntimeParameters { - ip_availability: IpAvailability::Ipv6, - }; + let runtime_parameters = RuntimeIpAvailability::new(false, true); let user_result = relay_selector.get_relay(0, runtime_parameters).unwrap(); assert!( matches!(user_result, GetRelay::Wireguard { @@ -1665,16 +1659,14 @@ fn test_shadowsocks_runtime_ipv4_unavailable() { #[test] fn test_runtime_ipv4_unavailable() { // Make a valid user relay constraint - let (relay_constraints, _, _, _) = RelayQueryBuilder::wireguard().build().into_settings(); + 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 = RuntimeParameters { - ip_availability: IpAvailability::Ipv6, - }; + let runtime_parameters = RuntimeIpAvailability::new(false, true); let relay = relay_selector.get_relay(0, runtime_parameters).unwrap(); match relay { GetRelay::Wireguard { endpoint, .. } => {