diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-04-17 17:58:19 +0200 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-04-17 17:58:19 +0200 |
| commit | 52b70e8221c6709f2e8997ba94fc2e2894c7d191 (patch) | |
| tree | b5e0b3a6083685de454b47e4b936f971d4fb1373 | |
| parent | f1d97bc5fd2107f3f90d9aea8f9e55a12f6c13bb (diff) | |
| parent | ce59eb20d2cde53a829f891e5e0aca0938e654a8 (diff) | |
| download | mullvadvpn-52b70e8221c6709f2e8997ba94fc2e2894c7d191.tar.xz mullvadvpn-52b70e8221c6709f2e8997ba94fc2e2894c7d191.zip | |
Merge branch 'relay-selector-bug'
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/mod.rs | 138 | ||||
| -rw-r--r-- | mullvad-relay-selector/tests/relay_selector.rs | 27 |
2 files changed, 88 insertions, 77 deletions
diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index 3f92bd344b..843eea9a20 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -472,7 +472,7 @@ impl RelaySelector { let near_location = match specialized_config { SpecializedSelectorConfig::Normal(config) => { let user_preferences = RelayQuery::from(config.clone()); - Self::get_relay_midpoint(&user_preferences, parsed_relays, &config) + Self::get_relay_midpoint(&user_preferences, parsed_relays, config.custom_lists) } SpecializedSelectorConfig::Custom(_) => None, }; @@ -508,9 +508,9 @@ impl RelaySelector { SpecializedSelectorConfig::Custom(custom_config) => { Ok(GetRelay::Custom(custom_config.clone())) } - SpecializedSelectorConfig::Normal(pure_config) => { + SpecializedSelectorConfig::Normal(normal_config) => { let parsed_relays = &self.parsed_relays.lock().unwrap(); - Self::get_relay_inner(query, parsed_relays, &pure_config) + Self::get_relay_inner(&query, parsed_relays, normal_config.custom_lists) } } } @@ -527,30 +527,6 @@ impl RelaySelector { self.get_relay_with_custom_params(retry_attempt, &RETRY_ORDER, runtime_params) } - /// Peek at which [`TunnelType`] that would be returned for a certain connection attempt for a - /// given [`SelectorConfig`]. Returns [`Option::None`] if the given config would return a - /// custom tunnel endpoint. - /// - /// # Note - /// This function is only really useful for testing-purposes. It is exposed to ease testing of - /// other mullvad crates which depend on the retry behaviour of [`RelaySelector`]. - pub fn would_return(connection_attempt: usize, config: &SelectorConfig) -> Option<TunnelType> { - match SpecializedSelectorConfig::from(config) { - // This case is not really interesting - SpecializedSelectorConfig::Custom(_) => None, - SpecializedSelectorConfig::Normal(config) => Some( - Self::pick_and_merge_query( - connection_attempt, - &RETRY_ORDER, - RuntimeParameters::default(), - RelayQuery::from(config), - ) - .tunnel_protocol - .unwrap_or(TunnelType::Wireguard), - ), - } - } - /// Returns a random relay and relay endpoint matching the current constraints defined by /// `retry_order` corresponding to `retry_attempt`. pub fn get_relay_with_custom_params( @@ -571,14 +547,14 @@ impl RelaySelector { SpecializedSelectorConfig::Normal(normal_config) => { let parsed_relays = &self.parsed_relays.lock().unwrap(); // Merge user preferences with the relay selector's default preferences. - let user_preferences = RelayQuery::from(normal_config.clone()); let query = Self::pick_and_merge_query( retry_attempt, retry_order, runtime_params, - user_preferences, - ); - Self::get_relay_inner(query, parsed_relays, &normal_config) + &normal_config, + parsed_relays, + )?; + Self::get_relay_inner(&query, parsed_relays, normal_config.custom_lists) } } } @@ -593,22 +569,29 @@ impl RelaySelector { /// Runtime parameters may affect which of the default queries that are considered. For example, /// queries which rely on IPv6 will not be considered if working IPv6 is not available at /// runtime. + /// + /// Returns an error iff the intersection between the user's preferences and every default retry + /// attempt-query yields queries with no matching relays. I.e., no retry attempt could ever + /// resolve to a relay. fn pick_and_merge_query( retry_attempt: usize, retry_order: &[RelayQuery], runtime_params: RuntimeParameters, - user_preferences: RelayQuery, - ) -> RelayQuery { - log::trace!("Merging user preferences {user_preferences:?} with default retry strategy"); + user_config: &NormalSelectorConfig<'_>, + parsed_relays: &ParsedRelays, + ) -> Result<RelayQuery, Error> { + let user_query = RelayQuery::from(user_config.clone()); + 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)) - .cycle() - .filter_map(|query| query.clone().intersection(user_preferences.clone())) + .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 .nth(retry_attempt) - .unwrap_or(user_preferences) + .ok_or(Error::NoRelay) } /// "Execute" the given query, yielding a final set of relays and/or bridges which the VPN @@ -629,16 +612,16 @@ impl RelaySelector { /// * An `Err` if no suitable bridge is found #[cfg(not(target_os = "android"))] fn get_relay_inner( - query: RelayQuery, + query: &RelayQuery, parsed_relays: &ParsedRelays, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, ) -> Result<GetRelay, Error> { match query.tunnel_protocol { Constraint::Only(TunnelType::Wireguard) => { - Self::get_wireguard_relay(query, config, parsed_relays) + Self::get_wireguard_relay(query, custom_lists, parsed_relays) } Constraint::Only(TunnelType::OpenVpn) => { - Self::get_openvpn_relay(query, config, parsed_relays) + Self::get_openvpn_relay(query, custom_lists, parsed_relays) } Constraint::Any => { // Try Wireguard, then OpenVPN, then fail @@ -646,7 +629,9 @@ impl RelaySelector { let mut new_query = query.clone(); new_query.tunnel_protocol = Constraint::Only(tunnel_type); // If a suitable relay is found, short-circuit and return it - if let Ok(relay) = Self::get_relay_inner(new_query, parsed_relays, config) { + if let Ok(relay) = + Self::get_relay_inner(&new_query, parsed_relays, custom_lists) + { return Ok(relay); } } @@ -657,16 +642,17 @@ impl RelaySelector { #[cfg(target_os = "android")] fn get_relay_inner( - mut query: RelayQuery, + query: &RelayQuery, parsed_relays: &ParsedRelays, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, ) -> Result<GetRelay, Error> { // FIXME: A bit of defensive programming - calling `get_wiregurad_relay` 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.tunnel_protocol = Constraint::Only(TunnelType::Wireguard); - Self::get_wireguard_relay(query, config, parsed_relays) + Self::get_wireguard_relay(&query, custom_lists, parsed_relays) } /// Derive a valid Wireguard relay configuration from `query`. @@ -683,8 +669,8 @@ impl RelaySelector { /// /// [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint fn get_wireguard_relay( - query: RelayQuery, - config: &NormalSelectorConfig<'_>, + query: &RelayQuery, + custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, ) -> Result<GetRelay, Error> { assert_eq!( @@ -692,13 +678,13 @@ impl RelaySelector { Constraint::Only(TunnelType::Wireguard) ); let inner = if !query.wireguard_constraints.multihop() { - Self::get_wireguard_singlehop_config(&query, config, parsed_relays)? + Self::get_wireguard_singlehop_config(query, custom_lists, parsed_relays)? } else { - Self::get_wireguard_multihop_config(&query, config, parsed_relays)? + Self::get_wireguard_multihop_config(query, custom_lists, parsed_relays)? }; - let endpoint = Self::get_wireguard_endpoint(&query, parsed_relays, &inner)?; + let endpoint = Self::get_wireguard_endpoint(query, parsed_relays, &inner)?; let obfuscator = - Self::get_wireguard_obfuscator(&query, inner.clone(), &endpoint, parsed_relays)?; + Self::get_wireguard_obfuscator(query, inner.clone(), &endpoint, parsed_relays)?; Ok(GetRelay::Wireguard { endpoint, @@ -714,11 +700,10 @@ impl RelaySelector { /// * `Ok(WireguardInner::Singlehop)` otherwise fn get_wireguard_singlehop_config( query: &RelayQuery, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, ) -> Result<WireguardConfig, Error> { - let candidates = - filter_matching_relay_list(query, parsed_relays.relays(), config.custom_lists); + let candidates = filter_matching_relay_list(query, parsed_relays.relays(), custom_lists); helpers::pick_random_relay(&candidates) .cloned() .map(WireguardConfig::singlehop) @@ -734,7 +719,7 @@ impl RelaySelector { /// * `Ok(WireguardInner::Multihop)` otherwise fn get_wireguard_multihop_config( query: &RelayQuery, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, ) -> Result<WireguardConfig, Error> { // Here, we modify the original query just a bit. @@ -749,16 +734,10 @@ impl RelaySelector { let mut exit_relay_query = query.clone(); // DAITA should only be enabled for the entry relay exit_relay_query.wireguard_constraints.daita = Constraint::Only(false); - let exit_candidates = filter_matching_relay_list( - &exit_relay_query, - parsed_relays.relays(), - config.custom_lists, - ); - let entry_candidates = filter_matching_relay_list( - &entry_relay_query, - parsed_relays.relays(), - config.custom_lists, - ); + let exit_candidates = + filter_matching_relay_list(&exit_relay_query, parsed_relays.relays(), custom_lists); + let entry_candidates = + filter_matching_relay_list(&entry_relay_query, parsed_relays.relays(), custom_lists); fn pick_random_excluding<'a>(list: &'a [Relay], exclude: &'a Relay) -> Option<&'a Relay> { list.iter() @@ -846,16 +825,21 @@ impl RelaySelector { /// [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint #[cfg(not(target_os = "android"))] fn get_openvpn_relay( - query: RelayQuery, - config: &NormalSelectorConfig<'_>, + query: &RelayQuery, + custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, ) -> Result<GetRelay, Error> { assert_eq!(query.tunnel_protocol, Constraint::Only(TunnelType::OpenVpn)); let exit = - Self::choose_openvpn_relay(&query, config, parsed_relays).ok_or(Error::NoRelay)?; - let endpoint = Self::get_openvpn_endpoint(&query, &exit, parsed_relays)?; - let bridge = - Self::get_openvpn_bridge(&query, &exit, &endpoint.protocol, parsed_relays, config)?; + Self::choose_openvpn_relay(query, custom_lists, parsed_relays).ok_or(Error::NoRelay)?; + let endpoint = Self::get_openvpn_endpoint(query, &exit, parsed_relays)?; + let bridge = Self::get_openvpn_bridge( + query, + &exit, + &endpoint.protocol, + parsed_relays, + custom_lists, + )?; // FIXME: This assert would be better to encode at the type level. assert!(matches!(exit.endpoint_data, RelayEndpointData::Openvpn)); @@ -908,13 +892,13 @@ impl RelaySelector { relay: &Relay, protocol: &TransportProtocol, parsed_relays: &ParsedRelays, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, ) -> Result<Option<SelectedBridge>, Error> { if !BridgeQuery::should_use_bridge(&query.openvpn_constraints.bridge_settings) { Ok(None) } else { let bridge_query = &query.openvpn_constraints.bridge_settings.clone().unwrap(); - let custom_lists = &config.custom_lists; + let custom_lists = &custom_lists; match protocol { TransportProtocol::Udp => { log::error!("Can not use OpenVPN bridges over UDP"); @@ -1032,7 +1016,7 @@ impl RelaySelector { fn get_relay_midpoint( query: &RelayQuery, parsed_relays: &ParsedRelays, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, ) -> Option<Coordinates> { use std::ops::Not; if query.location.is_any() { @@ -1040,7 +1024,7 @@ impl RelaySelector { } let matching_locations: Vec<Location> = - filter_matching_relay_list(query, parsed_relays.relays(), config.custom_lists) + filter_matching_relay_list(query, parsed_relays.relays(), custom_lists) .into_iter() .filter_map(|relay| relay.location) .unique_by(|location| location.city.clone()) @@ -1058,12 +1042,12 @@ impl RelaySelector { #[cfg(not(target_os = "android"))] fn choose_openvpn_relay( query: &RelayQuery, - config: &NormalSelectorConfig<'_>, + custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, ) -> Option<Relay> { // Filter among all valid relays let relays = parsed_relays.relays(); - let candidates = filter_matching_relay_list(query, relays, config.custom_lists); + let candidates = filter_matching_relay_list(query, relays, custom_lists); // Pick one of the valid relays. helpers::pick_random_relay(&candidates).cloned() } diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index d92e7a72a9..7cf2329e9a 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -1296,3 +1296,30 @@ fn test_daita() { ), } } + +/// Check that if the original user query would yield a relay, the result of running the query +/// which is the intersection between the user query and any of the default queries shall never +/// fail. +#[test] +fn valid_user_setting_should_yield_relay() { + // Make a valid user relay constraint + let location = GeographicLocationConstraint::hostname("se", "got", "se9-wireguard"); + let user_query = RelayQueryBuilder::new().location(location.clone()).build(); + let user_constraints = RelayQueryBuilder::new() + .location(location.clone()) + .into_constraint(); + + let config = SelectorConfig { + relay_settings: user_constraints.into(), + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + let user_result = relay_selector.get_relay_by_query(user_query.clone()); + for retry_attempt in 0..RETRY_ORDER.len() { + let post_unification_result = + relay_selector.get_relay(retry_attempt, RuntimeParameters::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) + } + } +} |
