diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-02-16 16:24:33 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-03-27 11:43:44 +0100 |
| commit | 707ecf44bd2b21642e51c8b9f5440bc287bcc511 (patch) | |
| tree | 1c4e914a879cc6d1c126db1e47019cc2f5f2cea4 /mullvad-relay-selector | |
| parent | 66f2127aed8bea1e1434c7e8efc50293ebdd9223 (diff) | |
| download | mullvadvpn-707ecf44bd2b21642e51c8b9f5440bc287bcc511.tar.xz mullvadvpn-707ecf44bd2b21642e51c8b9f5440bc287bcc511.zip | |
Refactor `mullvad-relay-selector`
Implement a system built on 'queries' for selecting appropriate relays.
A query is a set of constraints which dictates which relay(s) that *can*
be chosen by the relay selector.
The user's settings can naturally be expressed as a query. The semantics
of merging two queries in a way that always prefer user settings is
defined by the new `Intersection` trait.
Split `mullvad-relay-selector` into several modules:
- `query.rs`: Definition of a query on different types of relays. This
module is integral to the new API of `mullvad-relay-selector`
- `matcher.rs`: Logic for filtering out candidate relays based on a
query.
- `detailer.rs`: Logic for deriving connection details for the selected
relay.
- `tests/`: Integration tests for the new relay selector. These tests
only use the public APIs of `RelaySelector` and make sure that the
output matches the expected output in different scenarios.
Diffstat (limited to 'mullvad-relay-selector')
| -rw-r--r-- | mullvad-relay-selector/Cargo.toml | 4 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/constants.rs | 4 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/error.rs | 66 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/lib.rs | 2413 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/matcher.rs | 341 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/detailer.rs | 283 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/helpers.rs | 124 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/matcher.rs | 186 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/mod.rs | 978 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/parsed_relays.rs | 189 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/query.rs | 916 | ||||
| -rw-r--r-- | mullvad-relay-selector/tests/relay_selector.rs | 1112 |
12 files changed, 3872 insertions, 2744 deletions
diff --git a/mullvad-relay-selector/Cargo.toml b/mullvad-relay-selector/Cargo.toml index c5f83018ea..ec8943df0b 100644 --- a/mullvad-relay-selector/Cargo.toml +++ b/mullvad-relay-selector/Cargo.toml @@ -14,7 +14,9 @@ workspace = true chrono = { workspace = true } thiserror = { workspace = true } ipnetwork = "0.16" +itertools = "0.12" log = { workspace = true } +once_cell = { workspace = true } rand = "0.8.5" serde_json = "1.0" @@ -22,4 +24,4 @@ talpid-types = { path = "../talpid-types" } mullvad-types = { path = "../mullvad-types" } [dev-dependencies] -once_cell = { workspace = true } +proptest = { workspace = true } diff --git a/mullvad-relay-selector/src/constants.rs b/mullvad-relay-selector/src/constants.rs new file mode 100644 index 0000000000..5e6b511195 --- /dev/null +++ b/mullvad-relay-selector/src/constants.rs @@ -0,0 +1,4 @@ +//! Constants used throughout the relay selector + +/// All the valid ports when using UDP2TCP obfuscation. +pub(crate) const UDP2TCP_PORTS: [u16; 2] = [80, 5001]; diff --git a/mullvad-relay-selector/src/error.rs b/mullvad-relay-selector/src/error.rs new file mode 100644 index 0000000000..f988be1b89 --- /dev/null +++ b/mullvad-relay-selector/src/error.rs @@ -0,0 +1,66 @@ +//! Definition of relay selector errors +#![allow(dead_code)] + +use mullvad_types::{relay_constraints::MissingCustomBridgeSettings, relay_list::Relay}; + +use crate::{detailer, WireguardConfig}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to open relay cache file")] + OpenRelayCache(#[source] std::io::Error), + + #[error("Failed to write relay cache file to disk")] + WriteRelayCache(#[source] std::io::Error), + + #[error("No relays matching current constraints")] + NoRelay, + + #[error("No bridges matching current constraints")] + NoBridge, + + #[error("No obfuscators matching current constraints")] + NoObfuscator, + + #[error("No endpoint could be constructed due to {} for relay {:?}", .internal, .relay)] + NoEndpoint { + internal: detailer::Error, + relay: EndpointErrorDetails, + }, + + #[error("Failure in serialization of the relay list")] + Serialize(#[from] serde_json::Error), + + #[error("Invalid bridge settings")] + InvalidBridgeSettings(#[from] MissingCustomBridgeSettings), +} + +/// Special type which only shows up in [`Error`]. This error variant signals that no valid +/// endpoint could be constructed from the selected relay. +#[derive(Debug)] +pub enum EndpointErrorDetails { + /// No valid Wireguard endpoint could be constructed from this [`WireguardConfig`]. + /// + /// # Note + /// The inner value is boxed to not bloat the size of [`Error`] due to the size of [`WireguardConfig`]. + Wireguard(Box<WireguardConfig>), + /// No valid OpenVPN endpoint could be constructed from this [`Relay`] + /// + /// # Note + /// The inner value is boxed to not bloat the size of [`Error`] due to the size of [`Relay`]. + OpenVpn(Box<Relay>), +} + +impl EndpointErrorDetails { + /// Helper function for constructing an [`Error::NoEndpoint`] from `relay`. + /// Takes care of boxing the [`WireguardConfig`] for you! + pub(crate) fn from_wireguard(relay: WireguardConfig) -> Self { + EndpointErrorDetails::Wireguard(Box::new(relay)) + } + + /// Helper function for constructing an [`Error::NoEndpoint`] from `relay`. + /// Takes care of boxing the [`Relay`] for you! + pub(crate) fn from_openvpn(relay: Relay) -> Self { + EndpointErrorDetails::OpenVpn(Box::new(relay)) + } +} diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index b255ef9105..9ac49d0b1f 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -1,2406 +1,15 @@ //! When changing relay selection, please verify if `docs/relay-selector.md` needs to be //! updated as well. -use chrono::{DateTime, Local}; -use ipnetwork::IpNetwork; -use mullvad_types::{ - custom_list::CustomListsSettings, - endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, - location::{Coordinates, Location}, - relay_constraints::{ - BridgeSettings, BridgeState, Constraint, InternalBridgeConstraints, LocationConstraint, - Match, MissingCustomBridgeSettings, ObfuscationSettings, OpenVpnConstraints, Ownership, - Providers, RelayConstraints, RelayConstraintsFormatter, RelayOverride, RelaySettings, - ResolvedBridgeSettings, ResolvedLocationConstraint, SelectedObfuscation, Set, - TransportPort, Udp2TcpObfuscationSettings, - }, - relay_list::{BridgeEndpointData, Relay, RelayEndpointData, RelayList}, - settings::Settings, - CustomTunnelEndpoint, -}; -use rand::{seq::SliceRandom, Rng}; -use std::{ - collections::HashMap, - io, - net::{IpAddr, SocketAddr}, - path::Path, - sync::{Arc, Mutex, MutexGuard}, - time::{self, SystemTime}, -}; -use talpid_types::{ - net::{ - obfuscation::ObfuscatorConfig, proxy::CustomProxy, wireguard, IpVersion, TransportProtocol, - TunnelType, - }, - ErrorExt, -}; - -use matcher::{BridgeMatcher, EndpointMatcher, OpenVpnMatcher, RelayMatcher, WireguardMatcher}; - -mod matcher; - -const DATE_TIME_FORMAT_STR: &str = "%Y-%m-%d %H:%M:%S%.3f"; - -const WIREGUARD_EXIT_PORT: Constraint<u16> = Constraint::Only(51820); -const WIREGUARD_EXIT_IP_VERSION: Constraint<IpVersion> = Constraint::Only(IpVersion::V4); - -const UDP2TCP_PORTS: [u16; 2] = [80, 5001]; - -/// Minimum number of bridges to keep for selection when filtering by distance. -const MIN_BRIDGE_COUNT: usize = 5; - -/// Max distance of bridges to consider for selection (km). -const MAX_BRIDGE_DISTANCE: f64 = 1500f64; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Failed to open relay cache file")] - OpenRelayCache(#[source] io::Error), - - #[error("Failed to write relay cache file to disk")] - WriteRelayCache(#[source] io::Error), - - #[error("No relays matching current constraints")] - NoRelay, - - #[error("No bridges matching current constraints")] - NoBridge, - - #[error("No obfuscators matching current constraints")] - NoObfuscator, - - #[error("Failure in serialization of the relay list")] - Serialize(#[source] serde_json::Error), - - #[error("Downloader already shut down")] - DownloaderShutDown, - - #[error("Invalid bridge settings")] - InvalidBridgeSettings(#[source] MissingCustomBridgeSettings), -} - -struct ParsedRelays { - last_updated: SystemTime, - parsed_list: RelayList, - original_list: RelayList, - overrides: Vec<RelayOverride>, -} - -impl ParsedRelays { - /// Return a flat iterator with all relays - pub fn relays(&self) -> impl Iterator<Item = &Relay> + Clone + '_ { - self.parsed_list.relays() - } - - pub fn update(&mut self, new_relays: RelayList) { - *self = Self::from_relay_list(new_relays, SystemTime::now(), &self.overrides); - - log::info!( - "Updated relay inventory has {} relays", - self.relays().count() - ); - } - - pub fn last_updated(&self) -> SystemTime { - self.last_updated - } - - pub fn etag(&self) -> Option<String> { - self.parsed_list.etag.clone() - } - - fn set_overrides(&mut self, new_overrides: &[RelayOverride]) { - self.parsed_list = Self::parse_relay_list(&self.original_list, new_overrides); - self.overrides = new_overrides.to_vec(); - } - - fn empty() -> Self { - ParsedRelays { - last_updated: time::UNIX_EPOCH, - parsed_list: RelayList::empty(), - original_list: RelayList::empty(), - overrides: vec![], - } - } - - /// Try to read the relays from disk, preferring the newer ones. - fn from_file( - cache_path: impl AsRef<Path>, - resource_path: impl AsRef<Path>, - overrides: &[RelayOverride], - ) -> Result<Self, Error> { - // prefer the resource path's relay list if the cached one doesn't exist or was modified - // before the resource one was created. - let cached_relays = Self::from_file_inner(cache_path, overrides); - let bundled_relays = match Self::from_file_inner(resource_path, overrides) { - Ok(bundled_relays) => bundled_relays, - Err(e) => { - log::error!("Failed to load bundled relays: {}", e); - return cached_relays; - } - }; - - if cached_relays - .as_ref() - .map(|cached| cached.last_updated > bundled_relays.last_updated) - .unwrap_or(false) - { - cached_relays - } else { - Ok(bundled_relays) - } - } - - fn from_file_inner(path: impl AsRef<Path>, overrides: &[RelayOverride]) -> Result<Self, Error> { - log::debug!("Reading relays from {}", path.as_ref().display()); - let (last_modified, file) = - Self::open_file(path.as_ref()).map_err(Error::OpenRelayCache)?; - let relay_list = - serde_json::from_reader(io::BufReader::new(file)).map_err(Error::Serialize)?; - - Ok(Self::from_relay_list(relay_list, last_modified, overrides)) - } - - fn open_file(path: &Path) -> io::Result<(SystemTime, std::fs::File)> { - let file = std::fs::File::open(path)?; - let last_modified = file.metadata()?.modified()?; - Ok((last_modified, file)) - } - - fn from_relay_list( - relay_list: RelayList, - last_updated: SystemTime, - overrides: &[RelayOverride], - ) -> Self { - ParsedRelays { - last_updated, - parsed_list: Self::parse_relay_list(&relay_list, overrides), - original_list: relay_list, - overrides: overrides.to_vec(), - } - } - - fn parse_relay_list(relay_list: &RelayList, overrides: &[RelayOverride]) -> RelayList { - let mut remaining_overrides = HashMap::new(); - for relay_override in overrides { - remaining_overrides.insert( - relay_override.hostname.to_owned(), - relay_override.to_owned(), - ); - } - - let mut parsed_list = relay_list.clone(); - - // Append data for obfuscation protocols ourselves, since the API does not provide it. - if parsed_list.wireguard.udp2tcp_ports.is_empty() { - parsed_list.wireguard.udp2tcp_ports.extend(UDP2TCP_PORTS); - } - - // Add location and override relay data - for country in &mut parsed_list.countries { - for city in &mut country.cities { - for relay in &mut city.relays { - // Append location data - relay.location = Some(Location { - country: country.name.clone(), - country_code: country.code.clone(), - city: city.name.clone(), - city_code: city.code.clone(), - latitude: city.latitude, - longitude: city.longitude, - }); - - // Append overrides - if let Some(overrides) = remaining_overrides.remove(&relay.hostname) { - overrides.apply_to_relay(relay); - } - } - } - } - - parsed_list - } -} - -#[derive(Clone)] -pub struct SelectorConfig { - pub relay_settings: RelaySettings, - pub bridge_state: BridgeState, - pub bridge_settings: BridgeSettings, - pub obfuscation_settings: ObfuscationSettings, - pub custom_lists: CustomListsSettings, - pub relay_overrides: Vec<RelayOverride>, -} - -impl Default for SelectorConfig { - fn default() -> Self { - let default_settings = Settings::default(); - SelectorConfig { - relay_settings: default_settings.relay_settings, - bridge_settings: default_settings.bridge_settings, - obfuscation_settings: default_settings.obfuscation_settings, - bridge_state: default_settings.bridge_state, - custom_lists: default_settings.custom_lists, - relay_overrides: default_settings.relay_overrides, - } - } -} - -#[derive(Clone)] -pub struct RelaySelector { - config: Arc<Mutex<SelectorConfig>>, - parsed_relays: Arc<Mutex<ParsedRelays>>, -} - -impl RelaySelector { - /// Returns a new `RelaySelector` backed by relays cached on disk. - pub fn new( - config: SelectorConfig, - resource_path: impl AsRef<Path>, - cache_path: impl AsRef<Path>, - ) -> Self { - let unsynchronized_parsed_relays = - ParsedRelays::from_file(&cache_path, &resource_path, &config.relay_overrides) - .unwrap_or_else(|error| { - log::error!( - "{}", - error.display_chain_with_msg("Unable to load cached and bundled relays") - ); - ParsedRelays::empty() - }); - log::info!( - "Initialized with {} cached relays from {}", - unsynchronized_parsed_relays.relays().count(), - DateTime::<Local>::from(unsynchronized_parsed_relays.last_updated()) - .format(DATE_TIME_FORMAT_STR) - ); - - RelaySelector { - config: Arc::new(Mutex::new(config)), - parsed_relays: Arc::new(Mutex::new(unsynchronized_parsed_relays)), - } - } - - pub fn from_list(config: SelectorConfig, relay_list: RelayList) -> Self { - RelaySelector { - parsed_relays: Arc::new(Mutex::new(ParsedRelays::from_relay_list( - relay_list, - SystemTime::now(), - &config.relay_overrides, - ))), - config: Arc::new(Mutex::new(config)), - } - } - - pub fn set_config(&mut self, config: SelectorConfig) { - self.set_overrides(&config.relay_overrides); - let mut config_mutex = self.config.lock().unwrap(); - *config_mutex = config; - } - - pub fn set_relays(&self, relays: RelayList) { - let mut parsed_relays = self.parsed_relays.lock().unwrap(); - parsed_relays.update(relays); - } - - fn set_overrides(&mut self, relay_overrides: &[RelayOverride]) { - let mut parsed_relays = self.parsed_relays.lock().unwrap(); - parsed_relays.set_overrides(relay_overrides); - } - - /// Returns all countries and cities. The cities in the object returned does not have any - /// relays in them. - pub fn get_relays(&mut self) -> RelayList { - let parsed_relays = self.parsed_relays.lock().unwrap(); - parsed_relays.original_list.clone() - } - - pub fn etag(&self) -> Option<String> { - self.parsed_relays.lock().unwrap().etag() - } - - pub fn last_updated(&self) -> SystemTime { - self.parsed_relays.lock().unwrap().last_updated() - } - - /// Returns a random relay and relay endpoint matching the current constraints. - pub fn get_relay( - &self, - retry_attempt: u32, - ) -> Result< - ( - SelectedRelay, - Option<SelectedBridge>, - Option<SelectedObfuscator>, - ), - Error, - > { - let config_mutex = self.config.lock().unwrap(); - match &config_mutex.relay_settings { - RelaySettings::CustomTunnelEndpoint(custom_relay) => { - Ok((SelectedRelay::Custom(custom_relay.clone()), None, None)) - } - RelaySettings::Normal(constraints) => { - let relay = self.get_tunnel_endpoint( - constraints, - config_mutex.bridge_state, - retry_attempt, - &config_mutex.custom_lists, - )?; - let bridge = match relay.endpoint { - MullvadEndpoint::OpenVpn(endpoint) - if endpoint.protocol == TransportProtocol::Tcp => - { - let location = relay - .exit_relay - .location - .as_ref() - .expect("Relay has no location set"); - self.get_bridge_for( - &config_mutex, - location, - retry_attempt, - &config_mutex.custom_lists, - )? - } - _ => None, - }; - let obfuscator = match relay.endpoint { - MullvadEndpoint::Wireguard(ref endpoint) => { - let obfuscator_relay = - relay.entry_relay.as_ref().unwrap_or(&relay.exit_relay); - self.get_obfuscator_inner( - &config_mutex, - obfuscator_relay, - endpoint, - retry_attempt, - )? - } - _ => None, - }; - Ok((SelectedRelay::Normal(relay), bridge, obfuscator)) - } - } - } - - /// Returns a random relay and relay endpoint matching the given constraints and with - /// preferences applied. - #[cfg_attr(target_os = "android", allow(unused_variables))] - fn get_tunnel_endpoint( - &self, - relay_constraints: &RelayConstraints, - bridge_state: BridgeState, - retry_attempt: u32, - custom_lists: &CustomListsSettings, - ) -> Result<NormalSelectedRelay, Error> { - #[cfg(target_os = "android")] - { - self.get_wireguard_endpoint(relay_constraints, retry_attempt, custom_lists) - } - - #[cfg(not(target_os = "android"))] - match relay_constraints.tunnel_protocol { - Constraint::Only(TunnelType::OpenVpn) => self.get_openvpn_endpoint( - relay_constraints, - bridge_state, - retry_attempt, - custom_lists, - ), - - Constraint::Only(TunnelType::Wireguard) => { - self.get_wireguard_endpoint(relay_constraints, retry_attempt, custom_lists) - } - Constraint::Any => self.get_any_tunnel_endpoint( - relay_constraints, - bridge_state, - retry_attempt, - custom_lists, - ), - } - } - - /// Returns the average location of relays that match the given constraints. - /// This returns none if the location is `any` or if no relays match the constraints. - pub fn get_relay_midpoint( - &self, - relay_constraints: &RelayConstraints, - custom_lists: &CustomListsSettings, - ) -> Option<Coordinates> { - if relay_constraints.location.is_any() { - return None; - } - - let (openvpn_data, wireguard_data) = { - let relays = self.parsed_relays.lock().unwrap(); - ( - relays.parsed_list.openvpn.clone(), - relays.parsed_list.wireguard.clone(), - ) - }; - - let matcher = RelayMatcher::new( - relay_constraints.clone(), - openvpn_data, - wireguard_data, - custom_lists, - ); - - let mut matching_locations: Vec<Location> = { - let parsed_relays = self.parsed_relays.lock().unwrap(); - matcher - .filter_matching_relay_list(parsed_relays.relays()) - .into_iter() - .filter_map(|relay| relay.location) - .collect() - }; - matching_locations.dedup_by(|a, b| a.has_same_city(b)); - - if matching_locations.is_empty() { - return None; - } - Some(Coordinates::midpoint(&matching_locations)) - } - - /// Returns an OpenVpn endpoint, should only ever be used when the user has specified the tunnel - /// protocol as only OpenVPN. - #[cfg_attr(target_os = "android", allow(dead_code))] - fn get_openvpn_endpoint( - &self, - relay_constraints: &RelayConstraints, - bridge_state: BridgeState, - retry_attempt: u32, - custom_lists: &CustomListsSettings, - ) -> Result<NormalSelectedRelay, Error> { - let mut relay_matcher = RelayMatcher { - locations: ResolvedLocationConstraint::from_constraint( - relay_constraints.location.clone(), - custom_lists, - ), - providers: relay_constraints.providers.clone(), - ownership: relay_constraints.ownership, - endpoint_matcher: OpenVpnMatcher::new(relay_constraints.openvpn_constraints, { - let parsed_relays = self.parsed_relays.lock().unwrap(); - parsed_relays.parsed_list.openvpn.clone() - }), - }; - - if relay_matcher.endpoint_matcher.constraints.port.is_any() - && bridge_state == BridgeState::On - { - relay_matcher.endpoint_matcher.constraints.port = Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Any, - }); - - return self.get_tunnel_endpoint_internal(&relay_matcher); - } - - let mut preferred_relay_matcher = relay_matcher.clone(); - - let (preferred_port, preferred_protocol) = - Self::preferred_openvpn_constraints(retry_attempt); - let should_try_preferred = - match &mut preferred_relay_matcher.endpoint_matcher.constraints.port { - any @ Constraint::Any => { - *any = Constraint::Only(TransportPort { - protocol: preferred_protocol, - port: preferred_port, - }); - true - } - Constraint::Only(ref mut port_constraints) - if port_constraints.protocol == preferred_protocol - && port_constraints.port.is_any() => - { - port_constraints.port = preferred_port; - true - } - _ => false, - }; - - if should_try_preferred { - self.get_tunnel_endpoint_internal(&preferred_relay_matcher) - .or_else(|_| self.get_tunnel_endpoint_internal(&relay_matcher)) - } else { - self.get_tunnel_endpoint_internal(&relay_matcher) - } - } - - fn get_wireguard_multi_hop_endpoint( - &self, - mut entry_matcher: RelayMatcher<WireguardMatcher>, - exit_locations: Constraint<LocationConstraint>, - custom_lists: &CustomListsSettings, - ) -> Result<NormalSelectedRelay, Error> { - let mut exit_matcher = RelayMatcher { - locations: ResolvedLocationConstraint::from_constraint(exit_locations, custom_lists), - providers: entry_matcher.providers.clone(), - ownership: entry_matcher.ownership, - endpoint_matcher: self.wireguard_exit_matcher(), - }; - - let (exit_relay, entry_relay, exit_endpoint, mut entry_endpoint) = - if entry_matcher.locations.is_subset(&exit_matcher.locations) { - let (entry_relay, entry_endpoint) = self.get_entry_endpoint(&entry_matcher)?; - exit_matcher.set_peer(entry_relay.clone()); - let exit_result = self.get_tunnel_endpoint_internal(&exit_matcher)?; - ( - exit_result.exit_relay, - entry_relay, - exit_result.endpoint, - entry_endpoint, - ) - } else { - let exit_result = self.get_tunnel_endpoint_internal(&exit_matcher)?; - - entry_matcher.set_peer(exit_result.exit_relay.clone()); - let (entry_relay, entry_endpoint) = self.get_entry_endpoint(&entry_matcher)?; - ( - exit_result.exit_relay, - entry_relay, - exit_result.endpoint, - entry_endpoint, - ) - }; - - Self::set_entry_peers(&exit_endpoint.unwrap_wireguard().peer, &mut entry_endpoint); - - log::info!( - "Selected entry relay {} at {} going through {} at {}", - entry_relay.hostname, - entry_endpoint.peer.endpoint.ip(), - exit_relay.hostname, - exit_endpoint.to_endpoint().address.ip(), - ); - let result = NormalSelectedRelay::wireguard_multihop_endpoint( - exit_relay, - entry_endpoint, - entry_relay, - ); - Ok(result) - } - - /// Returns a WireGuard endpoint, should only ever be used when the user has specified the - /// tunnel protocol as only WireGuard. - fn get_wireguard_endpoint( - &self, - relay_constraints: &RelayConstraints, - retry_attempt: u32, - custom_lists: &CustomListsSettings, - ) -> Result<NormalSelectedRelay, Error> { - let wg_endpoint_data = { - let parsed_relays = self.parsed_relays.lock().unwrap(); - parsed_relays.parsed_list.wireguard.clone() - }; - - // NOTE: If not using multihop then `location` is set as the only location constraint. - // If using multihop then location is the exit constraint and - // `wireguard_constraints.entry_location` is set as the entry location constraint. - if !relay_constraints.wireguard_constraints.use_multihop { - let relay_matcher = RelayMatcher { - locations: ResolvedLocationConstraint::from_constraint( - relay_constraints.location.clone(), - custom_lists, - ), - providers: relay_constraints.providers.clone(), - ownership: relay_constraints.ownership, - endpoint_matcher: WireguardMatcher::new( - relay_constraints.wireguard_constraints.clone(), - wg_endpoint_data, - ), - }; - - // Nightly clippy seems wrong about this being a redundant clone - #[allow(clippy::redundant_clone)] - let mut preferred_matcher: RelayMatcher<WireguardMatcher> = relay_matcher.clone(); - preferred_matcher.endpoint_matcher.port = preferred_matcher - .endpoint_matcher - .port - .or(Self::preferred_wireguard_port(retry_attempt)); - - self.get_tunnel_endpoint_internal(&preferred_matcher) - .or_else(|_| self.get_tunnel_endpoint_internal(&relay_matcher)) - } else { - let mut entry_relay_matcher = RelayMatcher { - locations: ResolvedLocationConstraint::from_constraint( - relay_constraints - .wireguard_constraints - .entry_location - .clone(), - custom_lists, - ), - providers: relay_constraints.providers.clone(), - ownership: relay_constraints.ownership, - endpoint_matcher: WireguardMatcher::new( - relay_constraints.wireguard_constraints.clone(), - wg_endpoint_data, - ), - }; - entry_relay_matcher.endpoint_matcher.port = entry_relay_matcher - .endpoint_matcher - .port - .or(Self::preferred_wireguard_port(retry_attempt)); - - self.get_wireguard_multi_hop_endpoint( - entry_relay_matcher, - relay_constraints.location.clone(), - custom_lists, - ) - } - } - - /// Like [Self::get_tunnel_endpoint_internal] but also selects an entry endpoint if applicable. - #[cfg_attr(target_os = "android", allow(dead_code))] - fn get_multihop_tunnel_endpoint_internal( - &self, - relay_constraints: &RelayConstraints, - custom_lists: &CustomListsSettings, - ) -> Result<NormalSelectedRelay, Error> { - let (openvpn_data, wireguard_data) = { - let relays = self.parsed_relays.lock().unwrap(); - ( - relays.parsed_list.openvpn.clone(), - relays.parsed_list.wireguard.clone(), - ) - }; - let mut matcher = RelayMatcher::new( - relay_constraints.clone(), - openvpn_data, - wireguard_data, - custom_lists, - ); - - let mut selected_entry_relay = None; - let mut selected_entry_endpoint = None; - let mut entry_matcher = RelayMatcher { - locations: ResolvedLocationConstraint::from_constraint( - relay_constraints - .wireguard_constraints - .entry_location - .clone(), - custom_lists, - ), - providers: relay_constraints.providers.clone(), - ownership: relay_constraints.ownership, - endpoint_matcher: matcher.endpoint_matcher.clone(), - } - .into_wireguard_matcher(); - - // Pick the entry relay first if its location constraint is a subset of the exit location. - if relay_constraints.wireguard_constraints.use_multihop { - matcher.endpoint_matcher.wireguard = self.wireguard_exit_matcher(); - if entry_matcher.locations.is_subset(&matcher.locations) { - if let Ok((entry_relay, entry_endpoint)) = self.get_entry_endpoint(&entry_matcher) { - matcher.endpoint_matcher.wireguard.peer = Some(entry_relay.clone()); - selected_entry_relay = Some(entry_relay); - selected_entry_endpoint = Some(entry_endpoint); - } - } - } - - let mut selected_relay = self.get_tunnel_endpoint_internal(&matcher)?; - - // Pick the entry relay last if its location constraint is NOT a subset of the exit - // location. - if matches!(selected_relay.endpoint, MullvadEndpoint::Wireguard(..)) - && relay_constraints.wireguard_constraints.use_multihop - { - if !entry_matcher.locations.is_subset(&matcher.locations) { - entry_matcher.endpoint_matcher.peer = Some(selected_relay.exit_relay.clone()); - if let Ok((entry_relay, entry_endpoint)) = self.get_entry_endpoint(&entry_matcher) { - selected_entry_relay = Some(entry_relay); - selected_entry_endpoint = Some(entry_endpoint); - } - } - - match (selected_entry_endpoint, selected_entry_relay) { - (Some(mut entry_endpoint), Some(entry_relay)) => { - Self::set_entry_peers( - &selected_relay.endpoint.unwrap_wireguard().peer, - &mut entry_endpoint, - ); - - log::info!( - "Selected entry relay {} at {} going through {} at {}", - entry_relay.hostname, - entry_endpoint.peer.endpoint.ip(), - selected_relay.exit_relay.hostname, - selected_relay.endpoint.to_endpoint().address.ip(), - ); - - selected_relay.endpoint = MullvadEndpoint::Wireguard(entry_endpoint); - selected_relay.entry_relay = Some(entry_relay); - } - _ => return Err(Error::NoRelay), - } - } - - Ok(selected_relay) - } - - /// Returns a tunnel endpoint of any type, should only be used when the user hasn't specified a - /// tunnel protocol. - #[cfg_attr(target_os = "android", allow(dead_code))] - fn get_any_tunnel_endpoint( - &self, - relay_constraints: &RelayConstraints, - bridge_state: BridgeState, - retry_attempt: u32, - custom_lists: &CustomListsSettings, - ) -> Result<NormalSelectedRelay, Error> { - let preferred_constraints = self.preferred_constraints( - relay_constraints, - bridge_state, - retry_attempt, - custom_lists, - ); - - if let Ok(result) = - self.get_multihop_tunnel_endpoint_internal(&preferred_constraints, custom_lists) - { - log::debug!( - "Relay matched on highest preference for retry attempt {}", - retry_attempt - ); - Ok(result) - } else if let Ok(result) = - self.get_multihop_tunnel_endpoint_internal(relay_constraints, custom_lists) - { - log::debug!( - "Relay matched on second preference for retry attempt {}", - retry_attempt - ); - Ok(result) - } else { - log::warn!( - "No relays matching constraints: {}", - RelayConstraintsFormatter { - constraints: relay_constraints, - custom_lists, - } - ); - Err(Error::NoRelay) - } - } - - // This function ignores the tunnel type constraint on purpose. - #[cfg_attr(target_os = "android", allow(dead_code))] - fn preferred_constraints( - &self, - original_constraints: &RelayConstraints, - bridge_state: BridgeState, - retry_attempt: u32, - custom_lists: &CustomListsSettings, - ) -> RelayConstraints { - let location = ResolvedLocationConstraint::from_constraint( - original_constraints.location.clone(), - custom_lists, - ); - let (preferred_port, preferred_protocol, preferred_tunnel) = self - .preferred_tunnel_constraints_for_location( - retry_attempt, - &location, - &original_constraints.providers, - original_constraints.ownership, - ); - - let mut relay_constraints = original_constraints.clone(); - relay_constraints.openvpn_constraints = Default::default(); - - // Highest priority preference. Where we prefer OpenVPN using UDP. But without changing - // any constraints that are explicitly specified. - match original_constraints.tunnel_protocol { - // If no tunnel protocol is selected, use preferred constraints - Constraint::Any => { - if bridge_state == BridgeState::On { - relay_constraints.openvpn_constraints = OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Any, - }), - }; - } else if original_constraints.openvpn_constraints.port.is_any() { - relay_constraints.openvpn_constraints = OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: preferred_protocol, - port: preferred_port, - }), - }; - } else { - relay_constraints.openvpn_constraints = - original_constraints.openvpn_constraints; - } - - if relay_constraints.wireguard_constraints.port.is_any() { - relay_constraints.wireguard_constraints.port = preferred_port; - } - - relay_constraints.tunnel_protocol = Constraint::Only(preferred_tunnel); - } - Constraint::Only(TunnelType::OpenVpn) => { - let openvpn_constraints = &mut relay_constraints.openvpn_constraints; - *openvpn_constraints = original_constraints.openvpn_constraints; - if bridge_state == BridgeState::On && openvpn_constraints.port.is_any() { - openvpn_constraints.port = Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Any, - }); - } else if openvpn_constraints.port.is_any() { - let (preferred_port, preferred_protocol) = - Self::preferred_openvpn_constraints(retry_attempt); - openvpn_constraints.port = Constraint::Only(TransportPort { - protocol: preferred_protocol, - port: preferred_port, - }); - } - } - Constraint::Only(TunnelType::Wireguard) => { - relay_constraints.wireguard_constraints = - original_constraints.wireguard_constraints.clone(); - if relay_constraints.wireguard_constraints.port.is_any() { - relay_constraints.wireguard_constraints.port = - Self::preferred_wireguard_port(retry_attempt); - } - } - }; - - relay_constraints - } - - fn get_entry_endpoint( - &self, - matcher: &RelayMatcher<WireguardMatcher>, - ) -> Result<(Relay, MullvadWireguardEndpoint), Error> { - let matching_relays: Vec<Relay> = { - let parsed_relays = self.parsed_relays.lock().unwrap(); - matcher - .filter_matching_relay_list(parsed_relays.relays()) - .into_iter() - .collect() - }; - - let relay = self - .pick_random_relay(&matching_relays) - .cloned() - .ok_or(Error::NoRelay)?; - let endpoint = matcher - .mullvad_endpoint(&relay) - .ok_or(Error::NoRelay)? - .unwrap_wireguard() - .clone(); - - Ok((relay, endpoint)) - } - - fn set_entry_peers( - exit_peer: &wireguard::PeerConfig, - entry_endpoint: &mut MullvadWireguardEndpoint, - ) { - entry_endpoint.peer.allowed_ips = vec![IpNetwork::from(exit_peer.endpoint.ip())]; - entry_endpoint.exit_peer = Some(exit_peer.clone()); - } - - fn get_bridge_for( - &self, - config: &MutexGuard<'_, SelectorConfig>, - location: &mullvad_types::location::Location, - retry_attempt: u32, - custom_lists: &CustomListsSettings, - ) -> Result<Option<SelectedBridge>, Error> { - match config - .bridge_settings - .resolve() - .map_err(Error::InvalidBridgeSettings)? - { - ResolvedBridgeSettings::Normal(settings) => { - let bridge_constraints = InternalBridgeConstraints { - location: settings.location.clone(), - providers: settings.providers.clone(), - ownership: settings.ownership, - // FIXME: This is temporary while talpid-core only supports TCP proxies - transport_protocol: Constraint::Only(TransportProtocol::Tcp), - }; - match config.bridge_state { - BridgeState::On => { - let (settings, relay) = self - .get_proxy_settings(&bridge_constraints, Some(location), custom_lists) - .ok_or(Error::NoBridge)?; - Ok(Some(SelectedBridge::Normal(NormalSelectedBridge { - settings, - relay, - }))) - } - BridgeState::Auto if Self::should_use_bridge(retry_attempt) => Ok(self - .get_proxy_settings(&bridge_constraints, Some(location), custom_lists) - .map(|(settings, relay)| { - SelectedBridge::Normal(NormalSelectedBridge { settings, relay }) - })), - BridgeState::Auto | BridgeState::Off => Ok(None), - } - } - ResolvedBridgeSettings::Custom(bridge_settings) => match config.bridge_state { - BridgeState::On => Ok(Some(SelectedBridge::Custom(bridge_settings.clone()))), - BridgeState::Auto if Self::should_use_bridge(retry_attempt) => { - Ok(Some(SelectedBridge::Custom(bridge_settings.clone()))) - } - BridgeState::Auto | BridgeState::Off => Ok(None), - }, - } - } - - /// Returns a non-custom bridge based on the relay and bridge constraints, ignoring the bridge - /// state. - pub fn get_bridge_forced(&self) -> Option<CustomProxy> { - let config = self.config.lock().unwrap(); - // let relay_settings = { - // let config = self.config.lock().unwrap(); - // config.relay_settings.clone() - // }; - - let near_location = match &config.relay_settings { - RelaySettings::Normal(settings) => { - let custom_lists = { - // let config = self.config.lock().unwrap(); - config.custom_lists.clone() - }; - self.get_relay_midpoint(settings, &custom_lists) - } - _ => None, - }; - let bridge_settings = &config.bridge_settings; - let constraints = match bridge_settings.resolve() { - Ok(ResolvedBridgeSettings::Normal(settings)) => InternalBridgeConstraints { - location: settings.location.clone(), - providers: settings.providers.clone(), - ownership: settings.ownership, - transport_protocol: Constraint::Only(TransportProtocol::Tcp), - }, - _ => InternalBridgeConstraints { - location: Constraint::Any, - providers: Constraint::Any, - ownership: Constraint::Any, - transport_protocol: Constraint::Only(TransportProtocol::Tcp), - }, - }; - - let custom_lists = &config.custom_lists; - self.get_proxy_settings(&constraints, near_location, custom_lists) - .map(|(settings, _relay)| settings) - } - - fn should_use_bridge(retry_attempt: u32) -> bool { - // shouldn't use a bridge for the first 3 times - retry_attempt > 3 && - // i.e. 4th and 5th with bridge, 6th & 7th without - // The test is to see whether the current _couple of connections_ is even or not. - // | retry_attempt | 4 | 5 | 6 | 7 | 8 | 9 | - // | (retry_attempt % 4) < 2 | t | t | f | f | t | t | - (retry_attempt % 4) < 2 - } - - fn get_proxy_settings<T: Into<Coordinates>>( - &self, - constraints: &InternalBridgeConstraints, - location: Option<T>, - custom_lists: &CustomListsSettings, - ) -> Option<(CustomProxy, Relay)> { - let matcher = RelayMatcher { - locations: ResolvedLocationConstraint::from_constraint( - constraints.location.clone(), - custom_lists, - ), - providers: constraints.providers.clone(), - ownership: constraints.ownership, - endpoint_matcher: BridgeMatcher(()), - }; - - let matching_relays: Vec<Relay> = { - let parsed_relays = self.parsed_relays.lock().unwrap(); - matcher.filter_matching_relay_list(parsed_relays.relays()) - }; - - if matching_relays.is_empty() { - return None; - } - - let relay = if let Some(location) = location { - let location = location.into(); - - #[derive(Debug, Clone)] - struct RelayWithDistance { - relay: Relay, - distance: f64, - } - - let mut matching_relays: Vec<RelayWithDistance> = matching_relays - .into_iter() - .map(|relay| RelayWithDistance { - distance: relay.location.as_ref().unwrap().distance_from(&location), - relay, - }) - .collect(); - matching_relays - .sort_unstable_by_key(|relay: &RelayWithDistance| relay.distance as usize); - - let mut greatest_distance = 0f64; - matching_relays = matching_relays - .into_iter() - .enumerate() - .filter_map(|(i, relay)| { - if i < MIN_BRIDGE_COUNT || relay.distance <= MAX_BRIDGE_DISTANCE { - if relay.distance > greatest_distance { - greatest_distance = relay.distance; - } - return Some(relay); - } - None - }) - .collect(); - - let weight_fn = - |relay: &RelayWithDistance| 1 + (greatest_distance - relay.distance) as u64; - - self.pick_random_relay_fn(&matching_relays, weight_fn) - .cloned() - .map(|relay_with_distance| relay_with_distance.relay) - } else { - self.pick_random_relay(&matching_relays).cloned() - }; - relay.and_then(|relay| { - let parsed_relays = self.parsed_relays.lock().unwrap(); - let bridge = &parsed_relays.parsed_list.bridge; - self.pick_random_bridge(bridge, &relay) - .map(|bridge| (bridge, relay.clone())) - }) - } - - fn get_obfuscator_inner( - &self, - config: &MutexGuard<'_, SelectorConfig>, - relay: &Relay, - endpoint: &MullvadWireguardEndpoint, - retry_attempt: u32, - ) -> Result<Option<SelectedObfuscator>, Error> { - match &config.obfuscation_settings.selected_obfuscation { - SelectedObfuscation::Auto => Ok(self.get_auto_obfuscator( - &config.obfuscation_settings, - relay, - endpoint, - retry_attempt, - )), - SelectedObfuscation::Off => Ok(None), - SelectedObfuscation::Udp2Tcp => Ok(Some( - self.get_udp2tcp_obfuscator( - &config.obfuscation_settings.udp2tcp, - relay, - endpoint, - retry_attempt, - ) - .ok_or(Error::NoObfuscator)?, - )), - } - } - - fn get_auto_obfuscator( - &self, - obfuscation_settings: &ObfuscationSettings, - relay: &Relay, - endpoint: &MullvadWireguardEndpoint, - retry_attempt: u32, - ) -> Option<SelectedObfuscator> { - let obfuscation_attempt = Self::get_auto_obfuscator_retry_attempt(retry_attempt)?; - self.get_udp2tcp_obfuscator( - &obfuscation_settings.udp2tcp, - relay, - endpoint, - obfuscation_attempt, - ) - } - - const fn get_auto_obfuscator_retry_attempt(retry_attempt: u32) -> Option<u32> { - match retry_attempt % 4 { - 0 | 1 => None, - // when the retry attempt is 2-3, 6-7, 10-11 ... obfuscation will be used - filtered_retry => Some(retry_attempt / 4 + filtered_retry - 2), - } - } - - fn get_udp2tcp_obfuscator( - &self, - obfuscation_settings: &Udp2TcpObfuscationSettings, - relay: &Relay, - endpoint: &MullvadWireguardEndpoint, - retry_attempt: u32, - ) -> Option<SelectedObfuscator> { - let udp2tcp_ports = { - &self - .parsed_relays - .lock() - .unwrap() - .parsed_list - .wireguard - .udp2tcp_ports - }; - let udp2tcp_endpoint = if obfuscation_settings.port.is_only() { - udp2tcp_ports - .iter() - .find(|&candidate| obfuscation_settings.port == Constraint::Only(*candidate)) - } else { - udp2tcp_ports.get(retry_attempt as usize % udp2tcp_ports.len()) - }; - udp2tcp_endpoint - .map(|udp2tcp_endpoint| ObfuscatorConfig::Udp2Tcp { - endpoint: SocketAddr::new(endpoint.peer.endpoint.ip(), *udp2tcp_endpoint), - }) - .map(|config| SelectedObfuscator { - config, - relay: relay.clone(), - }) - } - - /// Return the preferred constraints, on attempt `retry_attempt`, for matching locations - fn preferred_tunnel_constraints_for_location( - &self, - retry_attempt: u32, - location: &Constraint<ResolvedLocationConstraint>, - providers: &Constraint<Providers>, - ownership: Constraint<Ownership>, - ) -> (Constraint<u16>, TransportProtocol, TunnelType) { - let (location_supports_wg, location_supports_openvpn) = { - let parsed_relays = self.parsed_relays.lock().unwrap(); - let mut active_location_relays = parsed_relays.relays().filter(|relay| { - relay.active - && location.matches_with_opts(relay, true) - && providers.matches(relay) - && ownership.matches(relay) - }); - let location_supports_wg = active_location_relays - .clone() - .any(|relay| matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_))); - let location_supports_openvpn = active_location_relays - .any(|relay| matches!(relay.endpoint_data, RelayEndpointData::Openvpn)); +mod constants; +mod error; +#[cfg_attr(target_os = "android", allow(unused))] +mod relay_selector; - (location_supports_wg, location_supports_openvpn) - }; - match (location_supports_wg, location_supports_openvpn) { - (true, true) | (false, false) => Self::preferred_tunnel_constraints(retry_attempt), - (true, false) => { - let port = Self::preferred_wireguard_port(retry_attempt); - (port, TransportProtocol::Udp, TunnelType::Wireguard) - } - (false, true) => { - let (port, transport) = Self::preferred_openvpn_constraints(retry_attempt); - (port, transport, TunnelType::OpenVpn) - } - } - } - - /// Return the preferred constraints, on attempt `retry_attempt`, given no other constraints - pub const fn preferred_tunnel_constraints( - retry_attempt: u32, - ) -> (Constraint<u16>, TransportProtocol, TunnelType) { - // Use WireGuard on the first three attempts, then OpenVPN - match retry_attempt { - 0..=2 => ( - Self::preferred_wireguard_port(retry_attempt), - TransportProtocol::Udp, - TunnelType::Wireguard, - ), - _ => { - let (preferred_port, preferred_protocol) = - Self::preferred_openvpn_constraints(retry_attempt - 2); - (preferred_port, preferred_protocol, TunnelType::OpenVpn) - } - } - } - - const fn preferred_wireguard_port(retry_attempt: u32) -> Constraint<u16> { - // Alternate between using a random port and port 53 - if retry_attempt % 2 == 0 { - Constraint::Any - } else { - Constraint::Only(53) - } - } - - const fn preferred_openvpn_constraints( - retry_attempt: u32, - ) -> (Constraint<u16>, TransportProtocol) { - // Prefer UDP by default. But if that has failed a couple of times, then try TCP port - // 443, which works for many with UDP problems. After that, just alternate - // between protocols. - // If the tunnel type constraint is set OpenVpn, from the 4th attempt onwards, the first - // two retry attempts OpenVpn constraints should be set to TCP as a bridge will be used, - // and to UDP or TCP for the next two attempts. - match retry_attempt { - 0 | 1 => (Constraint::Any, TransportProtocol::Udp), - 2 | 3 => (Constraint::Only(443), TransportProtocol::Tcp), - attempt if attempt % 4 < 2 => (Constraint::Any, TransportProtocol::Tcp), - attempt if attempt % 4 == 2 => (Constraint::Any, TransportProtocol::Udp), - _ => (Constraint::Any, TransportProtocol::Tcp), - } - } - - /// Returns a random relay endpoint if any is matching the given constraints. - fn get_tunnel_endpoint_internal<T: EndpointMatcher>( - &self, - matcher: &RelayMatcher<T>, - ) -> Result<NormalSelectedRelay, Error> { - let matching_relays: Vec<Relay> = { - let parsed_relays = self.parsed_relays.lock().unwrap(); - matcher - .filter_matching_relay_list(parsed_relays.relays()) - .into_iter() - .collect() - }; - - self.pick_random_relay(&matching_relays) - .and_then(|selected_relay| { - let endpoint = matcher.mullvad_endpoint(selected_relay); - let addr_in = endpoint - .as_ref() - .map(|endpoint| endpoint.to_endpoint().address.ip()) - .unwrap_or_else(|| IpAddr::from(selected_relay.ipv4_addr_in)); - log::info!("Selected relay {} at {}", selected_relay.hostname, addr_in); - endpoint.map(|endpoint| NormalSelectedRelay::new(endpoint, selected_relay.clone())) - }) - .ok_or(Error::NoRelay) - } - - /// Picks a relay using [Self::pick_random_relay_fn], using the `weight` member of each relay - /// as the weight function. - fn pick_random_relay<'a>(&self, relays: &'a [Relay]) -> Option<&'a Relay> { - self.pick_random_relay_fn(relays, |relay| relay.weight) - } - - /// Pick a random relay from the given slice. Will return `None` if the given slice is empty. - /// If all of the relays have a weight of 0, one will be picked at random without bias, - /// otherwise roulette wheel selection will be used to pick only relays with non-zero - /// weights. - fn pick_random_relay_fn<'a, RelayType>( - &self, - relays: &'a [RelayType], - weight_fn: impl Fn(&RelayType) -> u64, - ) -> Option<&'a RelayType> { - let total_weight: u64 = relays.iter().map(&weight_fn).sum(); - let mut rng = rand::thread_rng(); - if total_weight == 0 { - relays.choose(&mut rng) - } else { - // Pick a random number in the range 1..=total_weight. This choses the relay with a - // non-zero weight. - let mut i: u64 = rng.gen_range(1..=total_weight); - Some( - relays - .iter() - .find(|relay| { - i = i.saturating_sub(weight_fn(relay)); - i == 0 - }) - .expect("At least one relay must've had a weight above 0"), - ) - } - } - - /// Picks a random bridge from a relay. - fn pick_random_bridge(&self, data: &BridgeEndpointData, relay: &Relay) -> Option<CustomProxy> { - if relay.endpoint_data != RelayEndpointData::Bridge { - return None; - } - data.shadowsocks - .choose(&mut rand::thread_rng()) - .map(|shadowsocks_endpoint| { - log::info!( - "Selected Shadowsocks bridge {} at {}:{}/{}", - relay.hostname, - relay.ipv4_addr_in, - shadowsocks_endpoint.port, - shadowsocks_endpoint.protocol - ); - shadowsocks_endpoint.to_proxy_settings(relay.ipv4_addr_in.into()) - }) - } - - fn wireguard_exit_matcher(&self) -> WireguardMatcher { - let wg = { - self.parsed_relays - .lock() - .unwrap() - .parsed_list - .wireguard - .clone() - }; - let mut tunnel = WireguardMatcher::from_endpoint(wg); - tunnel.ip_version = WIREGUARD_EXIT_IP_VERSION; - tunnel.port = WIREGUARD_EXIT_PORT; - tunnel - } -} - -#[derive(Debug)] -pub enum SelectedBridge { - Normal(NormalSelectedBridge), - Custom(CustomProxy), -} - -#[derive(Debug)] -pub struct NormalSelectedBridge { - pub settings: CustomProxy, - pub relay: Relay, -} - -#[derive(Debug)] -pub enum SelectedRelay { - Normal(NormalSelectedRelay), - Custom(CustomTunnelEndpoint), -} - -#[derive(Debug)] -pub struct NormalSelectedRelay { - pub exit_relay: Relay, - pub endpoint: MullvadEndpoint, - pub entry_relay: Option<Relay>, -} - -#[derive(Debug)] -pub struct SelectedObfuscator { - pub config: ObfuscatorConfig, - pub relay: Relay, -} - -impl NormalSelectedRelay { - fn new(endpoint: MullvadEndpoint, exit_relay: Relay) -> Self { - Self { - exit_relay, - endpoint, - entry_relay: None, - } - } - - fn wireguard_multihop_endpoint( - exit_relay: Relay, - endpoint: MullvadWireguardEndpoint, - entry: Relay, - ) -> Self { - Self { - exit_relay, - endpoint: MullvadEndpoint::Wireguard(endpoint), - entry_relay: Some(entry), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use mullvad_types::{ - relay_constraints::{GeographicLocationConstraint, WireguardConstraints}, - relay_list::{ - OpenVpnEndpoint, OpenVpnEndpointData, RelayListCity, RelayListCountry, - ShadowsocksEndpointData, WireguardEndpointData, WireguardRelayEndpointData, - }, - }; - use once_cell::sync::Lazy; - use std::collections::HashSet; - use talpid_types::net::{wireguard::PublicKey, Endpoint}; - - impl RelaySelector { - fn get_obfuscator( - &self, - relay: &Relay, - endpoint: &MullvadWireguardEndpoint, - retry_attempt: u32, - ) -> Result<Option<SelectedObfuscator>, Error> { - self.get_obfuscator_inner(&self.config.lock().unwrap(), relay, endpoint, retry_attempt) - } - } - - static RELAYS: Lazy<RelayList> = Lazy::new(|| RelayList { - etag: None, - countries: vec![RelayListCountry { - name: "Sweden".to_string(), - code: "se".to_string(), - cities: vec![RelayListCity { - name: "Gothenburg".to_string(), - code: "got".to_string(), - latitude: 57.70887, - longitude: 11.97456, - relays: vec![ - Relay { - hostname: "se9-wireguard".to_string(), - ipv4_addr_in: "185.213.154.68".parse().unwrap(), - ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), - include_in_country: true, - active: true, - owned: true, - provider: "provider0".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - }), - location: None, - }, - Relay { - hostname: "se10-wireguard".to_string(), - ipv4_addr_in: "185.213.154.69".parse().unwrap(), - ipv6_addr_in: Some("2a03:1b20:5:f011::a10f".parse().unwrap()), - include_in_country: true, - active: true, - owned: false, - provider: "provider1".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - }), - location: None, - }, - Relay { - hostname: "se-got-001".to_string(), - ipv4_addr_in: "185.213.154.131".parse().unwrap(), - ipv6_addr_in: None, - include_in_country: true, - active: true, - owned: true, - provider: "provider2".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Openvpn, - location: None, - }, - Relay { - hostname: "se-got-002".to_string(), - ipv4_addr_in: "1.2.3.4".parse().unwrap(), - ipv6_addr_in: None, - include_in_country: true, - active: true, - owned: true, - provider: "provider0".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Openvpn, - location: None, - }, - Relay { - hostname: "se-got-br-001".to_string(), - ipv4_addr_in: "1.3.3.7".parse().unwrap(), - ipv6_addr_in: None, - include_in_country: true, - active: true, - owned: true, - provider: "provider3".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Bridge, - location: None, - }, - ], - }], - }], - openvpn: OpenVpnEndpointData { - ports: vec![ - OpenVpnEndpoint { - port: 1194, - protocol: TransportProtocol::Udp, - }, - OpenVpnEndpoint { - port: 443, - protocol: TransportProtocol::Tcp, - }, - OpenVpnEndpoint { - port: 80, - protocol: TransportProtocol::Tcp, - }, - ], - }, - bridge: BridgeEndpointData { - shadowsocks: vec![ - ShadowsocksEndpointData { - port: 443, - cipher: "aes-256-gcm".to_string(), - password: "mullvad".to_string(), - protocol: TransportProtocol::Tcp, - }, - ShadowsocksEndpointData { - port: 1234, - cipher: "aes-256-cfb".to_string(), - password: "mullvad".to_string(), - protocol: TransportProtocol::Udp, - }, - ShadowsocksEndpointData { - port: 1236, - cipher: "aes-256-gcm".to_string(), - password: "mullvad".to_string(), - protocol: TransportProtocol::Udp, - }, - ], - }, - wireguard: WireguardEndpointData { - port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)], - ipv4_gateway: "10.64.0.1".parse().unwrap(), - ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), - udp2tcp_ports: vec![], - }, - }); - - #[test] - fn test_preferred_tunnel_protocol() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - // Prefer WG if the location only supports it - let location = GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - "se9-wireguard".to_string(), - ); - let relay_constraints = RelayConstraints { - location: Constraint::Only(LocationConstraint::from(location)), - tunnel_protocol: Constraint::Any, - ..RelayConstraints::default() - }; - - let preferred = relay_selector.preferred_constraints( - &relay_constraints, - BridgeState::Off, - 0, - &CustomListsSettings::default(), - ); - assert_eq!( - preferred.tunnel_protocol, - Constraint::Only(TunnelType::Wireguard) - ); - - for attempt in 0..10 { - assert!(relay_selector - .get_any_tunnel_endpoint( - &relay_constraints, - BridgeState::Off, - attempt, - &CustomListsSettings::default() - ) - .is_ok()); - } - - // Prefer OpenVPN if the location only supports it - let location = GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - "se-got-001".to_string(), - ); - let relay_constraints = RelayConstraints { - location: Constraint::Only(LocationConstraint::from(location)), - tunnel_protocol: Constraint::Any, - ..RelayConstraints::default() - }; - - let preferred = relay_selector.preferred_constraints( - &relay_constraints, - BridgeState::Off, - 0, - &CustomListsSettings::default(), - ); - assert_eq!( - preferred.tunnel_protocol, - Constraint::Only(TunnelType::OpenVpn) - ); - - for attempt in 0..10 { - assert!(relay_selector - .get_any_tunnel_endpoint( - &relay_constraints, - BridgeState::Off, - attempt, - &CustomListsSettings::default() - ) - .is_ok()); - } - } - - #[test] - fn test_wg_entry_hostname_collision() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let location1 = GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - "se9-wireguard".to_string(), - ); - let location2 = GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - "se10-wireguard".to_string(), - ); - - let mut relay_constraints = RelayConstraints { - location: Constraint::Only(LocationConstraint::from(location1.clone())), - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - ..RelayConstraints::default() - }; - - relay_constraints.wireguard_constraints.use_multihop = true; - relay_constraints.wireguard_constraints.entry_location = - Constraint::Only(LocationConstraint::from(location1)); - - // The same host cannot be used for entry and exit - assert!(relay_selector - .get_tunnel_endpoint( - &relay_constraints, - BridgeState::Off, - 0, - &CustomListsSettings::default() - ) - .is_err()); - - relay_constraints.wireguard_constraints.entry_location = - Constraint::Only(LocationConstraint::from(location2)); - - // If the entry and exit differ, this should succeed - assert!(relay_selector - .get_tunnel_endpoint( - &relay_constraints, - BridgeState::Off, - 0, - &CustomListsSettings::default() - ) - .is_ok()); - } - - #[test] - fn test_wg_entry_filter() -> Result<(), String> { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let specific_hostname = "se10-wireguard"; - - let location_general = LocationConstraint::from(GeographicLocationConstraint::City( - "se".to_string(), - "got".to_string(), - )); - let location_specific = LocationConstraint::from(GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - specific_hostname.to_string(), - )); - - let mut relay_constraints = RelayConstraints { - location: Constraint::Only(location_general.clone()), - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - ..RelayConstraints::default() - }; - - relay_constraints.wireguard_constraints.use_multihop = true; - relay_constraints.wireguard_constraints.entry_location = - Constraint::Only(location_specific.clone()); - - // The exit must not equal the entry - let exit_relay = relay_selector - .get_tunnel_endpoint( - &relay_constraints, - BridgeState::Off, - 0, - &CustomListsSettings::default(), - ) - .map_err(|error| error.to_string())? - .exit_relay; - - assert_ne!(exit_relay.hostname, specific_hostname); - - relay_constraints.location = Constraint::Only(location_specific); - relay_constraints.wireguard_constraints.entry_location = Constraint::Only(location_general); - - // The entry must not equal the exit - let NormalSelectedRelay { - exit_relay, - endpoint, - .. - } = relay_selector - .get_tunnel_endpoint( - &relay_constraints, - BridgeState::Off, - 0, - &CustomListsSettings::default(), - ) - .map_err(|error| error.to_string())?; - - assert_eq!(exit_relay.hostname, specific_hostname); - - let endpoint = endpoint.unwrap_wireguard(); - assert_eq!( - exit_relay.ipv4_addr_in, - endpoint.exit_peer.as_ref().unwrap().endpoint.ip() - ); - assert_ne!(exit_relay.ipv4_addr_in, endpoint.peer.endpoint.ip()); - - Ok(()) - } - - #[test] - fn test_openvpn_constraints() -> Result<(), String> { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - const ACTUAL_TCP_PORT: u16 = 443; - const ACTUAL_UDP_PORT: u16 = 1194; - const NON_EXISTENT_PORT: u16 = 1337; - - // Test all combinations of constraints, and whether they should - // match some relay - const CONSTRAINT_COMBINATIONS: [(OpenVpnConstraints, bool); 7] = [ - ( - OpenVpnConstraints { - port: Constraint::Any, - }, - true, - ), - ( - OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Udp, - port: Constraint::Any, - }), - }, - true, - ), - ( - OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Any, - }), - }, - true, - ), - ( - OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Udp, - port: Constraint::Only(ACTUAL_UDP_PORT), - }), - }, - true, - ), - ( - OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Udp, - port: Constraint::Only(NON_EXISTENT_PORT), - }), - }, - false, - ), - ( - OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Only(ACTUAL_TCP_PORT), - }), - }, - true, - ), - ( - OpenVpnConstraints { - port: Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Only(NON_EXISTENT_PORT), - }), - }, - false, - ), - ]; - - let matches_constraints = - |endpoint: Endpoint, constraints: &OpenVpnConstraints| match constraints.port { - Constraint::Any => true, - Constraint::Only(TransportPort { protocol, port }) => { - if endpoint.protocol != protocol { - return false; - } - match port { - Constraint::Any => true, - Constraint::Only(port) => port == endpoint.address.port(), - } - } - }; - - let mut relay_constraints = RelayConstraints { - tunnel_protocol: Constraint::Only(TunnelType::OpenVpn), - ..RelayConstraints::default() - }; - - for (openvpn_constraints, should_match) in &CONSTRAINT_COMBINATIONS { - relay_constraints.openvpn_constraints = *openvpn_constraints; - - for retry_attempt in 0..10 { - let relay = relay_selector.get_tunnel_endpoint( - &relay_constraints, - BridgeState::Auto, - retry_attempt, - &CustomListsSettings::default(), - ); - - println!("relay: {relay:?}, constraints: {relay_constraints:?}"); - - if !should_match { - relay.expect_err("unexpected relay"); - continue; - } - - let relay = relay.expect("expected to find a relay"); - - assert!( - matches_constraints( - relay.endpoint.to_endpoint(), - &relay_constraints.openvpn_constraints, - ), - "{relay:?}, on attempt {retry_attempt}, did not match constraints: {relay_constraints:?}" - ); - } - } - - Ok(()) - } - - #[test] - fn test_bridge_constraints() -> Result<(), String> { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let location = LocationConstraint::from(GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - "se-got-001".to_string(), - )); - let mut relay_constraints = RelayConstraints { - location: Constraint::Only(location), - tunnel_protocol: Constraint::Any, - ..RelayConstraints::default() - }; - relay_constraints.openvpn_constraints.port = Constraint::Only(TransportPort { - protocol: TransportProtocol::Udp, - port: Constraint::Any, - }); - - let preferred = relay_selector.preferred_constraints( - &relay_constraints, - BridgeState::On, - 0, - &CustomListsSettings::default(), - ); - assert_eq!( - preferred.tunnel_protocol, - Constraint::Only(TunnelType::OpenVpn) - ); - // NOTE: TCP is preferred for bridges - assert_eq!( - preferred.openvpn_constraints.port, - Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Any, - }) - ); - - // Ignore bridge state where WireGuard is used - let location = LocationConstraint::from(GeographicLocationConstraint::Hostname( - "se".to_string(), - "got".to_string(), - "se10-wireguard".to_string(), - )); - let relay_constraints = RelayConstraints { - location: Constraint::Only(location), - tunnel_protocol: Constraint::Any, - ..RelayConstraints::default() - }; - let preferred = relay_selector.preferred_constraints( - &relay_constraints, - BridgeState::On, - 0, - &CustomListsSettings::default(), - ); - assert_eq!( - preferred.tunnel_protocol, - Constraint::Only(TunnelType::Wireguard) - ); - - // Handle bridge setting when falling back on OpenVPN - let mut relay_constraints = RelayConstraints { - location: Constraint::Any, - tunnel_protocol: Constraint::Any, - ..RelayConstraints::default() - }; - relay_constraints.openvpn_constraints.port = Constraint::Only(TransportPort { - protocol: TransportProtocol::Udp, - port: Constraint::Any, - }); - let preferred = relay_selector.preferred_constraints( - &relay_constraints, - BridgeState::On, - 0, - &CustomListsSettings::default(), - ); - assert_eq!( - preferred.tunnel_protocol, - Constraint::Only(TunnelType::Wireguard) - ); - let preferred = relay_selector.preferred_constraints( - &relay_constraints, - BridgeState::On, - 3, - &CustomListsSettings::default(), - ); - assert_eq!( - preferred.tunnel_protocol, - Constraint::Only(TunnelType::OpenVpn) - ); - assert_eq!( - preferred.openvpn_constraints.port, - Constraint::Only(TransportPort { - protocol: TransportProtocol::Tcp, - port: Constraint::Any, - }) - ); - - Ok(()) - } - - #[test] - fn test_selecting_any_relay_will_consider_multihop() { - let relay_constraints = RelayConstraints { - wireguard_constraints: WireguardConstraints { - use_multihop: true, - ..WireguardConstraints::default() - }, - // This has to be explicit otherwise Android will chose WireGuard when default - // constructing. - tunnel_protocol: Constraint::Any, - ..RelayConstraints::default() - }; - - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let result = relay_selector.get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, &CustomListsSettings::default()) - .expect("Failed to get relay when tunnel constraints are set to Any and retrying the selection"); - - assert!( - matches!(result.endpoint, MullvadEndpoint::Wireguard(_)) - && result.entry_relay.is_some() - ); - } - - const WIREGUARD_MULTIHOP_CONSTRAINTS: RelayConstraints = RelayConstraints { - location: Constraint::Any, - providers: Constraint::Any, - ownership: Constraint::Any, - wireguard_constraints: WireguardConstraints { - use_multihop: true, - port: Constraint::Any, - ip_version: Constraint::Any, - entry_location: Constraint::Any, - }, - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - openvpn_constraints: OpenVpnConstraints { - port: Constraint::Any, - }, - }; - - const WIREGUARD_SINGLEHOP_CONSTRAINTS: RelayConstraints = RelayConstraints { - location: Constraint::Any, - providers: Constraint::Any, - ownership: Constraint::Any, - wireguard_constraints: WireguardConstraints { - use_multihop: false, - port: Constraint::Any, - ip_version: Constraint::Any, - entry_location: Constraint::Any, - }, - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - openvpn_constraints: OpenVpnConstraints { - port: Constraint::Any, - }, - }; - - #[test] - fn test_selecting_wireguard_location_will_consider_multihop() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_MULTIHOP_CONSTRAINTS, BridgeState::Off, 0, &CustomListsSettings::default()) - .expect("Failed to get relay when tunnel constraints are set to default WireGuard multihop constraints"); - - assert!(result.entry_relay.is_some()); - // TODO: Verify that neither endpoint is using obfuscation for retry attempt 0 - } - - #[test] - fn test_selecting_wg_endpoint_with_udp2tcp_obfuscation() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_SINGLEHOP_CONSTRAINTS, BridgeState::Off, 0, &CustomListsSettings::default()) - .expect("Failed to get relay when tunnel constraints are set to default WireGuard constraints"); - - assert!(result.entry_relay.is_none()); - assert!(matches!(result.endpoint, MullvadEndpoint::Wireguard { .. })); - - { - relay_selector.config.lock().unwrap().obfuscation_settings = ObfuscationSettings { - selected_obfuscation: SelectedObfuscation::Udp2Tcp, - ..ObfuscationSettings::default() - }; - } - - let obfs_config = relay_selector - .get_obfuscator(&result.exit_relay, result.endpoint.unwrap_wireguard(), 0) - .unwrap() - .unwrap(); - - assert!(matches!( - obfs_config, - SelectedObfuscator { - config: ObfuscatorConfig::Udp2Tcp { .. }, - .. - } - )); - } - - #[test] - fn test_selecting_wg_endpoint_with_auto_obfuscation() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_SINGLEHOP_CONSTRAINTS, BridgeState::Off, 0, &CustomListsSettings::default()) - .expect("Failed to get relay when tunnel constraints are set to default WireGuard constraints"); - - assert!(result.entry_relay.is_none()); - assert!(matches!(result.endpoint, MullvadEndpoint::Wireguard { .. })); - - { - relay_selector.config.lock().unwrap().obfuscation_settings = ObfuscationSettings { - selected_obfuscation: SelectedObfuscation::Auto, - ..ObfuscationSettings::default() - }; - } - - assert!(relay_selector - .get_obfuscator(&result.exit_relay, result.endpoint.unwrap_wireguard(), 0,) - .unwrap() - .is_none()); - - assert!(relay_selector - .get_obfuscator(&result.exit_relay, result.endpoint.unwrap_wireguard(), 1,) - .unwrap() - .is_none()); - - assert!(relay_selector - .get_obfuscator(&result.exit_relay, result.endpoint.unwrap_wireguard(), 2,) - .unwrap() - .is_some()); - } - - #[test] - fn test_selected_endpoints_use_correct_port_ranges() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - const TCP2UDP_PORTS: [u16; 3] = [80, 443, 5001]; - - { - relay_selector.config.lock().unwrap().obfuscation_settings = ObfuscationSettings { - selected_obfuscation: SelectedObfuscation::Udp2Tcp, - ..ObfuscationSettings::default() - }; - } - - for attempt in 0..1000 { - let result = relay_selector - .get_tunnel_endpoint( - &WIREGUARD_SINGLEHOP_CONSTRAINTS, - BridgeState::Off, - attempt, - &CustomListsSettings::default(), - ) - .expect("Failed to select a WireGuard relay"); - assert!(result.entry_relay.is_none()); - - let obfs_config = relay_selector - .get_obfuscator( - &result.exit_relay, - result.endpoint.unwrap_wireguard(), - attempt, - ) - .unwrap() - .expect("Failed to get Tcp2Udp endpoint"); - - assert!(matches!( - obfs_config, - SelectedObfuscator { - config: ObfuscatorConfig::Udp2Tcp { .. }, - .. - } - )); - - let SelectedObfuscator { - config: ObfuscatorConfig::Udp2Tcp { endpoint }, - .. - } = obfs_config; - assert!(TCP2UDP_PORTS.contains(&endpoint.port())); - } - } - - #[test] - fn test_ownership() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - let mut constraints = RelayConstraints::default(); - for i in 0..10 { - constraints.ownership = Constraint::Only(Ownership::MullvadOwned); - let relay = relay_selector - .get_tunnel_endpoint( - &constraints, - BridgeState::Auto, - i, - &CustomListsSettings::default(), - ) - .unwrap(); - assert!(matches!( - relay, - NormalSelectedRelay { - exit_relay: Relay { owned: true, .. }, - .. - } - )); - - constraints.ownership = Constraint::Only(Ownership::Rented); - let relay = relay_selector - .get_tunnel_endpoint( - &constraints, - BridgeState::Auto, - i, - &CustomListsSettings::default(), - ) - .unwrap(); - assert!(matches!( - relay, - NormalSelectedRelay { - exit_relay: Relay { owned: false, .. }, - .. - } - )); - } - } - - // Make sure server and port selection varies between retry attempts. - #[test] - fn test_load_balancing() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - for tunnel_protocol in [ - Constraint::Any, - Constraint::Only(TunnelType::Wireguard), - Constraint::Only(TunnelType::OpenVpn), - ] { - { - let mut config = relay_selector.config.lock().unwrap(); - config.relay_settings = RelaySettings::Normal(RelayConstraints { - tunnel_protocol, - location: Constraint::Only(LocationConstraint::from( - GeographicLocationConstraint::Country("se".to_string()), - )), - ..RelayConstraints::default() - }); - } - - let mut actual_ports = HashSet::new(); - let mut actual_ips = HashSet::new(); - - for retry_attempt in 0..30 { - let (relay, ..) = relay_selector.get_relay(retry_attempt).unwrap(); - match relay { - SelectedRelay::Normal(relay) => { - let address = relay.endpoint.to_endpoint().address; - actual_ports.insert(address.port()); - actual_ips.insert(address.ip()); - } - SelectedRelay::Custom(_) => unreachable!("not using custom relay"), - } - } - - assert!( - actual_ports.len() > 1, - "expected more than 1 port, got {actual_ports:?}, for tunnel protocol {tunnel_protocol:?}", - ); - assert!( - actual_ips.len() > 1, - "expected more than 1 server, got {actual_ips:?}, for tunnel protocol {tunnel_protocol:?}", - ); - } - } - - #[test] - fn test_providers() { - const EXPECTED_PROVIDERS: [&str; 2] = ["provider0", "provider2"]; - - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - let mut constraints = RelayConstraints::default(); - - for i in 0..10 { - constraints.providers = Constraint::Only( - Providers::new(EXPECTED_PROVIDERS.into_iter().map(|p| p.to_owned())).unwrap(), - ); - let relay = relay_selector - .get_tunnel_endpoint( - &constraints, - BridgeState::Auto, - i, - &CustomListsSettings::default(), - ) - .unwrap(); - assert!( - EXPECTED_PROVIDERS.contains(&relay.exit_relay.provider.as_str()), - "cannot find provider {} in {:?}", - relay.exit_relay.provider, - EXPECTED_PROVIDERS - ); - } - } - - /// Verify that bridges are automatically used when bridge mode is set - /// to automatic. - #[test] - fn test_auto_bridge() { - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); - - { - let mut config = relay_selector.config.lock().unwrap(); - config.bridge_state = BridgeState::Auto; - } - - const ATTEMPT_SHOULD_USE_BRIDGE: [bool; 5] = [false, false, false, false, true]; - - for (i, should_use_bridge) in ATTEMPT_SHOULD_USE_BRIDGE.iter().enumerate() { - let (_relay, bridge, _obfs) = relay_selector.get_relay(i as u32).unwrap(); - assert_eq!(*should_use_bridge, bridge.is_some()); - } - - // Verify that bridges are ignored when tunnel protocol is WireGuard - { - let mut config = relay_selector.config.lock().unwrap(); - config.relay_settings = RelaySettings::Normal(RelayConstraints { - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - ..RelayConstraints::default() - }); - } - for i in 0..20 { - let (_relay, bridge, _obfs) = relay_selector.get_relay(i).unwrap(); - assert!(bridge.is_none()); - } - } - - /// Ensure that `include_in_country` is ignored if all relays have it set to false (i.e., some - /// relay is returned). Also ensure that `include_in_country` is respected if some relays - /// have it set to true (i.e., that relay is never returned) - #[test] - fn test_include_in_country() { - let mut relay_list = RelayList { - etag: None, - countries: vec![RelayListCountry { - name: "Sweden".to_string(), - code: "se".to_string(), - cities: vec![RelayListCity { - name: "Gothenburg".to_string(), - code: "got".to_string(), - latitude: 57.70887, - longitude: 11.97456, - relays: vec![ - Relay { - hostname: "se9-wireguard".to_string(), - ipv4_addr_in: "185.213.154.68".parse().unwrap(), - ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), - include_in_country: false, - active: true, - owned: true, - provider: "31173".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Wireguard( - WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - }, - ), - location: None, - }, - Relay { - hostname: "se10-wireguard".to_string(), - ipv4_addr_in: "185.213.154.69".parse().unwrap(), - ipv6_addr_in: Some("2a03:1b20:5:f011::a10f".parse().unwrap()), - include_in_country: false, - active: true, - owned: false, - provider: "31173".to_string(), - weight: 1, - endpoint_data: RelayEndpointData::Wireguard( - WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - }, - ), - location: None, - }, - ], - }], - }], - openvpn: OpenVpnEndpointData { - ports: vec![ - OpenVpnEndpoint { - port: 1194, - protocol: TransportProtocol::Udp, - }, - OpenVpnEndpoint { - port: 443, - protocol: TransportProtocol::Tcp, - }, - OpenVpnEndpoint { - port: 80, - protocol: TransportProtocol::Tcp, - }, - ], - }, - bridge: BridgeEndpointData { - shadowsocks: vec![], - }, - wireguard: WireguardEndpointData { - port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)], - ipv4_gateway: "10.64.0.1".parse().unwrap(), - ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), - udp2tcp_ports: vec![], - }, - }; - - // 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).is_ok()); - - // If include_in_country is true for some relay, it must always be selected. - // - - relay_list.countries[0].cities[0].relays[0].include_in_country = true; - let expected_hostname = relay_list.countries[0].cities[0].relays[0].hostname.clone(); - - let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list); - let (relay, ..) = relay_selector.get_relay(0).expect("expected match"); - - assert!( - matches!( - relay, - SelectedRelay::Normal(NormalSelectedRelay { - exit_relay: Relay { - ref hostname, - .. - }, - .. - }) if hostname == &expected_hostname, - ), - "found {relay:?}, expected {expected_hostname:?}", - ) - } -} +// Re-exports +pub use error::Error; +pub use relay_selector::detailer; +pub use relay_selector::{ + query, GetRelay, RelaySelector, RuntimeParameters, SelectedBridge, SelectedObfuscator, + SelectorConfig, WireguardConfig, RETRY_ORDER, +}; diff --git a/mullvad-relay-selector/src/matcher.rs b/mullvad-relay-selector/src/matcher.rs deleted file mode 100644 index e02d8abc45..0000000000 --- a/mullvad-relay-selector/src/matcher.rs +++ /dev/null @@ -1,341 +0,0 @@ -use crate::CustomListsSettings; -use mullvad_types::{ - endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, - relay_constraints::{ - Constraint, Match, OpenVpnConstraints, Ownership, Providers, RelayConstraints, - ResolvedLocationConstraint, WireguardConstraints, - }, - relay_list::{ - OpenVpnEndpoint, OpenVpnEndpointData, Relay, RelayEndpointData, WireguardEndpointData, - }, -}; -use rand::{ - seq::{IteratorRandom, SliceRandom}, - Rng, -}; -use std::net::{IpAddr, SocketAddr}; -use talpid_types::net::{all_of_the_internet, wireguard, Endpoint, IpVersion, TunnelType}; - -#[derive(Clone)] -pub struct RelayMatcher<T: EndpointMatcher> { - /// Locations allowed to be picked from. In the case of custom lists this may be multiple - /// locations. In normal circumstances this contains only 1 location. - pub locations: Constraint<ResolvedLocationConstraint>, - pub providers: Constraint<Providers>, - pub ownership: Constraint<Ownership>, - pub endpoint_matcher: T, -} - -impl RelayMatcher<AnyTunnelMatcher> { - pub fn new( - constraints: RelayConstraints, - openvpn_data: OpenVpnEndpointData, - wireguard_data: WireguardEndpointData, - custom_lists: &CustomListsSettings, - ) -> Self { - Self { - locations: ResolvedLocationConstraint::from_constraint( - constraints.location, - custom_lists, - ), - providers: constraints.providers, - ownership: constraints.ownership, - endpoint_matcher: AnyTunnelMatcher { - wireguard: WireguardMatcher::new(constraints.wireguard_constraints, wireguard_data), - openvpn: OpenVpnMatcher::new(constraints.openvpn_constraints, openvpn_data), - tunnel_type: constraints.tunnel_protocol, - }, - } - } - - pub fn into_wireguard_matcher(self) -> RelayMatcher<WireguardMatcher> { - RelayMatcher { - endpoint_matcher: self.endpoint_matcher.wireguard, - locations: self.locations, - providers: self.providers, - ownership: self.ownership, - } - } -} - -impl RelayMatcher<WireguardMatcher> { - pub fn set_peer(&mut self, peer: Relay) { - self.endpoint_matcher.peer = Some(peer); - } -} - -impl<T: EndpointMatcher> RelayMatcher<T> { - /// Filter a list of relays and their endpoints based on constraints. - /// Only relays with (and including) matching endpoints are returned. - pub fn filter_matching_relay_list<'a, R: Iterator<Item = &'a Relay> + Clone>( - &self, - relays: R, - ) -> Vec<Relay> { - let matches = relays.filter(|relay| self.pre_filter_matching_relay(relay)); - let ignore_include_in_country = !matches.clone().any(|relay| relay.include_in_country); - matches - .filter(|relay| self.post_filter_matching_relay(relay, ignore_include_in_country)) - .cloned() - .collect() - } - - /// Filter a relay based on constraints and endpoint type, 1st pass. - fn pre_filter_matching_relay(&self, relay: &Relay) -> bool { - relay.active - && self.providers.matches(relay) - && self.ownership.matches(relay) - && self.locations.matches_with_opts(relay, true) - && self.endpoint_matcher.is_matching_relay(relay) - } - - /// Filter a relay based on constraints and endpoint type, 2nd pass. - fn post_filter_matching_relay(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { - self.locations - .matches_with_opts(relay, ignore_include_in_country) - } - - pub fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { - self.endpoint_matcher.mullvad_endpoint(relay) - } -} - -/// EndpointMatcher allows to abstract over different tunnel-specific or bridge constraints. -/// This enables one to not have false dependencies on OpenVpn specific constraints when -/// selecting only WireGuard tunnels. -pub trait EndpointMatcher: Clone { - /// Returns whether the relay has matching endpoints. - fn is_matching_relay(&self, relay: &Relay) -> bool; - /// Constructs a MullvadEndpoint for a given Relay using extra data from the relay matcher - /// itself. - fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint>; -} - -impl EndpointMatcher for OpenVpnMatcher { - fn is_matching_relay(&self, relay: &Relay) -> bool { - self.matches(&self.data) && matches!(relay.endpoint_data, RelayEndpointData::Openvpn) - } - - fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { - if !self.is_matching_relay(relay) { - return None; - } - - self.get_transport_port().map(|endpoint| { - MullvadEndpoint::OpenVpn(Endpoint::new( - relay.ipv4_addr_in, - endpoint.port, - endpoint.protocol, - )) - }) - } -} - -#[derive(Debug, Clone)] -pub struct OpenVpnMatcher { - pub constraints: OpenVpnConstraints, - pub data: OpenVpnEndpointData, -} - -impl OpenVpnMatcher { - pub fn new(constraints: OpenVpnConstraints, data: OpenVpnEndpointData) -> Self { - Self { constraints, data } - } - - fn get_transport_port(&self) -> Option<&OpenVpnEndpoint> { - match self.constraints.port { - Constraint::Any => self.data.ports.choose(&mut rand::thread_rng()), - Constraint::Only(transport_port) => self - .data - .ports - .iter() - .filter(|endpoint| { - transport_port - .port - .map(|port| port == endpoint.port) - .unwrap_or(true) - && transport_port.protocol == endpoint.protocol - }) - .choose(&mut rand::thread_rng()), - } - } -} - -impl Match<OpenVpnEndpointData> for OpenVpnMatcher { - fn matches(&self, endpoint: &OpenVpnEndpointData) -> bool { - match self.constraints.port { - Constraint::Any => true, - Constraint::Only(transport_port) => endpoint.ports.iter().any(|endpoint| { - transport_port.protocol == endpoint.protocol - && (transport_port.port.is_any() - || transport_port.port == Constraint::Only(endpoint.port)) - }), - } - } -} - -#[derive(Clone)] -pub struct AnyTunnelMatcher { - pub wireguard: WireguardMatcher, - pub openvpn: OpenVpnMatcher, - /// in the case that a user hasn't specified a tunnel protocol, the relay - /// selector might still construct preferred constraints that do select a - /// specific tunnel protocol, which is why the tunnel type may be specified - /// in the `AnyTunnelMatcher`. - pub tunnel_type: Constraint<TunnelType>, -} - -impl EndpointMatcher for AnyTunnelMatcher { - fn is_matching_relay(&self, relay: &Relay) -> bool { - match self.tunnel_type { - Constraint::Any => { - self.wireguard.is_matching_relay(relay) || self.openvpn.is_matching_relay(relay) - } - Constraint::Only(TunnelType::OpenVpn) => self.openvpn.is_matching_relay(relay), - Constraint::Only(TunnelType::Wireguard) => self.wireguard.is_matching_relay(relay), - } - } - - fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { - #[cfg(not(target_os = "android"))] - match self.tunnel_type { - Constraint::Any => self - .openvpn - .mullvad_endpoint(relay) - .or_else(|| self.wireguard.mullvad_endpoint(relay)), - Constraint::Only(TunnelType::OpenVpn) => self.openvpn.mullvad_endpoint(relay), - Constraint::Only(TunnelType::Wireguard) => self.wireguard.mullvad_endpoint(relay), - } - - #[cfg(target_os = "android")] - self.wireguard.mullvad_endpoint(relay) - } -} - -#[derive(Default, Clone)] -pub struct WireguardMatcher { - /// The peer is an already selected peer relay to be used with multihop. - /// It's stored here so we can exclude it from further selections being made. - pub peer: Option<Relay>, - pub port: Constraint<u16>, - pub ip_version: Constraint<IpVersion>, - - pub data: WireguardEndpointData, -} - -impl WireguardMatcher { - pub fn new(constraints: WireguardConstraints, data: WireguardEndpointData) -> Self { - Self { - peer: None, - port: constraints.port, - ip_version: constraints.ip_version, - data, - } - } - - pub fn from_endpoint(data: WireguardEndpointData) -> Self { - Self { - data, - ..Default::default() - } - } - - fn wg_data_to_endpoint( - &self, - relay: &Relay, - data: &WireguardEndpointData, - ) -> Option<MullvadEndpoint> { - let host = self.get_address_for_wireguard_relay(relay)?; - let port = self.get_port_for_wireguard_relay(data)?; - let peer_config = wireguard::PeerConfig { - public_key: relay - .endpoint_data - .unwrap_wireguard_ref() - .public_key - .clone(), - endpoint: SocketAddr::new(host, port), - allowed_ips: all_of_the_internet(), - psk: None, - }; - Some(MullvadEndpoint::Wireguard(MullvadWireguardEndpoint { - peer: peer_config, - exit_peer: None, - ipv4_gateway: data.ipv4_gateway, - ipv6_gateway: data.ipv6_gateway, - })) - } - - fn get_address_for_wireguard_relay(&self, relay: &Relay) -> Option<IpAddr> { - match self.ip_version { - Constraint::Any | Constraint::Only(IpVersion::V4) => Some(relay.ipv4_addr_in.into()), - Constraint::Only(IpVersion::V6) => relay.ipv6_addr_in.map(|addr| addr.into()), - } - } - - fn get_port_for_wireguard_relay(&self, data: &WireguardEndpointData) -> Option<u16> { - match self.port { - Constraint::Any => { - let get_port_amount = - |range: &(u16, u16)| -> u64 { (1 + range.1 - range.0) as u64 }; - let port_amount: u64 = data.port_ranges.iter().map(get_port_amount).sum(); - - if port_amount < 1 { - return None; - } - - let mut port_index = rand::thread_rng().gen_range(0..port_amount); - - for range in data.port_ranges.iter() { - let ports_in_range = get_port_amount(range); - if port_index < ports_in_range { - return Some(port_index as u16 + range.0); - } - port_index -= ports_in_range; - } - log::error!("Port selection algorithm is broken!"); - None - } - Constraint::Only(port) => { - if data - .port_ranges - .iter() - .any(|range| (range.0 <= port && port <= range.1)) - { - Some(port) - } else { - None - } - } - } - } -} - -impl EndpointMatcher for WireguardMatcher { - fn is_matching_relay(&self, relay: &Relay) -> bool { - !self - .peer - .as_ref() - .map(|peer_relay| peer_relay.hostname == relay.hostname) - .unwrap_or(false) - && matches!(relay.endpoint_data, RelayEndpointData::Wireguard(..)) - } - - fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { - if !self.is_matching_relay(relay) { - return None; - } - self.wg_data_to_endpoint(relay, &self.data) - } -} - -#[derive(Clone)] -pub struct BridgeMatcher(pub ()); - -impl EndpointMatcher for BridgeMatcher { - fn is_matching_relay(&self, relay: &Relay) -> bool { - matches!(relay.endpoint_data, RelayEndpointData::Bridge) - } - - fn mullvad_endpoint(&self, _relay: &Relay) -> Option<MullvadEndpoint> { - None - } -} diff --git a/mullvad-relay-selector/src/relay_selector/detailer.rs b/mullvad-relay-selector/src/relay_selector/detailer.rs new file mode 100644 index 0000000000..b0e6e3b0ea --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/detailer.rs @@ -0,0 +1,283 @@ +//! This module implements functions for producing a [`MullvadEndpoint`] given a Wireguard or +//! OpenVPN relay chosen by the relay selector. +//! +//! [`MullvadEndpoint`] contains all the necessary information for establishing a connection +//! between the client and Mullvad VPN. It is the daemon's responsibility to establish this +//! connection. +//! +//! [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint + +use std::net::{IpAddr, SocketAddr}; + +use ipnetwork::IpNetwork; +use mullvad_types::{ + constraints::Constraint, + endpoint::MullvadWireguardEndpoint, + relay_constraints::TransportPort, + relay_list::{OpenVpnEndpoint, OpenVpnEndpointData, Relay, WireguardEndpointData}, +}; +use talpid_types::net::{ + all_of_the_internet, wireguard::PeerConfig, Endpoint, IpVersion, TransportProtocol, +}; + +use super::{ + query::{BridgeQuery, OpenVpnRelayQuery, WireguardRelayQuery}, + WireguardConfig, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("No OpenVPN endpoint could be derived")] + NoOpenVpnEndpoint, + #[error("No bridge endpoint could be derived")] + NoBridgeEndpoint, + #[error("The selected relay does not support IPv6")] + NoIPv6(Box<Relay>), + #[error("Invalid port argument: port {0} is not in any valid Wireguard port range")] + PortNotInRange(u16), + #[error("Port selection algorithm is broken")] + PortSelectionAlgorithm, +} + +/// Constructs a [`MullvadWireguardEndpoint`] with details for how to connect to a Wireguard relay. +/// +/// # Returns +/// - A configured endpoint for Wireguard relay, encapsulating either a single-hop or multi-hop connection. +/// - Returns [`Option::None`] if the desired port is not in a valid port range (see +/// [`WireguardRelayQuery::port`]) or relay addresses cannot be resolved. +pub fn wireguard_endpoint( + query: &WireguardRelayQuery, + data: &WireguardEndpointData, + relay: &WireguardConfig, +) -> Result<MullvadWireguardEndpoint, Error> { + match relay { + WireguardConfig::Singlehop { exit } => wireguard_singlehop_endpoint(query, data, exit), + WireguardConfig::Multihop { exit, entry } => { + wireguard_multihop_endpoint(query, data, exit, entry) + } + } +} + +/// Configure a single-hop connection using the exit relay data. +fn wireguard_singlehop_endpoint( + query: &WireguardRelayQuery, + data: &WireguardEndpointData, + exit: &Relay, +) -> Result<MullvadWireguardEndpoint, Error> { + let endpoint = { + let host = get_address_for_wireguard_relay(query, exit)?; + let port = get_port_for_wireguard_relay(query, data)?; + SocketAddr::new(host, port) + }; + let peer_config = PeerConfig { + public_key: exit.endpoint_data.unwrap_wireguard_ref().public_key.clone(), + endpoint, + allowed_ips: all_of_the_internet(), + // This will be filled in later, not the relay selector's problem + psk: None, + }; + Ok(MullvadWireguardEndpoint { + peer: peer_config, + exit_peer: None, + ipv4_gateway: data.ipv4_gateway, + ipv6_gateway: data.ipv6_gateway, + }) +} + +/// Configure a multihop connection using the entry & exit relay data. +/// +/// # Note +/// In a multihop circuit, we need to provide an exit peer configuration in addition to the +/// peer configuration. +fn wireguard_multihop_endpoint( + query: &WireguardRelayQuery, + data: &WireguardEndpointData, + exit: &Relay, + entry: &Relay, +) -> Result<MullvadWireguardEndpoint, Error> { + /// The standard port on which an exit relay accepts connections from an entry relay in a + /// multihop circuit. + const WIREGUARD_EXIT_PORT: u16 = 51820; + let exit_endpoint = { + let ip = exit.ipv4_addr_in; + // The port that the exit relay listens for incoming connections from entry + // relays is *not* derived from the original query / user settings. + let port = WIREGUARD_EXIT_PORT; + SocketAddr::from((ip, port)) + }; + let exit = PeerConfig { + public_key: exit.endpoint_data.unwrap_wireguard_ref().public_key.clone(), + endpoint: exit_endpoint, + // The exit peer should be able to route incoming VPN traffic to the rest of + // the internet. + allowed_ips: all_of_the_internet(), + // This will be filled in later, not the relay selector's problem + psk: None, + }; + + let entry_endpoint = { + let host = get_address_for_wireguard_relay(query, entry)?; + let port = get_port_for_wireguard_relay(query, data)?; + SocketAddr::from((host, port)) + }; + let entry = PeerConfig { + public_key: entry + .endpoint_data + .unwrap_wireguard_ref() + .public_key + .clone(), + endpoint: entry_endpoint, + // The entry peer should only be able to route incoming VPN traffic to the + // exit peer. + allowed_ips: vec![IpNetwork::from(exit.endpoint.ip())], + // This will be filled in later + psk: None, + }; + + Ok(MullvadWireguardEndpoint { + peer: entry, + exit_peer: Some(exit), + ipv4_gateway: data.ipv4_gateway, + ipv6_gateway: data.ipv6_gateway, + }) +} + +/// Get the correct IP address for the given relay. +fn get_address_for_wireguard_relay( + query: &WireguardRelayQuery, + relay: &Relay, +) -> Result<IpAddr, Error> { + match query.ip_version { + Constraint::Any | Constraint::Only(IpVersion::V4) => Ok(relay.ipv4_addr_in.into()), + Constraint::Only(IpVersion::V6) => relay + .ipv6_addr_in + .map(|addr| addr.into()) + .ok_or(Error::NoIPv6(Box::new(relay.clone()))), + } +} + +/// Try to pick a valid Wireguard port. +fn get_port_for_wireguard_relay( + query: &WireguardRelayQuery, + data: &WireguardEndpointData, +) -> Result<u16, Error> { + match query.port { + Constraint::Any => select_random_port(&data.port_ranges), + Constraint::Only(port) => { + if data + .port_ranges + .iter() + .any(|range| (range.0 <= port && port <= range.1)) + { + Ok(port) + } else { + Err(Error::PortNotInRange(port)) + } + } + } +} + +/// Selects a random port number from a list of provided port ranges. +/// +/// This function iterates over a list of port ranges, each represented as a tuple (u16, u16) +/// where the first element is the start of the range and the second is the end (inclusive), +/// and selects a random port from the set of all ranges. +/// +/// # Parameters +/// - `port_ranges`: A slice of tuples, each representing a range of valid port numbers. +/// +/// # Returns +/// - `Option<u16>`: A randomly selected port number within the given ranges, or `None` if +/// the input is empty or the total number of available ports is zero. +fn select_random_port(port_ranges: &[(u16, u16)]) -> Result<u16, Error> { + use rand::Rng; + let get_port_amount = |range: &(u16, u16)| -> u64 { (1 + range.1 - range.0) as u64 }; + let port_amount: u64 = port_ranges.iter().map(get_port_amount).sum(); + + if port_amount < 1 { + return Err(Error::PortSelectionAlgorithm); + } + + let mut port_index = rand::thread_rng().gen_range(0..port_amount); + + for range in port_ranges.iter() { + let ports_in_range = get_port_amount(range); + if port_index < ports_in_range { + return Ok(port_index as u16 + range.0); + } + port_index -= ports_in_range; + } + Err(Error::PortSelectionAlgorithm) +} + +/// Constructs an [`Endpoint`] with details for how to connect to an OpenVPN relay. +/// +/// If this endpoint is to be used in conjunction with a bridge, the resulting endpoint is +/// guaranteed to use transport protocol `TCP`. +/// +/// This function can fail if no valid port + transport protocol combination is found. +/// See [`OpenVpnEndpointData`] for more details. +pub fn openvpn_endpoint( + query: &OpenVpnRelayQuery, + data: &OpenVpnEndpointData, + relay: &Relay, +) -> Result<Endpoint, Error> { + // If `bridge_mode` is true, this function may only return endpoints which use TCP, not UDP. + if BridgeQuery::should_use_bridge(&query.bridge_settings) { + openvpn_bridge_endpoint(&query.port, data, relay) + } else { + openvpn_singlehop_endpoint(&query.port, data, relay) + } +} + +/// Configure a single-hop connection using the exit relay data. +fn openvpn_singlehop_endpoint( + port_constraint: &Constraint<TransportPort>, + data: &OpenVpnEndpointData, + exit: &Relay, +) -> Result<Endpoint, Error> { + use rand::seq::IteratorRandom; + data.ports + .iter() + .filter(|&endpoint| compatible_openvpn_port_combo(port_constraint, endpoint)) + .choose(&mut rand::thread_rng()) + .map(|endpoint| Endpoint::new(exit.ipv4_addr_in, endpoint.port, endpoint.protocol)) + .ok_or(Error::NoOpenVpnEndpoint) +} + +/// Configure an endpoint that will be used together with a bridge. +/// +/// # Note +/// In bridge mode, the only viable transport protocol is TCP. Otherwise, this function is +/// identical to [`Self::to_singlehop_endpoint`]. +fn openvpn_bridge_endpoint( + port_constraint: &Constraint<TransportPort>, + data: &OpenVpnEndpointData, + exit: &Relay, +) -> Result<Endpoint, Error> { + use rand::seq::IteratorRandom; + data.ports + .iter() + .filter(|endpoint| matches!(endpoint.protocol, TransportProtocol::Tcp)) + .filter(|endpoint| compatible_openvpn_port_combo(port_constraint, endpoint)) + .choose(&mut rand::thread_rng()) + .map(|endpoint| Endpoint::new(exit.ipv4_addr_in, endpoint.port, endpoint.protocol)) + .ok_or(Error::NoBridgeEndpoint) +} + +/// Returns true if `port_constraint` can be used to connect to `endpoint`. +/// Otherwise, false is returned. +fn compatible_openvpn_port_combo( + port_constraint: &Constraint<TransportPort>, + endpoint: &OpenVpnEndpoint, +) -> bool { + match port_constraint { + Constraint::Any => true, + Constraint::Only(transport_port) => match transport_port.port { + Constraint::Any => transport_port.protocol == endpoint.protocol, + Constraint::Only(port) => { + port == endpoint.port && transport_port.protocol == endpoint.protocol + } + }, + } +} diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs new file mode 100644 index 0000000000..5ad5bf7ab4 --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -0,0 +1,124 @@ +//! This module contains various helper functions for the relay selector implementation. + +use std::net::SocketAddr; + +use mullvad_types::{ + constraints::Constraint, + endpoint::MullvadWireguardEndpoint, + relay_constraints::Udp2TcpObfuscationSettings, + relay_list::{BridgeEndpointData, Relay, RelayEndpointData}, +}; +use rand::{ + seq::{IteratorRandom, SliceRandom}, + thread_rng, Rng, +}; +use talpid_types::net::{obfuscation::ObfuscatorConfig, proxy::CustomProxy}; + +use crate::SelectedObfuscator; + +/// Pick a random element out of `from`, excluding the element `exclude` from the selection. +pub fn random<'a, A: PartialEq>( + from: impl IntoIterator<Item = &'a A>, + exclude: &A, +) -> Option<&'a A> { + from.into_iter() + .filter(|&a| a != exclude) + .choose(&mut thread_rng()) +} + +/// Picks a relay using [pick_random_relay_fn], using the `weight` member of each relay +/// as the weight function. +pub fn pick_random_relay(relays: &[Relay]) -> Option<&Relay> { + pick_random_relay_weighted(relays, |relay| relay.weight) +} + +/// Pick a random relay from the given slice. Will return `None` if the given slice is empty. +/// If all of the relays have a weight of 0, one will be picked at random without bias, +/// otherwise roulette wheel selection will be used to pick only relays with non-zero +/// weights. +pub fn pick_random_relay_weighted<RelayType>( + relays: &[RelayType], + weight: impl Fn(&RelayType) -> u64, +) -> Option<&RelayType> { + let total_weight: u64 = relays.iter().map(&weight).sum(); + let mut rng = thread_rng(); + if total_weight == 0 { + relays.choose(&mut rng) + } else { + // Assign each relay a subset of the range 0..total_weight with size equal to its weight. + // Pick a random number in the range 1..=total_weight. This choses the relay with a + // non-zero weight. + // + // rng(1..=total_weight) + // | + // v + // _____________________________i___________________________________________________ + // 0|_____________|__________________________|___________|_____|___________|__________| total_weight + // ^ ^ ^ ^ ^ + // | | | | | + // ------------------------------------------ ------------ + // | | | + // weight(relay 0) weight(relay 1) .. .. .. weight(relay n) + let mut i: u64 = rng.gen_range(1..=total_weight); + Some( + relays + .iter() + .find(|relay| { + i = i.saturating_sub(weight(relay)); + i == 0 + }) + .expect("At least one relay must've had a weight above 0"), + ) + } +} + +/// Picks a random bridge from a relay. +pub fn pick_random_bridge(data: &BridgeEndpointData, relay: &Relay) -> Option<CustomProxy> { + if relay.endpoint_data != RelayEndpointData::Bridge { + return None; + } + let shadowsocks_endpoint = data.shadowsocks.choose(&mut rand::thread_rng()); + if let Some(shadowsocks_endpoint) = shadowsocks_endpoint { + log::info!( + "Selected Shadowsocks bridge {} at {}:{}/{}", + relay.hostname, + relay.ipv4_addr_in, + shadowsocks_endpoint.port, + shadowsocks_endpoint.protocol + ); + Some(shadowsocks_endpoint.to_proxy_settings(relay.ipv4_addr_in.into())) + } else { + None + } +} + +pub fn get_udp2tcp_obfuscator( + obfuscation_settings_constraint: &Constraint<Udp2TcpObfuscationSettings>, + udp2tcp_ports: &[u16], + relay: Relay, + endpoint: &MullvadWireguardEndpoint, +) -> Option<SelectedObfuscator> { + let udp2tcp_endpoint_port = + get_udp2tcp_obfuscator_port(obfuscation_settings_constraint, udp2tcp_ports)?; + let config = ObfuscatorConfig::Udp2Tcp { + endpoint: SocketAddr::new(endpoint.peer.endpoint.ip(), udp2tcp_endpoint_port), + }; + + Some(SelectedObfuscator { config, relay }) +} + +pub fn get_udp2tcp_obfuscator_port( + obfuscation_settings_constraint: &Constraint<Udp2TcpObfuscationSettings>, + udp2tcp_ports: &[u16], +) -> Option<u16> { + match obfuscation_settings_constraint { + Constraint::Only(obfuscation_settings) if obfuscation_settings.port.is_only() => { + udp2tcp_ports + .iter() + .find(|&candidate| obfuscation_settings.port == Constraint::Only(*candidate)) + .copied() + } + // There are no specific obfuscation settings to take into consideration in this case. + Constraint::Any | Constraint::Only(_) => udp2tcp_ports.choose(&mut thread_rng()).copied(), + } +} diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs new file mode 100644 index 0000000000..c287e30fe6 --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -0,0 +1,186 @@ +//! This module is responsible for filtering the whole relay list based on queries. +use std::collections::HashSet; + +use mullvad_types::{ + constraints::{Constraint, Match}, + custom_list::CustomListsSettings, + relay_constraints::{ + GeographicLocationConstraint, InternalBridgeConstraints, LocationConstraint, Ownership, + Providers, + }, + relay_list::{Relay, RelayEndpointData}, +}; +use talpid_types::net::TunnelType; + +use super::query::RelayQuery; + +/// Filter a list of relays and their endpoints based on constraints. +/// Only relays with (and including) matching endpoints are returned. +pub fn filter_matching_relay_list<'a, R: Iterator<Item = &'a Relay> + Clone>( + query: &RelayQuery, + relays: R, + custom_lists: &CustomListsSettings, +) -> Vec<Relay> { + let locations = ResolvedLocationConstraint::from_constraint(&query.location, custom_lists); + let shortlist = relays + // Filter on tunnel type + .filter(|relay| filter_tunnel_type(&query.tunnel_protocol, relay)) + // Filter on active relays + .filter(|relay| filter_on_active(relay)) + // Filter by location + .filter(|relay| filter_on_location(&locations, relay)) + // Filter by ownership + .filter(|relay| filter_on_ownership(&query.ownership, relay)) + // Filter by providers + .filter(|relay| filter_on_providers(&query.providers, relay)); + + // The last filtering to be done is on the `include_in_country` attribute found on each + // relay. When the location constraint is based on country, a relay which has `include_in_country` + // set to true should always be prioritized over relays which has this flag set to false. + // We should only consider relays with `include_in_country` set to false if there are no + // other candidates left. + match &locations { + Constraint::Any => shortlist.cloned().collect(), + Constraint::Only(locations) => { + let mut included = HashSet::new(); + let mut excluded = HashSet::new(); + for location in locations { + let (included_in_country, not_included_in_country): (Vec<_>, Vec<_>) = shortlist + .clone() + .partition(|relay| location.is_country() && relay.include_in_country); + included.extend(included_in_country); + excluded.extend(not_included_in_country); + } + if included.is_empty() { + excluded.into_iter().cloned().collect() + } else { + included.into_iter().cloned().collect() + } + } + } +} + +pub fn filter_matching_bridges<'a, R: Iterator<Item = &'a Relay> + Clone>( + constraints: &InternalBridgeConstraints, + relays: R, + custom_lists: &CustomListsSettings, +) -> Vec<Relay> { + let locations = + ResolvedLocationConstraint::from_constraint(&constraints.location, custom_lists); + relays + // Filter on active relays + .filter(|relay| filter_on_active(relay)) + // Filter on bridge type + .filter(|relay| filter_bridge(relay)) + // Filter by location + .filter(|relay| filter_on_location(&locations, relay)) + // Filter by ownership + .filter(|relay| filter_on_ownership(&constraints.ownership, relay)) + // Filter by constraints + .filter(|relay| filter_on_providers(&constraints.providers, relay)) + .cloned() + .collect() +} + +// --- Define relay filters as simple functions / predicates --- +// The intent is to make it easier to re-use in iterator chains. + +/// Returns whether `relay` is active. +pub const fn filter_on_active(relay: &Relay) -> bool { + relay.active +} + +/// Returns whether `relay` satisfy the location constraint posed by `filter`. +pub fn filter_on_location( + filter: &Constraint<ResolvedLocationConstraint<'_>>, + relay: &Relay, +) -> bool { + filter.matches(relay) +} + +/// Returns whether `relay` satisfy the ownership constraint posed by `filter`. +pub fn filter_on_ownership(filter: &Constraint<Ownership>, relay: &Relay) -> bool { + filter.matches(relay) +} + +/// Returns whether `relay` satisfy the providers constraint posed by `filter`. +pub fn filter_on_providers(filter: &Constraint<Providers>, relay: &Relay) -> bool { + filter.matches(relay) +} + +/// Returns whether the relay is an OpenVPN relay. +pub const fn filter_openvpn(relay: &Relay) -> bool { + matches!(relay.endpoint_data, RelayEndpointData::Openvpn) +} + +/// Returns whether the relay matches the tunnel constraint `filter` +pub const fn filter_tunnel_type(filter: &Constraint<TunnelType>, relay: &Relay) -> bool { + match filter { + Constraint::Any => true, + Constraint::Only(typ) => match typ { + // Do not keep OpenVPN relays on Android + TunnelType::OpenVpn if cfg!(target_os = "android") => false, + TunnelType::OpenVpn => filter_openvpn(relay), + TunnelType::Wireguard => filter_wireguard(relay), + }, + } +} + +/// Returns whether the relay is a Wireguard relay. +pub const fn filter_wireguard(relay: &Relay) -> bool { + matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_)) +} + +/// Returns whether the relay is a bridge. +pub const fn filter_bridge(relay: &Relay) -> bool { + matches!(relay.endpoint_data, RelayEndpointData::Bridge) +} + +/// Wrapper around [`GeographicLocationConstraint`]. +/// Useful for iterating over a set of [`GeographicLocationConstraint`] where custom lists +/// are considered. +#[derive(Debug, Clone)] +pub struct ResolvedLocationConstraint<'a>(Vec<&'a GeographicLocationConstraint>); + +impl<'a> ResolvedLocationConstraint<'a> { + /// Define the mapping from a [location][`LocationConstraint`] and a set of + /// [custom lists][`CustomListsSettings`] to [`ResolvedLocationConstraint`]. + pub fn from_constraint( + location_constraint: &'a Constraint<LocationConstraint>, + custom_lists: &'a CustomListsSettings, + ) -> Constraint<ResolvedLocationConstraint<'a>> { + match location_constraint { + Constraint::Any => Constraint::Any, + Constraint::Only(location) => Constraint::Only(match location { + LocationConstraint::Location(location) => { + ResolvedLocationConstraint(vec![location]) + } + LocationConstraint::CustomList { list_id } => custom_lists + .iter() + .find(|list| list.id == *list_id) + .map(|custom_list| { + ResolvedLocationConstraint(custom_list.locations.iter().collect()) + }) + .unwrap_or_else(|| { + log::warn!("Resolved non-existent custom list"); + ResolvedLocationConstraint(vec![]) + }), + }), + } + } +} + +impl<'a> IntoIterator for &'a ResolvedLocationConstraint<'a> { + type Item = &'a GeographicLocationConstraint; + type IntoIter = std::iter::Copied<std::slice::Iter<'a, &'a GeographicLocationConstraint>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().copied() + } +} + +impl Match<Relay> for ResolvedLocationConstraint<'_> { + fn matches(&self, relay: &Relay) -> bool { + self.into_iter().any(|location| location.matches(relay)) + } +} diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs new file mode 100644 index 0000000000..4967267e79 --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -0,0 +1,978 @@ +//! The implementation of the relay selector. + +pub mod detailer; +mod helpers; +mod matcher; +mod parsed_relays; +pub mod query; + +use chrono::{DateTime, Local}; +use itertools::Itertools; +use once_cell::sync::Lazy; +use std::{ + path::Path, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +use mullvad_types::{ + constraints::Constraint, + custom_list::CustomListsSettings, + endpoint::MullvadWireguardEndpoint, + location::{Coordinates, Location}, + relay_constraints::{ + BridgeSettings, BridgeState, InternalBridgeConstraints, ObfuscationSettings, + OpenVpnConstraints, RelayConstraints, RelayOverride, RelaySettings, ResolvedBridgeSettings, + SelectedObfuscation, WireguardConstraints, + }, + relay_list::{Relay, RelayList}, + settings::Settings, + CustomTunnelEndpoint, +}; +use talpid_types::{ + net::{ + obfuscation::ObfuscatorConfig, proxy::CustomProxy, Endpoint, TransportProtocol, TunnelType, + }, + ErrorExt, +}; + +use crate::error::{EndpointErrorDetails, Error}; + +use self::{ + detailer::{openvpn_endpoint, wireguard_endpoint}, + matcher::{filter_matching_bridges, filter_matching_relay_list}, + parsed_relays::ParsedRelays, + query::{BridgeQuery, Intersection, OpenVpnRelayQuery, RelayQuery, WireguardRelayQuery}, +}; + +/// [`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` +pub static RETRY_ORDER: Lazy<Vec<RelayQuery>> = Lazy::new(|| { + use query::builder::{IpVersion, RelayQueryBuilder}; + vec![ + // 1 + // Note: This query can be unified with all possible user preferences. + // If the user has tunnel protocol set to 'Auto', the relay selector will + // default to picking a Wireguard relay. + RelayQueryBuilder::new().build(), + // 2 + RelayQueryBuilder::new().wireguard().port(443).build(), + // 3 + RelayQueryBuilder::new() + .wireguard() + .ip_version(IpVersion::V6) + .build(), + // 4 + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(TransportProtocol::Tcp) + .port(443) + .build(), + // 5 + RelayQueryBuilder::new().wireguard().udp2tcp().build(), + // 6 + RelayQueryBuilder::new() + .wireguard() + .udp2tcp() + .ip_version(IpVersion::V6) + .build(), + // 7 + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(TransportProtocol::Tcp) + .bridge() + .build(), + ] +}); + +#[derive(Clone)] +pub struct RelaySelector { + config: Arc<Mutex<SelectorConfig>>, + parsed_relays: Arc<Mutex<ParsedRelays>>, +} + +#[derive(Clone)] +pub struct SelectorConfig { + // Normal relay settings + pub relay_settings: RelaySettings, + pub custom_lists: CustomListsSettings, + pub relay_overrides: Vec<RelayOverride>, + // Wireguard specific data + pub obfuscation_settings: ObfuscationSettings, + // OpenVPN specific data + pub bridge_state: BridgeState, + pub bridge_settings: BridgeSettings, +} + +/// 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 [`RelaySettings::Normal`]. +/// This is the most common variant, and there exists a mapping from this variant to [`RelayQueryBuilder`]. +/// Being able to implement `From<NormalSelectorConfig> for RelayQueryBuilder` was the main +/// motivator for introducing these seemingly useless derivates of [`SelectorConfig`]. +/// +/// The second one is a custom config, where [`SelectorConfig::relay_settings`] is [`RelaySettings::Custom`]. +/// For this variant, the endpoint where the client should connect to is already specified inside of the variant, +/// so in practice the relay selector becomes superfluous. Also, there exists no mapping to [`RelayQueryBuilder`]. +#[derive(Debug, Clone)] +enum SpecializedSelectorConfig<'a> { + // This variant implements `From<NormalSelectorConfig> for RelayQuery` + Normal(NormalSelectorConfig<'a>), + // This variant does not + Custom(&'a CustomTunnelEndpoint), +} + +/// A special-cased variant of [`SelectorConfig`]. +/// +/// For context, see [`SpecializedSelectorConfig`]. +#[derive(Debug, Clone)] +struct NormalSelectorConfig<'a> { + user_preferences: &'a RelayConstraints, + custom_lists: &'a CustomListsSettings, + // Wireguard specific data + obfuscation_settings: &'a ObfuscationSettings, + // OpenVPN specific data + bridge_state: &'a BridgeState, + bridge_settings: &'a BridgeSettings, +} + +/// The return type of [`RelaySelector::get_relay`]. +#[derive(Clone, Debug)] +pub enum GetRelay { + Wireguard { + endpoint: MullvadWireguardEndpoint, + obfuscator: Option<SelectedObfuscator>, + inner: WireguardConfig, + }, + #[cfg(not(target_os = "android"))] + OpenVpn { + endpoint: Endpoint, + exit: Relay, + bridge: Option<SelectedBridge>, + }, + Custom(CustomTunnelEndpoint), +} + +/// This struct defines the different Wireguard relays the the relay selector can end up selecting +/// for an arbitrary Wireguard [`query`]. +/// +/// - [`WireguardConfig::Singlehop`]; A normal wireguard relay where VPN traffic enters and exits +/// through this sole relay. +/// - [`WireguardConfig::Multihop`]; Two wireguard relays to be used in a multihop circuit. VPN +/// traffic will enter through `entry` and eventually come out from `exit` before the traffic +/// will actually be routed to the broader internet. +#[derive(Clone, Debug)] +pub enum WireguardConfig { + Singlehop { exit: Relay }, + Multihop { exit: Relay, entry: Relay }, +} + +#[derive(Clone, Debug)] +pub enum SelectedBridge { + Normal { settings: CustomProxy, relay: Relay }, + Custom(CustomProxy), +} + +impl SelectedBridge { + /// Get the bridge settings. + pub fn settings(&self) -> &CustomProxy { + match self { + SelectedBridge::Normal { settings, .. } => settings, + SelectedBridge::Custom(settings) => settings, + } + } + + /// Get the relay acting as a bridge. + /// This is not applicable if `self` is a [custom bridge][`SelectedBridge::Custom`]. + pub fn relay(&self) -> Option<&Relay> { + match self { + SelectedBridge::Normal { relay, .. } => Some(relay), + _ => None, + } + } +} + +#[derive(Clone, Debug)] +pub struct SelectedObfuscator { + pub config: ObfuscatorConfig, + pub relay: Relay, +} + +impl Default for SelectorConfig { + fn default() -> Self { + let default_settings = Settings::default(); + SelectorConfig { + relay_settings: default_settings.relay_settings, + bridge_settings: default_settings.bridge_settings, + obfuscation_settings: default_settings.obfuscation_settings, + bridge_state: default_settings.bridge_state, + custom_lists: default_settings.custom_lists, + relay_overrides: default_settings.relay_overrides, + } + } +} + +impl<'a> From<&'a SelectorConfig> for SpecializedSelectorConfig<'a> { + fn from(value: &'a SelectorConfig) -> SpecializedSelectorConfig<'a> { + match &value.relay_settings { + RelaySettings::CustomTunnelEndpoint(custom_tunnel_endpoint) => { + SpecializedSelectorConfig::Custom(custom_tunnel_endpoint) + } + RelaySettings::Normal(user_preferences) => { + SpecializedSelectorConfig::Normal(NormalSelectorConfig { + user_preferences, + obfuscation_settings: &value.obfuscation_settings, + bridge_state: &value.bridge_state, + bridge_settings: &value.bridge_settings, + custom_lists: &value.custom_lists, + }) + } + } + } +} + +impl<'a> From<NormalSelectorConfig<'a>> for RelayQuery { + /// Map user settings to [`RelayQuery`]. + fn from(value: NormalSelectorConfig<'a>) -> Self { + /// Map the Wireguard-specific bits of `value` to [`WireguradRelayQuery`] + fn wireguard_constraints( + wireguard_constraints: WireguardConstraints, + obfuscation_settings: ObfuscationSettings, + ) -> WireguardRelayQuery { + let WireguardConstraints { + port, + ip_version, + use_multihop, + entry_location, + } = wireguard_constraints; + WireguardRelayQuery { + port, + ip_version, + use_multihop: Constraint::Only(use_multihop), + entry_location, + obfuscation: obfuscation_settings.selected_obfuscation, + udp2tcp_port: Constraint::Only(obfuscation_settings.udp2tcp.clone()), + } + } + + /// Map the OpenVPN-specific bits of `value` to [`OpenVpnRelayQuery`] + fn openvpn_constraints( + openvpn_constraints: OpenVpnConstraints, + bridge_state: BridgeState, + bridge_settings: BridgeSettings, + ) -> OpenVpnRelayQuery { + OpenVpnRelayQuery { + port: openvpn_constraints.port, + bridge_settings: match bridge_state { + BridgeState::On => match bridge_settings.bridge_type { + mullvad_types::relay_constraints::BridgeType::Normal => { + Constraint::Only(BridgeQuery::Normal(bridge_settings.normal.clone())) + } + mullvad_types::relay_constraints::BridgeType::Custom => { + Constraint::Only(BridgeQuery::Custom(bridge_settings.custom.clone())) + } + }, + BridgeState::Auto => Constraint::Only(BridgeQuery::Auto), + BridgeState::Off => Constraint::Only(BridgeQuery::Off), + }, + } + } + + let wireguard_constraints = wireguard_constraints( + value.user_preferences.wireguard_constraints.clone(), + value.obfuscation_settings.clone(), + ); + let openvpn_constraints = openvpn_constraints( + value.user_preferences.openvpn_constraints, + *value.bridge_state, + value.bridge_settings.clone(), + ); + RelayQuery { + location: value.user_preferences.location.clone(), + providers: value.user_preferences.providers.clone(), + ownership: value.user_preferences.ownership, + tunnel_protocol: value.user_preferences.tunnel_protocol, + wireguard_constraints, + openvpn_constraints, + } + } +} + +impl RelaySelector { + /// Returns a new `RelaySelector` backed by relays cached on disk. + pub fn new( + config: SelectorConfig, + resource_path: impl AsRef<Path>, + cache_path: impl AsRef<Path>, + ) -> Self { + const DATE_TIME_FORMAT_STR: &str = "%Y-%m-%d %H:%M:%S%.3f"; + let unsynchronized_parsed_relays = + ParsedRelays::from_file(&cache_path, &resource_path, &config.relay_overrides) + .unwrap_or_else(|error| { + log::error!( + "{}", + error.display_chain_with_msg("Unable to load cached and bundled relays") + ); + ParsedRelays::empty() + }); + log::info!( + "Initialized with {} cached relays from {}", + unsynchronized_parsed_relays.relays().count(), + DateTime::<Local>::from(unsynchronized_parsed_relays.last_updated()) + .format(DATE_TIME_FORMAT_STR) + ); + + RelaySelector { + config: Arc::new(Mutex::new(config)), + parsed_relays: Arc::new(Mutex::new(unsynchronized_parsed_relays)), + } + } + + pub fn from_list(config: SelectorConfig, relay_list: RelayList) -> Self { + RelaySelector { + parsed_relays: Arc::new(Mutex::new(ParsedRelays::from_relay_list( + relay_list, + SystemTime::now(), + &config.relay_overrides, + ))), + config: Arc::new(Mutex::new(config)), + } + } + + pub fn set_config(&mut self, config: SelectorConfig) { + self.set_overrides(&config.relay_overrides); + let mut config_mutex = self.config.lock().unwrap(); + *config_mutex = config; + } + + pub fn set_relays(&self, relays: RelayList) { + let mut parsed_relays = self.parsed_relays.lock().unwrap(); + parsed_relays.update(relays); + } + + fn set_overrides(&mut self, relay_overrides: &[RelayOverride]) { + let mut parsed_relays = self.parsed_relays.lock().unwrap(); + parsed_relays.set_overrides(relay_overrides); + } + + /// Returns all countries and cities. The cities in the object returned does not have any + /// relays in them. + pub fn get_relays(&mut self) -> RelayList { + let parsed_relays = self.parsed_relays.lock().unwrap(); + parsed_relays.original_list().clone() + } + + pub fn etag(&self) -> Option<String> { + self.parsed_relays.lock().unwrap().etag() + } + + pub fn last_updated(&self) -> SystemTime { + self.parsed_relays.lock().unwrap().last_updated() + } + + /// Returns a non-custom bridge based on the relay and bridge constraints, ignoring the bridge + /// state. + pub fn get_bridge_forced(&self) -> Option<CustomProxy> { + let parsed_relays = &self.parsed_relays.lock().unwrap(); + let config = self.config.lock().unwrap(); + let specialized_config = SpecializedSelectorConfig::from(&*config); + + 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) + } + SpecializedSelectorConfig::Custom(_) => None, + }; + + let bridge_settings = &config.bridge_settings; + let constraints = match bridge_settings.resolve() { + Ok(ResolvedBridgeSettings::Normal(settings)) => InternalBridgeConstraints { + location: settings.location.clone(), + providers: settings.providers.clone(), + ownership: settings.ownership, + transport_protocol: Constraint::Only(TransportProtocol::Tcp), + }, + _ => InternalBridgeConstraints { + location: Constraint::Any, + providers: Constraint::Any, + ownership: Constraint::Any, + transport_protocol: Constraint::Only(TransportProtocol::Tcp), + }, + }; + + let custom_lists = &config.custom_lists; + Self::get_proxy_settings(parsed_relays, &constraints, near_location, custom_lists) + .map(|(settings, _relay)| settings) + } + + /// Returns random relay and relay endpoint matching `query`. + pub fn get_relay_by_query(&self, query: RelayQuery) -> Result<GetRelay, Error> { + let config_guard = self.config.lock().unwrap(); + let config = SpecializedSelectorConfig::from(&*config_guard); + match config { + SpecializedSelectorConfig::Custom(custom_config) => { + Ok(GetRelay::Custom(custom_config.clone())) + } + SpecializedSelectorConfig::Normal(pure_config) => { + let parsed_relays = &self.parsed_relays.lock().unwrap(); + Self::get_relay_inner(&query, parsed_relays, &pure_config) + } + } + } + + /// Returns a random relay and relay endpoint matching the current constraints corresponding to + /// `retry_attempt` in [`RETRY_ORDER`] while considering [runtime_params][`RuntimeParameters`]. + /// + /// [`RETRY_ORDER`]: crate::RETRY_ORDER + pub fn get_relay( + &self, + retry_attempt: usize, + runtime_params: RuntimeParameters, + ) -> Result<GetRelay, Error> { + 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( + &self, + retry_attempt: usize, + retry_order: &[RelayQuery], + runtime_params: RuntimeParameters, + ) -> Result<GetRelay, Error> { + let config_guard = self.config.lock().unwrap(); + let config = SpecializedSelectorConfig::from(&*config_guard); + + // Short-circuit if a custom tunnel endpoint is to be used - don't have to involve the + // relay selector further! + match config { + SpecializedSelectorConfig::Custom(custom_config) => { + Ok(GetRelay::Custom(custom_config.clone())) + } + 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) + } + } + } + + /// This function defines the merge between a set of pre-defined queries and `user_preferences` for the given + /// `retry_attempt`. + /// + /// This algorithm will loop back to the start of `retry_order` if `retry_attempt < retry_order.len()`. + /// If `user_preferences` is not compatible with any of the pre-defined queries in `retry_order`, `user_preferences` + /// is returned. + /// + /// 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. + 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"); + 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())) + .nth(retry_attempt) + .unwrap_or(user_preferences) + } + + /// "Execute" the given query, yielding a final set of relays and/or bridges which the VPN traffic shall be routed through. + /// + /// # Parameters + /// - `query`: Constraints that filter the available relays, such as geographic location or tunnel protocol. + /// - `config`: Configuration settings that influence relay selection, including bridge state and custom lists. + /// - `parsed_relays`: The complete set of parsed relays available for selection. + /// + /// # Returns + /// * A randomly selected relay that meets the specified constraints (and a random bridge/entry relay if applicable). + /// See [`GetRelay`] for more details. + /// * An `Err` if no suitable relay is found + /// * An `Err` if no suitable bridge is found + #[cfg(not(target_os = "android"))] + fn get_relay_inner( + query: &RelayQuery, + parsed_relays: &ParsedRelays, + config: &NormalSelectorConfig<'_>, + ) -> Result<GetRelay, Error> { + match query.tunnel_protocol { + Constraint::Only(TunnelType::Wireguard) => { + Self::get_wireguard_relay(query, config, parsed_relays) + } + Constraint::Only(TunnelType::OpenVpn) => { + Self::get_openvpn_relay(query, config, parsed_relays) + } + Constraint::Any => { + // Try Wireguard, then OpenVPN, then fail + for tunnel_type in [TunnelType::Wireguard, TunnelType::OpenVpn] { + 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) { + return Ok(relay); + } + } + Err(Error::NoRelay) + } + } + } + + #[cfg(target_os = "android")] + fn get_relay_inner( + query: &RelayQuery, + parsed_relays: &ParsedRelays, + config: &NormalSelectorConfig<'_>, + ) -> Result<GetRelay, Error> { + Self::get_wireguard_relay(query, config, parsed_relays) + } + + /// Derive a valid Wireguard relay configuration from `query`. + /// + /// # Returns + /// * An `Err` if no exit relay can be chosen + /// * An `Err` if no entry relay can be chosen (if multihop is enabled on `query`) + /// * an `Err` if no [`MullvadEndpoint`] can be derived from the selected relay(s). + /// * `Ok(GetRelay::Wireguard)` otherwise + /// + /// [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint + fn get_wireguard_relay( + query: &RelayQuery, + config: &NormalSelectorConfig<'_>, + parsed_relays: &ParsedRelays, + ) -> Result<GetRelay, Error> { + let inner = if !query.wireguard_constraints.multihop() { + Self::get_wireguard_singlehop_config(query, config, parsed_relays)? + } else { + Self::get_wireguard_multihop_config(query, config, parsed_relays)? + }; + let endpoint = Self::get_wireguard_endpoint(query, parsed_relays, &inner)?; + let obfuscator = + Self::get_wireguard_obfuscator(query, inner.clone(), &endpoint, parsed_relays)?; + + Ok(GetRelay::Wireguard { + endpoint, + obfuscator, + inner, + }) + } + + /// Select a valid Wireguard exit relay. + /// + /// # Returns + /// * An `Err` if no exit relay can be chosen + /// * `Ok(WireguardInner::Singlehop)` otherwise + fn get_wireguard_singlehop_config( + query: &RelayQuery, + config: &NormalSelectorConfig<'_>, + parsed_relays: &ParsedRelays, + ) -> Result<WireguardConfig, Error> { + let candidates = + filter_matching_relay_list(query, parsed_relays.relays(), config.custom_lists); + helpers::pick_random_relay(&candidates) + .map(|exit| WireguardConfig::Singlehop { exit: exit.clone() }) + .ok_or(Error::NoRelay) + } + + /// This function selects a valid entry and exit relay to be used in a multihop configuration. + /// + /// # Returns + /// * An `Err` if no exit relay can be chosen + /// * An `Err` if no entry relay can be chosen + /// * An `Err` if the chosen entry and exit relays are the same + /// * `Ok(WireguardInner::Multihop)` otherwise + fn get_wireguard_multihop_config( + query: &RelayQuery, + config: &NormalSelectorConfig<'_>, + parsed_relays: &ParsedRelays, + ) -> Result<WireguardConfig, Error> { + // Here, we modify the original query just a bit. + // The actual query for an exit relay is identical as for an exit relay, with the + // exception that the location is different. It is simply the location as dictated by + // the query's multihop constraint. + let mut entry_relay_query = query.clone(); + entry_relay_query.location = query.wireguard_constraints.entry_location.clone(); + // After we have our two queries (one for the exit relay & one for the entry relay), + // we can query for all exit & entry candidates! All candidates are needed for the next step. + let exit_candidates = + filter_matching_relay_list(query, parsed_relays.relays(), config.custom_lists); + let entry_candidates = filter_matching_relay_list( + &entry_relay_query, + parsed_relays.relays(), + config.custom_lists, + ); + + // This algorithm gracefully handles a particular edge case that arise when a constraint on + // the exit relay is more specific than on the entry relay which forces the relay selector + // to choose one specific relay. The relay selector could end up selecting that specific + // relay as the entry relay, thus leaving no remaining exit relay candidates or vice versa. + let (exit, entry) = match (exit_candidates.as_slice(), entry_candidates.as_slice()) { + ([exit], [entry]) if exit == entry => None, + (exits, [entry]) if exits.contains(entry) => { + let exit = helpers::random(exits, entry).ok_or(Error::NoRelay)?; + Some((exit, entry)) + } + ([exit], entrys) if entrys.contains(exit) => { + let entry = helpers::random(entrys, exit).ok_or(Error::NoRelay)?; + Some((exit, entry)) + } + (exits, entrys) => { + let exit = helpers::pick_random_relay(exits).ok_or(Error::NoRelay)?; + let entry = helpers::random(entrys, exit).ok_or(Error::NoRelay)?; + Some((exit, entry)) + } + } + .ok_or(Error::NoRelay)?; + + Ok(WireguardConfig::Multihop { + exit: exit.clone(), + entry: entry.clone(), + }) + } + + /// Constructs a [`MullvadEndpoint`] with details for how to connect to `relay`. + /// + /// [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint + fn get_wireguard_endpoint( + query: &RelayQuery, + parsed_relays: &ParsedRelays, + relay: &WireguardConfig, + ) -> Result<MullvadWireguardEndpoint, Error> { + wireguard_endpoint( + &query.wireguard_constraints, + &parsed_relays.parsed_list().wireguard, + relay, + ) + .map_err(|internal| Error::NoEndpoint { + internal, + relay: EndpointErrorDetails::from_wireguard(relay.clone()), + }) + } + + fn get_wireguard_obfuscator( + query: &RelayQuery, + relay: WireguardConfig, + endpoint: &MullvadWireguardEndpoint, + parsed_relays: &ParsedRelays, + ) -> Result<Option<SelectedObfuscator>, Error> { + match query.wireguard_constraints.obfuscation { + SelectedObfuscation::Off | SelectedObfuscation::Auto => Ok(None), + SelectedObfuscation::Udp2Tcp => { + let obfuscator_relay = match relay { + WireguardConfig::Singlehop { exit } => exit, + WireguardConfig::Multihop { entry, .. } => entry, + }; + let udp2tcp_ports = &parsed_relays.parsed_list().wireguard.udp2tcp_ports; + + helpers::get_udp2tcp_obfuscator( + &query.wireguard_constraints.udp2tcp_port, + udp2tcp_ports, + obfuscator_relay, + endpoint, + ) + .map(Some) + .ok_or(Error::NoObfuscator) + } + } + } + + /// Derive a valid OpenVPN relay configuration from `query`. + /// + /// # Returns + /// * An `Err` if no exit relay can be chosen + /// * An `Err` if no entry bridge can be chosen (if bridge mode is enabled on `query`) + /// * an `Err` if no [`MullvadEndpoint`] can be derived from the selected relay + /// * `Ok(GetRelay::OpenVpn)` otherwise + /// + /// [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint + #[cfg(not(target_os = "android"))] + fn get_openvpn_relay( + query: &RelayQuery, + config: &NormalSelectorConfig<'_>, + parsed_relays: &ParsedRelays, + ) -> Result<GetRelay, Error> { + 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)?; + + Ok(GetRelay::OpenVpn { + endpoint, + exit, + bridge, + }) + } + + /// Constructs a [`MullvadEndpoint`] with details for how to connect to `relay`. + /// + /// [`MullvadEndpoint`]: mullvad_types::endpoint::MullvadEndpoint + #[cfg(not(target_os = "android"))] + fn get_openvpn_endpoint( + query: &RelayQuery, + relay: &Relay, + parsed_relays: &ParsedRelays, + ) -> Result<Endpoint, Error> { + openvpn_endpoint( + &query.openvpn_constraints, + &parsed_relays.parsed_list().openvpn, + relay, + ) + .map_err(|internal| Error::NoEndpoint { + internal, + relay: EndpointErrorDetails::from_openvpn(relay.clone()), + }) + } + + /// Selects a suitable bridge based on the specified settings, relay information, and transport protocol. + /// + /// # Parameters + /// - `query`: The filter criteria for selecting a bridge. + /// - `relay`: Information about the current relay, including its location. + /// - `protocol`: The transport protocol (TCP or UDP) in use. + /// - `parsed_relays`: A structured representation of all available relays. + /// - `custom_lists`: User-defined or application-specific settings that may influence bridge selection. + /// + /// # Returns + /// * On success, returns an `Option` containing the selected bridge, if one is found. Returns `None` if no suitable bridge meets the criteria or bridges should not be used. + /// * `Error::NoBridge` if attempting to use OpenVPN bridges over UDP, as this is unsupported. + /// * `Error::NoRelay` if `relay` does not have a location set. + #[cfg(not(target_os = "android"))] + fn get_openvpn_bridge( + query: &RelayQuery, + relay: &Relay, + protocol: &TransportProtocol, + parsed_relays: &ParsedRelays, + config: &NormalSelectorConfig<'_>, + ) -> 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; + match protocol { + TransportProtocol::Udp => { + log::error!("Can not use OpenVPN bridges over UDP"); + Err(Error::NoBridge) + } + TransportProtocol::Tcp => { + let location = relay.location.as_ref().ok_or(Error::NoRelay)?; + Ok(Self::get_bridge_for( + bridge_query, + location, + // FIXME: This is temporary while talpid-core only supports TCP proxies + TransportProtocol::Tcp, + parsed_relays, + custom_lists, + )) + } + } + } + } + + fn get_bridge_for( + query: &BridgeQuery, + location: &Location, + transport_protocol: TransportProtocol, + parsed_relays: &ParsedRelays, + custom_lists: &CustomListsSettings, + ) -> Option<SelectedBridge> { + match query { + BridgeQuery::Normal(settings) => { + let bridge_constraints = InternalBridgeConstraints { + location: settings.location.clone(), + providers: settings.providers.clone(), + ownership: settings.ownership, + transport_protocol: Constraint::Only(transport_protocol), + }; + + Self::get_proxy_settings( + parsed_relays, + &bridge_constraints, + Some(location), + custom_lists, + ) + .map(|(settings, relay)| SelectedBridge::Normal { settings, relay }) + } + BridgeQuery::Custom(settings) => settings.clone().map(SelectedBridge::Custom), + BridgeQuery::Off | BridgeQuery::Auto => None, + } + } + + /// Try to get a bridge that matches the given `constraints`. + /// + /// The connection details are returned alongside the relay hosting the bridge. + fn get_proxy_settings<T: Into<Coordinates>>( + parsed_relays: &ParsedRelays, + constraints: &InternalBridgeConstraints, + location: Option<T>, + custom_lists: &CustomListsSettings, + ) -> Option<(CustomProxy, Relay)> { + let bridges = filter_matching_bridges(constraints, parsed_relays.relays(), custom_lists); + let bridge = match location { + Some(location) => Self::get_proximate_bridge(bridges, location), + None => helpers::pick_random_relay(&bridges).cloned(), + }?; + + let bridge_data = &parsed_relays.parsed_list().bridge; + helpers::pick_random_bridge(bridge_data, &bridge).map(|endpoint| (endpoint, bridge.clone())) + } + + /// Try to get a bridge which is close to `location`. + fn get_proximate_bridge<T: Into<Coordinates>>( + relays: Vec<Relay>, + location: T, + ) -> Option<Relay> { + /// Minimum number of bridges to keep for selection when filtering by distance. + const MIN_BRIDGE_COUNT: usize = 5; + /// Max distance of bridges to consider for selection (km). + const MAX_BRIDGE_DISTANCE: f64 = 1500f64; + let location = location.into(); + + #[derive(Clone)] + struct RelayWithDistance { + relay: Relay, + distance: f64, + } + + // Filter out all candidate bridges. + let matching_relays: Vec<RelayWithDistance> = relays + .into_iter() + .map(|relay| RelayWithDistance { + distance: relay.location.as_ref().unwrap().distance_from(&location), + relay, + }) + .sorted_unstable_by_key(|relay| relay.distance as usize) + .take(MIN_BRIDGE_COUNT) + .filter(|relay| relay.distance <= MAX_BRIDGE_DISTANCE) + .collect(); + + // Calculate the maximum distance from `location` among the candidates. + let greatest_distance: f64 = matching_relays + .iter() + .map(|relay| relay.distance) + .reduce(f64::max)?; + // Define the weight function to prioritize bridges which are closer to `location`. + let weight_fn = |relay: &RelayWithDistance| 1 + (greatest_distance - relay.distance) as u64; + + helpers::pick_random_relay_weighted(&matching_relays, weight_fn) + .cloned() + .map(|relay_with_distance| relay_with_distance.relay) + } + + /// Returns the average location of relays that match the given constraints. + /// This returns `None` if the location is [`Constraint::Any`] or if no + /// relays match the constraints. + fn get_relay_midpoint( + query: &RelayQuery, + parsed_relays: &ParsedRelays, + config: &NormalSelectorConfig<'_>, + ) -> Option<Coordinates> { + use std::ops::Not; + if query.location.is_any() { + return None; + } + + let matching_locations: Vec<Location> = + filter_matching_relay_list(query, parsed_relays.relays(), config.custom_lists) + .into_iter() + .filter_map(|relay| relay.location) + .unique_by(|location| location.city.clone()) + .collect(); + + matching_locations + .is_empty() + .not() + .then(|| Coordinates::midpoint(&matching_locations)) + } + + /// # Returns + /// A randomly selected relay that meets the specified constraints, or `None` if no suitable relay is found. + fn choose_openvpn_relay( + query: &RelayQuery, + config: &NormalSelectorConfig<'_>, + 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); + // Pick one of the valid relays. + helpers::pick_random_relay(&candidates).cloned() + } +} diff --git a/mullvad-relay-selector/src/relay_selector/parsed_relays.rs b/mullvad-relay-selector/src/relay_selector/parsed_relays.rs new file mode 100644 index 0000000000..7b53b529ee --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/parsed_relays.rs @@ -0,0 +1,189 @@ +//! This module provides functionality for managing and updating the local relay list, +//! including support for loading these lists from disk & applying [overrides][`RelayOverride`]. +//! +//! ## Overview +//! +//! The primary structure in this module, [`ParsedRelays`], holds information about the currently +//! available relays, including any overrides that have been applied to the original list fetched +//! from the Mullvad API or loaded from a local cache. + +use std::{ + collections::HashMap, + io::{self, BufReader}, + path::Path, + time::{SystemTime, UNIX_EPOCH}, +}; + +use mullvad_types::{ + location::Location, + relay_constraints::RelayOverride, + relay_list::{Relay, RelayList}, +}; + +use crate::{constants::UDP2TCP_PORTS, error::Error}; + +pub(crate) struct ParsedRelays { + /// Tracks when the relay list was last updated. + last_updated: SystemTime, + /// The current list of relays, after applying [overrides][`RelayOverride`]. + parsed_list: RelayList, + /// The original list of relays, as returned by the Mullvad relays API. + original_list: RelayList, + overrides: Vec<RelayOverride>, +} + +impl ParsedRelays { + /// Return a flat iterator with all relays + pub fn relays(&self) -> impl Iterator<Item = &Relay> + Clone + '_ { + self.parsed_list.relays() + } + + /// Replace `self` with a new [`ParsedRelays`] based on [new_relays][`ParsedRelays`], + /// bumping `self.last_updated` to the current system time. + pub fn update(&mut self, new_relays: RelayList) { + *self = Self::from_relay_list(new_relays, SystemTime::now(), &self.overrides); + + log::info!( + "Updated relay inventory has {} relays", + self.relays().count() + ); + } + + /// Tracks when the relay list was last updated. + /// + /// The relay list can be updated by calling [`ParsedRelays::update`]. + pub const fn last_updated(&self) -> SystemTime { + self.last_updated + } + + pub fn etag(&self) -> Option<String> { + self.parsed_list.etag.clone() + } + + /// The original list of relays, as returned by the Mullvad relays API. + pub const fn original_list(&self) -> &RelayList { + &self.original_list + } + + /// The current list of relays, after applying [overrides][`RelayOverride`]. + pub const fn parsed_list(&self) -> &RelayList { + &self.parsed_list + } + + /// Replace the previous set of [overrides][`RelayOverride`] with `new_overrides`. + /// This will update `self.parsed_list` as a side-effect. + pub(crate) fn set_overrides(&mut self, new_overrides: &[RelayOverride]) { + self.parsed_list = Self::parse_relay_list(&self.original_list, new_overrides); + self.overrides = new_overrides.to_vec(); + } + + pub(crate) fn empty() -> Self { + ParsedRelays { + last_updated: UNIX_EPOCH, + parsed_list: RelayList::empty(), + original_list: RelayList::empty(), + overrides: vec![], + } + } + + /// Try to read the relays from disk, preferring the newer ones. + pub(crate) fn from_file( + cache_path: impl AsRef<Path>, + resource_path: impl AsRef<Path>, + overrides: &[RelayOverride], + ) -> Result<Self, Error> { + // prefer the resource path's relay list if the cached one doesn't exist or was modified + // before the resource one was created. + let cached_relays = Self::from_file_inner(cache_path, overrides); + let bundled_relays = match Self::from_file_inner(resource_path, overrides) { + Ok(bundled_relays) => bundled_relays, + Err(e) => { + log::error!("Failed to load bundled relays: {}", e); + return cached_relays; + } + }; + + if cached_relays + .as_ref() + .map(|cached| cached.last_updated > bundled_relays.last_updated) + .unwrap_or(false) + { + cached_relays + } else { + Ok(bundled_relays) + } + } + + fn from_file_inner(path: impl AsRef<Path>, overrides: &[RelayOverride]) -> Result<Self, Error> { + log::debug!("Reading relays from {}", path.as_ref().display()); + let (last_modified, file) = + Self::open_file(path.as_ref()).map_err(Error::OpenRelayCache)?; + let relay_list = serde_json::from_reader(BufReader::new(file)).map_err(Error::Serialize)?; + + Ok(Self::from_relay_list(relay_list, last_modified, overrides)) + } + + fn open_file(path: &Path) -> io::Result<(SystemTime, std::fs::File)> { + let file = std::fs::File::open(path)?; + let last_modified = file.metadata()?.modified()?; + Ok((last_modified, file)) + } + + /// Create a new [`ParsedRelays`] from [relay_list][`RelayList`] and [overrides][`RelayOverride`]. + /// This will apply `overrides` to `relay_list` and store the result in `self.parsed_list`. + pub(crate) fn from_relay_list( + relay_list: RelayList, + last_updated: SystemTime, + overrides: &[RelayOverride], + ) -> Self { + ParsedRelays { + last_updated, + parsed_list: Self::parse_relay_list(&relay_list, overrides), + original_list: relay_list, + overrides: overrides.to_vec(), + } + } + + /// Apply [overrides][`RelayOverride`] to [relay_list][`RelayList`], yielding an updated relay + /// list. + fn parse_relay_list(relay_list: &RelayList, overrides: &[RelayOverride]) -> RelayList { + let mut remaining_overrides = HashMap::new(); + for relay_override in overrides { + remaining_overrides.insert( + relay_override.hostname.to_owned(), + relay_override.to_owned(), + ); + } + + let mut parsed_list = relay_list.clone(); + + // Append data for obfuscation protocols ourselves, since the API does not provide it. + if parsed_list.wireguard.udp2tcp_ports.is_empty() { + parsed_list.wireguard.udp2tcp_ports.extend(UDP2TCP_PORTS); + } + + // Add location and override relay data + for country in &mut parsed_list.countries { + for city in &mut country.cities { + for relay in &mut city.relays { + // Append location data + relay.location = Some(Location { + country: country.name.clone(), + country_code: country.code.clone(), + city: city.name.clone(), + city_code: city.code.clone(), + latitude: city.latitude, + longitude: city.longitude, + }); + + // Append overrides + if let Some(overrides) = remaining_overrides.remove(&relay.hostname) { + overrides.apply_to_relay(relay); + } + } + } + } + + parsed_list + } +} diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs new file mode 100644 index 0000000000..d5b17b2303 --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -0,0 +1,916 @@ +//! This module provides a flexible way to specify 'queries' for relays. +//! +//! A query is a set of constraints that the [`crate::RelaySelector`] will use when filtering out +//! potential relays that the daemon should connect to. It supports filtering relays by geographic location, +//! provider, ownership, and tunnel protocol, along with protocol-specific settings for WireGuard and OpenVPN. +//! +//! The main components of this module include: +//! +//! - [`RelayQuery`]: The core struct for specifying a query to select relay servers. It +//! aggregates constraints on location, providers, ownership, tunnel protocol, and +//! protocol-specific constraints for WireGuard and OpenVPN. +//! - [`WireguardRelayQuery`] and [`OpenVpnRelayQuery`]: Structs that define protocol-specific +//! constraints for selecting WireGuard and OpenVPN relays, respectively. +//! - [`Intersection`]: A trait implemented by the different query types that support intersection logic, +//! which allows for combining two queries into a single query that represents the common constraints of both. +//! - [Builder patterns][builder]: The module also provides builder patterns for creating instances +//! of `RelayQuery`, `WireguardRelayQuery`, and `OpenVpnRelayQuery` with a fluent API. +//! +//! ## Design +//! +//! This module has been built in such a way that it should be easy to reason about, +//! while providing a flexible and easy-to-use API. The `Intersection` trait provides +//! a robust framework for combining and refining queries based on multiple criteria. +//! +//! The builder patterns included in the module simplify the process of constructing +//! queries and ensure that queries are built in a type-safe manner, reducing the risk +//! of runtime errors and improving code readability. + +use mullvad_types::{ + constraints::Constraint, + relay_constraints::{ + BridgeConstraints, LocationConstraint, OpenVpnConstraints, Ownership, Providers, + RelayConstraints, SelectedObfuscation, TransportPort, Udp2TcpObfuscationSettings, + WireguardConstraints, + }, +}; +use talpid_types::net::{proxy::CustomProxy, IpVersion, TunnelType}; + +/// Represents a query for a relay based on various constraints. +/// +/// This struct contains constraints for the location, providers, ownership, +/// tunnel protocol, and additional protocol-specific constraints for WireGuard +/// and OpenVPN. These constraints are used by the [`crate::RelaySelector`] to +/// filter and select suitable relay servers that match the specified criteria. +/// +/// A [`RelayQuery`] is best constructed via the fluent builder API exposed by +/// [`builder::RelayQueryBuilder`]. +/// +/// # Examples +/// +/// Creating a basic `RelayQuery` to filter relays by location, ownership and tunnel protocol: +/// +/// ```rust +/// // Create a query for a Wireguard relay that is owned by Mullvad and located in Norway. +/// // The endpoint should specify port 443. +/// use mullvad_relay_selector::query::RelayQuery; +/// use mullvad_relay_selector::query::builder::RelayQueryBuilder; +/// use mullvad_relay_selector::query::builder::{Ownership, GeographicLocationConstraint}; +/// +/// let query: RelayQuery = RelayQueryBuilder::new() +/// .wireguard() // Specify the tunnel protocol +/// .location(GeographicLocationConstraint::country("no")) // Specify the country as Norway +/// .ownership(Ownership::MullvadOwned) // Specify that the relay must be owned by Mullvad +/// .port(443) // Specify the port to use when connecting to the relay +/// .build(); // Construct the query +/// ``` +/// +/// This example demonstrates creating a `RelayQuery` which can then be passed +/// to the [`crate::RelaySelector`] to find a relay that matches the criteria. +/// See [`builder`] for more info on how to construct queries. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RelayQuery { + pub location: Constraint<LocationConstraint>, + pub providers: Constraint<Providers>, + pub ownership: Constraint<Ownership>, + pub tunnel_protocol: Constraint<TunnelType>, + pub wireguard_constraints: WireguardRelayQuery, + pub openvpn_constraints: OpenVpnRelayQuery, +} + +impl RelayQuery { + /// Create a new [`RelayQuery`] with no opinionated defaults. This query matches every relay + /// with any configuration by setting each of its fields to [`Constraint::Any`]. Should be the + /// const equivalent to [`Default::default`]. + /// + /// Note that the following identity applies for any `other_query`: + /// ```rust + /// # use mullvad_relay_selector::query::RelayQuery; + /// # use crate::mullvad_relay_selector::query::Intersection; + /// + /// # let other_query = RelayQuery::new(); + /// assert_eq!(RelayQuery::new().intersection(other_query.clone()), Some(other_query)); + /// # let other_query = RelayQuery::new(); + /// assert_eq!(other_query.clone().intersection(RelayQuery::new()), Some(other_query)); + /// ``` + pub const fn new() -> RelayQuery { + RelayQuery { + location: Constraint::Any, + providers: Constraint::Any, + ownership: Constraint::Any, + tunnel_protocol: Constraint::Any, + wireguard_constraints: WireguardRelayQuery::new(), + openvpn_constraints: OpenVpnRelayQuery::new(), + } + } +} + +impl Intersection for RelayQuery { + /// Return a new [`RelayQuery`] which matches the intersected queries. + /// + /// * If two [`RelayQuery`]s differ such that no relay matches both, [`Option::None`] is returned: + /// ```rust + /// # use mullvad_relay_selector::query::builder::RelayQueryBuilder; + /// # use crate::mullvad_relay_selector::query::Intersection; + /// let query_a = RelayQueryBuilder::new().wireguard().build(); + /// let query_b = RelayQueryBuilder::new().openvpn().build(); + /// assert_eq!(query_a.intersection(query_b), None); + /// ``` + /// + /// * Otherwise, a new [`RelayQuery`] is returned where each constraint is + /// as specific as possible. See [`Constraint`] for further details. + /// ```rust + /// # use crate::mullvad_relay_selector::*; + /// # use crate::mullvad_relay_selector::query::*; + /// # use crate::mullvad_relay_selector::query::builder::*; + /// # use mullvad_types::relay_list::*; + /// # use talpid_types::net::wireguard::PublicKey; + /// + /// // The relay list used by `relay_selector` in this example + /// let relay_list = RelayList { + /// # etag: None, + /// # openvpn: OpenVpnEndpointData { ports: vec![] }, + /// # bridge: BridgeEndpointData { + /// # shadowsocks: vec![], + /// # }, + /// # wireguard: WireguardEndpointData { + /// # port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)], + /// # ipv4_gateway: "10.64.0.1".parse().unwrap(), + /// # ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), + /// # udp2tcp_ports: vec![], + /// # }, + /// countries: vec![RelayListCountry { + /// name: "Sweden".to_string(), + /// # code: "Sweden".to_string(), + /// cities: vec![RelayListCity { + /// name: "Gothenburg".to_string(), + /// # code: "Gothenburg".to_string(), + /// # latitude: 57.70887, + /// # longitude: 11.97456, + /// relays: vec![Relay { + /// hostname: "se9-wireguard".to_string(), + /// ipv4_addr_in: "185.213.154.68".parse().unwrap(), + /// # ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), + /// # include_in_country: false, + /// # active: true, + /// # owned: true, + /// # provider: "31173".to_string(), + /// # weight: 1, + /// # endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + /// # public_key: PublicKey::from_base64( + /// # "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + /// # ) + /// # .unwrap(), + /// # }), + /// # location: None, + /// }], + /// }], + /// }], + /// }; + /// + /// # let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list.clone()); + /// # let city = |country, city| GeographicLocationConstraint::city(country, city); + /// + /// let query_a = RelayQueryBuilder::new().wireguard().build(); + /// let query_b = RelayQueryBuilder::new().location(city("Sweden", "Gothenburg")).build(); + /// + /// let result = relay_selector.get_relay_by_query(query_a.intersection(query_b).unwrap()); + /// assert!(result.is_ok()); + /// ``` + /// + /// This way, if the mullvad app wants to check if the user's relay settings + /// are compatible with any other [`RelayQuery`], for examples those defined by + /// [`RETRY_ORDER`] , taking the intersection between them will never result in + /// a situation where the app can override the user's preferences. + /// + /// [`RETRY_ORDER`]: crate::RETRY_ORDER + fn intersection(self, other: Self) -> Option<Self> + where + Self: PartialEq, + Self: Sized, + { + Some(RelayQuery { + location: self.location.intersection(other.location)?, + providers: self.providers.intersection(other.providers)?, + ownership: self.ownership.intersection(other.ownership)?, + tunnel_protocol: self.tunnel_protocol.intersection(other.tunnel_protocol)?, + wireguard_constraints: self + .wireguard_constraints + .intersection(other.wireguard_constraints)?, + openvpn_constraints: self + .openvpn_constraints + .intersection(other.openvpn_constraints)?, + }) + } +} + +impl From<RelayQuery> for RelayConstraints { + /// The mapping from [`RelayQuery`] to [`RelayConstraints`]. + fn from(value: RelayQuery) -> Self { + RelayConstraints { + location: value.location, + providers: value.providers, + ownership: value.ownership, + tunnel_protocol: value.tunnel_protocol, + wireguard_constraints: WireguardConstraints::from(value.wireguard_constraints), + openvpn_constraints: OpenVpnConstraints::from(value.openvpn_constraints), + } + } +} + +/// A query for a relay with Wireguard-specific properties, such as `multihop` and [wireguard obfuscation][`SelectedObfuscation`]. +/// +/// This struct may look a lot like [`WireguardConstraints`], and that is the point! +/// This struct is meant to be that type in the "universe of relay queries". The difference +/// between them may seem subtle, but in a [`WireguardRelayQuery`] every field is represented +/// as a [`Constraint`], which allow us to implement [`Intersection`] in a straight forward manner. +/// Notice that [obfuscation][`SelectedObfuscation`] is not a [`Constraint`], but it is trivial +/// to define [`Intersection`] on it, so it is fine. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct WireguardRelayQuery { + pub port: Constraint<u16>, + pub ip_version: Constraint<IpVersion>, + pub use_multihop: Constraint<bool>, + pub entry_location: Constraint<LocationConstraint>, + pub obfuscation: SelectedObfuscation, + pub udp2tcp_port: Constraint<Udp2TcpObfuscationSettings>, +} + +impl WireguardRelayQuery { + pub fn multihop(&self) -> bool { + matches!(self.use_multihop, Constraint::Only(true)) + } +} + +impl WireguardRelayQuery { + pub const fn new() -> WireguardRelayQuery { + WireguardRelayQuery { + port: Constraint::Any, + ip_version: Constraint::Any, + use_multihop: Constraint::Any, + entry_location: Constraint::Any, + obfuscation: SelectedObfuscation::Auto, + udp2tcp_port: Constraint::Any, + } + } +} +impl Intersection for WireguardRelayQuery { + fn intersection(self, other: Self) -> Option<Self> + where + Self: PartialEq, + Self: Sized, + { + Some(WireguardRelayQuery { + port: self.port.intersection(other.port)?, + ip_version: self.ip_version.intersection(other.ip_version)?, + use_multihop: self.use_multihop.intersection(other.use_multihop)?, + entry_location: self.entry_location.intersection(other.entry_location)?, + obfuscation: self.obfuscation.intersection(other.obfuscation)?, + udp2tcp_port: self.udp2tcp_port.intersection(other.udp2tcp_port)?, + }) + } +} + +impl Intersection for SelectedObfuscation { + fn intersection(self, other: Self) -> Option<Self> + where + Self: PartialEq, + Self: Sized, + { + match (self, other) { + (left, SelectedObfuscation::Auto) => Some(left), + (SelectedObfuscation::Auto, right) => Some(right), + (left, right) if left == right => Some(left), + _ => None, + } + } +} + +impl From<WireguardRelayQuery> for WireguardConstraints { + /// The mapping from [`WireguardRelayQuery`] to [`WireguardConstraints`]. + fn from(value: WireguardRelayQuery) -> Self { + WireguardConstraints { + port: value.port, + ip_version: value.ip_version, + entry_location: value.entry_location, + use_multihop: value.use_multihop.unwrap_or(false), + } + } +} + +/// A query for a relay with OpenVPN-specific properties, such as `bridge_settings`. +/// +/// This struct may look a lot like [`OpenVpnConstraints`], and that is the point! +/// This struct is meant to be that type in the "universe of relay queries". The difference +/// between them may seem subtle, but in a [`OpenVpnRelayQuery`] every field is represented +/// as a [`Constraint`], which allow us to implement [`Intersection`] in a straight forward manner. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct OpenVpnRelayQuery { + pub port: Constraint<TransportPort>, + pub bridge_settings: Constraint<BridgeQuery>, +} + +impl OpenVpnRelayQuery { + pub const fn new() -> OpenVpnRelayQuery { + OpenVpnRelayQuery { + port: Constraint::Any, + bridge_settings: Constraint::Any, + } + } +} + +impl Intersection for OpenVpnRelayQuery { + fn intersection(self, other: Self) -> Option<Self> + where + Self: PartialEq, + Self: Sized, + { + let bridge_settings = { + match (self.bridge_settings, other.bridge_settings) { + // Recursive case + (Constraint::Only(left), Constraint::Only(right)) => { + Constraint::Only(left.intersection(right)?) + } + (left, right) => left.intersection(right)?, + } + }; + Some(OpenVpnRelayQuery { + port: self.port.intersection(other.port)?, + bridge_settings, + }) + } +} + +/// This is the reflection of [`BridgeState`] + [`BridgeSettings`] in the "universe of relay queries". +/// +/// [`BridgeState`]: mullvad_types::relay_constraints::BridgeState +/// [`BridgeSettings`]: mullvad_types::relay_constraints::BridgeSettings +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum BridgeQuery { + /// Bridges should not be used. + Off, + /// Don't care, let the relay selector choose! + /// + /// If this variant is intersected with another [`BridgeQuery`] `bq`, + /// `bq` is always preferred. + Auto, + /// Bridges should be used. + Normal(BridgeConstraints), + /// Bridges should be used. + Custom(Option<CustomProxy>), +} + +impl BridgeQuery { + ///If `bridge_constraints` is `Any`, bridges should not be used due to + /// latency concerns. + /// + /// If `bridge_constraints` is `Only(settings)`, then `settings` will be + /// used to decide if bridges should be used. See [`BridgeQuery`] for more + /// details, but the algorithm beaks down to this: + /// + /// * `BridgeQuery::Off`: bridges will not be used + /// * otherwise: bridges should be used + pub const fn should_use_bridge(bridge_constraints: &Constraint<BridgeQuery>) -> bool { + match bridge_constraints { + Constraint::Only(settings) => match settings { + BridgeQuery::Normal(_) | BridgeQuery::Custom(_) => true, + BridgeQuery::Off | BridgeQuery::Auto => false, + }, + Constraint::Any => false, + } + } +} + +impl Intersection for BridgeQuery { + fn intersection(self, other: Self) -> Option<Self> + where + Self: PartialEq, + Self: Sized, + { + match (self, other) { + (BridgeQuery::Normal(left), BridgeQuery::Normal(right)) => { + Some(BridgeQuery::Normal(left.intersection(right)?)) + } + (BridgeQuery::Auto, right) => Some(right), + (left, BridgeQuery::Auto) => Some(left), + (left, right) if left == right => Some(left), + _ => None, + } + } +} + +impl Intersection for BridgeConstraints { + fn intersection(self, other: Self) -> Option<Self> + where + Self: PartialEq, + Self: Sized, + { + Some(BridgeConstraints { + location: self.location.intersection(other.location)?, + providers: self.providers.intersection(other.providers)?, + ownership: self.ownership.intersection(other.ownership)?, + }) + } +} + +impl From<OpenVpnRelayQuery> for OpenVpnConstraints { + /// The mapping from [`OpenVpnRelayQuery`] to [`OpenVpnConstraints`]. + fn from(value: OpenVpnRelayQuery) -> Self { + OpenVpnConstraints { port: value.port } + } +} + +/// Any type that wish to implement `Intersection` should make sure that the +/// following properties are upheld: +/// +/// - idempotency (if there is an identity element) +/// - commutativity +/// - associativity +pub trait Intersection { + fn intersection(self, other: Self) -> Option<Self> + where + Self: Sized; +} + +impl<T: Intersection> Intersection for Constraint<T> { + /// Define the intersection between two arbitrary [`Constraint`]s. + /// + /// This operation may be compared to the set operation with the same name. + /// In contrast to the general set intersection, this function represents a + /// very specific case where [`Constraint::Any`] is equivalent to the set + /// universe and [`Constraint::Only`] represents a singleton set. Notable is + /// that the representation of any empty set is [`Option::None`]. + fn intersection(self, other: Constraint<T>) -> Option<Constraint<T>> { + use Constraint::*; + match (self, other) { + (Any, Any) => Some(Any), + (Only(t), Any) | (Any, Only(t)) => Some(Only(t)), + // Recurse on `left` and `right` to see if there exist an intersection + (Only(left), Only(right)) => Some(Only(left.intersection(right)?)), + } + } +} + +// Implement `Intersection` for different types + +impl Intersection for Providers { + fn intersection(self, other: Self) -> Option<Self> + where + Self: Sized, + { + Providers::new(self.providers().intersection(other.providers())).ok() + } +} + +impl Intersection for Udp2TcpObfuscationSettings { + fn intersection(self, other: Self) -> Option<Self> + where + Self: Sized, + { + Some(Udp2TcpObfuscationSettings { + port: self.port.intersection(other.port)?, + }) + } +} + +impl Intersection for TransportPort { + fn intersection(self, other: Self) -> Option<Self> + where + Self: Sized, + { + let protocol = if self.protocol == other.protocol { + Some(self.protocol) + } else { + None + }?; + let port = self.port.intersection(other.port)?; + Some(TransportPort { protocol, port }) + } +} + +/// Auto-implement `Intersection` for trivial cases where the logic should just check if +/// `self` is equal to `other`. +macro_rules! impl_intersection_partialeq { + ($ty:ty) => { + impl Intersection for $ty { + fn intersection(self, other: Self) -> Option<Self> { + if self == other { + Some(self) + } else { + None + } + } + } + }; +} +impl_intersection_partialeq!(u16); +impl_intersection_partialeq!(bool); +// FIXME: [`LocationConstraint`] deserves a hand-rolled implementation of [`Intersection`], but +// it would probably be best to implement it for [`ResolvedLocationConstraint`] instead to properly +// handle custom lists. +impl_intersection_partialeq!(LocationConstraint); +impl_intersection_partialeq!(Ownership); +impl_intersection_partialeq!(talpid_types::net::TransportProtocol); +impl_intersection_partialeq!(talpid_types::net::TunnelType); +impl_intersection_partialeq!(talpid_types::net::IpVersion); + +#[allow(unused)] +pub mod builder { + //! Strongly typed Builder pattern for of relay constraints though the use of the Typestate pattern. + use mullvad_types::{ + constraints::Constraint, + relay_constraints::{ + BridgeConstraints, LocationConstraint, RelayConstraints, SelectedObfuscation, + TransportPort, Udp2TcpObfuscationSettings, + }, + }; + use talpid_types::net::TunnelType; + + use super::{BridgeQuery, RelayQuery}; + + // Re-exports + pub use mullvad_types::relay_constraints::{ + GeographicLocationConstraint, Ownership, Providers, + }; + pub use talpid_types::net::{IpVersion, TransportProtocol}; + + /// Internal builder state for a [`RelayQuery`] parameterized over the + /// type of VPN tunnel protocol. Some [`RelayQuery`] options are + /// generic over the VPN protocol, while some options are protocol-specific. + /// + /// - The type parameter `VpnProtocol` keeps track of which VPN protocol that + /// is being configured. Different instantiations of `VpnProtocol` will + /// expose different functions for configuring a [`RelayQueryBuilder`] + /// further. + pub struct RelayQueryBuilder<VpnProtocol = Any> { + query: RelayQuery, + protocol: VpnProtocol, + } + + /// The `Any` type is equivalent to the `Constraint::Any` value. If a + /// type-parameter is of type `Any`, it means that the corresponding value + /// in the final `RelayQuery` is `Constraint::Any`. + pub struct Any; + + // This impl-block is quantified over all configurations, e.g. [`Any`], + // [`WireguardRelayQuery`] & [`OpenVpnRelayQuery`] + impl<VpnProtocol> RelayQueryBuilder<VpnProtocol> { + /// Configure the [`LocationConstraint`] to use. + pub fn location(mut self, location: GeographicLocationConstraint) -> Self { + self.query.location = Constraint::Only(LocationConstraint::from(location)); + self + } + + /// Configure which [`Ownership`] to use. + pub const fn ownership(mut self, ownership: Ownership) -> Self { + self.query.ownership = Constraint::Only(ownership); + self + } + + /// Configure which [`Providers`] to use. + pub fn providers(mut self, providers: Providers) -> Self { + self.query.providers = Constraint::Only(providers); + self + } + + /// Assemble the final [`RelayQuery`] that has been configured + /// through `self`. + pub fn build(self) -> RelayQuery { + self.query + } + + pub fn into_constraint(self) -> RelayConstraints { + RelayConstraints::from(self.build()) + } + } + + impl RelayQueryBuilder<Any> { + /// Create a new [`RelayQueryBuilder`] with unopinionated defaults. + /// + /// Call [`Self::build`] to convert the builder into a [`RelayQuery`], + /// which is used to guide the [`RelaySelector`] + /// + /// [`RelaySelector`]: crate::RelaySelector + pub const fn new() -> RelayQueryBuilder<Any> { + RelayQueryBuilder { + query: RelayQuery::new(), + protocol: Any, + } + } + /// Set the VPN protocol for this [`RelayQueryBuilder`] to Wireguard. + pub fn wireguard(mut self) -> RelayQueryBuilder<Wireguard<Any, Any>> { + let protocol = Wireguard { + multihop: Any, + obfuscation: Any, + }; + self.query.tunnel_protocol = Constraint::Only(TunnelType::Wireguard); + // Update the type state + RelayQueryBuilder { + query: self.query, + protocol, + } + } + + /// Set the VPN protocol for this [`RelayQueryBuilder`] to OpenVPN. + pub fn openvpn(mut self) -> RelayQueryBuilder<OpenVPN<Any, Any>> { + let protocol = OpenVPN { + transport_port: Any, + bridge_settings: Any, + }; + self.query.tunnel_protocol = Constraint::Only(TunnelType::OpenVpn); + // Update the type state + RelayQueryBuilder { + query: self.query, + protocol, + } + } + } + + // Type-safe builder for Wireguard relay constraints. + + /// Internal builder state for a [`WireguardRelayQuery`] configuration. + /// + /// - The type parameter `Multihop` keeps track of the state of multihop. + /// If multihop has been enabled, the builder should expose an option to + /// select entry point. + /// + /// [`WireguardRelayQuery`]: super::WireguardRelayQuery + pub struct Wireguard<Multihop, Obfuscation> { + multihop: Multihop, + obfuscation: Obfuscation, + } + + // This impl-block is quantified over all configurations + impl<Multihop, Obfuscation> RelayQueryBuilder<Wireguard<Multihop, Obfuscation>> { + /// Specify the port to ues when connecting to the selected + /// Wireguard relay. + pub const fn port(mut self, port: u16) -> Self { + self.query.wireguard_constraints.port = Constraint::Only(port); + self + } + + /// Set the [`IpVersion`] to use when connecting to the selected + /// Wireguard relay. + pub const fn ip_version(mut self, ip_version: IpVersion) -> Self { + self.query.wireguard_constraints.ip_version = Constraint::Only(ip_version); + self + } + } + + impl<Obfuscation> RelayQueryBuilder<Wireguard<Any, Obfuscation>> { + /// Enable multihop. + /// + /// To configure the entry relay, see [`RelayQueryBuilder::entry`]. + pub fn multihop(mut self) -> RelayQueryBuilder<Wireguard<bool, Obfuscation>> { + self.query.wireguard_constraints.use_multihop = Constraint::Only(true); + // Update the type state + RelayQueryBuilder { + query: self.query, + protocol: Wireguard { + multihop: true, + obfuscation: self.protocol.obfuscation, + }, + } + } + } + + impl<Obfuscation> RelayQueryBuilder<Wireguard<bool, Obfuscation>> { + /// Set the entry location in a multihop configuration. This requires + /// multihop to be enabled. + pub fn entry(mut self, location: GeographicLocationConstraint) -> Self { + self.query.wireguard_constraints.entry_location = + Constraint::Only(LocationConstraint::from(location)); + self + } + } + + impl<Multihop> RelayQueryBuilder<Wireguard<Multihop, Any>> { + /// Enable `UDP2TCP` obufscation. This will in turn enable the option to configure the + /// `UDP2TCP` port. + pub fn udp2tcp( + mut self, + ) -> RelayQueryBuilder<Wireguard<Multihop, Udp2TcpObfuscationSettings>> { + let obfuscation = Udp2TcpObfuscationSettings { + port: Constraint::Any, + }; + let protocol = Wireguard { + multihop: self.protocol.multihop, + obfuscation: obfuscation.clone(), + }; + self.query.wireguard_constraints.udp2tcp_port = Constraint::Only(obfuscation); + self.query.wireguard_constraints.obfuscation = SelectedObfuscation::Udp2Tcp; + RelayQueryBuilder { + query: self.query, + protocol, + } + } + } + + impl<Multihop> RelayQueryBuilder<Wireguard<Multihop, Udp2TcpObfuscationSettings>> { + /// Set the `UDP2TCP` port. This is the TCP port which the `UDP2TCP` obfuscation + /// protocol should use to connect to a relay. + pub fn udp2tcp_port(mut self, port: u16) -> Self { + self.protocol.obfuscation.port = Constraint::Only(port); + self.query.wireguard_constraints.udp2tcp_port = + Constraint::Only(self.protocol.obfuscation.clone()); + self + } + } + + // Type-safe builder pattern for OpenVPN relay constraints. + + /// Internal builder state for a [`OpenVpnRelayQuery`] configuration. + /// + /// - The type parameter `TransportPort` keeps track of which + /// [`TransportProtocol`] & port-combo to use. [`TransportProtocol`] has + /// to be set first before the option to select a specific port is + /// exposed. + /// + /// [`OpenVpnRelayQuery`]: super::OpenVpnRelayQuery + pub struct OpenVPN<TransportPort, Bridge> { + transport_port: TransportPort, + bridge_settings: Bridge, + } + + // This impl-block is quantified over all configurations + impl<Transport, Bridge> RelayQueryBuilder<OpenVPN<Transport, Bridge>> { + /// Configure what [`TransportProtocol`] to use. Calling this + /// function on a builder will expose the option to select which + /// port to use in combination with `protocol`. + pub fn transport_protocol( + mut self, + protocol: TransportProtocol, + ) -> RelayQueryBuilder<OpenVPN<TransportProtocol, Bridge>> { + let transport_port = TransportPort { + protocol, + port: Constraint::Any, + }; + self.query.openvpn_constraints.port = Constraint::Only(transport_port); + // Update the type state + RelayQueryBuilder { + query: self.query, + protocol: OpenVPN { + transport_port: protocol, + bridge_settings: self.protocol.bridge_settings, + }, + } + } + } + + impl<Bridge> RelayQueryBuilder<OpenVPN<TransportProtocol, Bridge>> { + /// Configure what port to use when connecting to a relay. + pub fn port(mut self, port: u16) -> RelayQueryBuilder<OpenVPN<TransportPort, Bridge>> { + let port = Constraint::Only(port); + let transport_port = TransportPort { + protocol: self.protocol.transport_port, + port, + }; + self.query.openvpn_constraints.port = Constraint::Only(transport_port); + // Update the type state + RelayQueryBuilder { + query: self.query, + protocol: OpenVPN { + transport_port, + bridge_settings: self.protocol.bridge_settings, + }, + } + } + } + + impl<Transport> RelayQueryBuilder<OpenVPN<Transport, Any>> { + /// Enable Bridges. This also sets the transport protocol to TCP and resets any + /// previous port settings. + pub fn bridge( + mut self, + ) -> RelayQueryBuilder<OpenVPN<TransportProtocol, BridgeConstraints>> { + let bridge_settings = BridgeConstraints { + location: Constraint::Any, + providers: Constraint::Any, + ownership: Constraint::Any, + }; + + let protocol = OpenVPN { + transport_port: self.protocol.transport_port, + bridge_settings: bridge_settings.clone(), + }; + + self.query.openvpn_constraints.bridge_settings = + Constraint::Only(BridgeQuery::Normal(bridge_settings)); + + let builder = RelayQueryBuilder { + query: self.query, + protocol, + }; + + builder.transport_protocol(TransportProtocol::Tcp) + } + } + + impl<Transport> RelayQueryBuilder<OpenVPN<Transport, BridgeConstraints>> { + /// Constraint the geographical location of the selected bridge. + pub fn bridge_location(mut self, location: GeographicLocationConstraint) -> Self { + self.protocol.bridge_settings.location = + Constraint::Only(LocationConstraint::from(location)); + self.query.openvpn_constraints.bridge_settings = + Constraint::Only(BridgeQuery::Normal(self.protocol.bridge_settings.clone())); + self + } + /// Constrain the [`Providers`] of the selected bridge. + pub fn bridge_providers(mut self, providers: Providers) -> Self { + self.protocol.bridge_settings.providers = Constraint::Only(providers); + self.query.openvpn_constraints.bridge_settings = + Constraint::Only(BridgeQuery::Normal(self.protocol.bridge_settings.clone())); + self + } + /// Constrain the [`Ownership`] of the selected bridge. + pub fn bridge_ownership(mut self, ownership: Ownership) -> Self { + self.protocol.bridge_settings.ownership = Constraint::Only(ownership); + self + } + } +} + +#[cfg(test)] +mod test { + use mullvad_types::constraints::Constraint; + use proptest::prelude::*; + + use super::Intersection; + + // Define proptest combinators for the `Constraint` type. + + pub fn constraint<T>( + base_strategy: impl Strategy<Value = T> + 'static, + ) -> impl Strategy<Value = Constraint<T>> + where + T: core::fmt::Debug + std::clone::Clone + 'static, + { + prop_oneof![any(), only(base_strategy),] + } + + pub fn only<T>( + base_strategy: impl Strategy<Value = T> + 'static, + ) -> impl Strategy<Value = Constraint<T>> + where + T: core::fmt::Debug + std::clone::Clone + 'static, + { + base_strategy.prop_map(Constraint::Only) + } + + pub fn any<T>() -> impl Strategy<Value = Constraint<T>> + where + T: core::fmt::Debug + std::clone::Clone + 'static, + { + Just(Constraint::Any) + } + + proptest! { + #[test] + fn identity(x in only(proptest::arbitrary::any::<bool>())) { + // Identity laws + // x ∩ identity = x + // identity ∩ x = x + + // The identity element + let identity = Constraint::Any; + prop_assert_eq!(x.intersection(identity), x.into()); + prop_assert_eq!(identity.intersection(x), x.into()); + } + + #[test] + fn idempotency (x in constraint(proptest::arbitrary::any::<bool>())) { + // Idempotency law + // x ∩ x = x + prop_assert_eq!(x.intersection(x), x.into()) // lift x to the return type of `intersection` + } + + #[test] + fn commutativity(x in constraint(proptest::arbitrary::any::<bool>()), + y in constraint(proptest::arbitrary::any::<bool>())) { + // Commutativity law + // x ∩ y = y ∩ x + prop_assert_eq!(x.intersection(y), y.intersection(x)) + } + + #[test] + fn associativity(x in constraint(proptest::arbitrary::any::<bool>()), + y in constraint(proptest::arbitrary::any::<bool>()), + z in constraint(proptest::arbitrary::any::<bool>())) + { + // Associativity law + // (x ∩ y) ∩ z = x ∩ (y ∩ z) + let left: Option<_> = { + x.intersection(y).and_then(|xy| xy.intersection(z)) + }; + let right: Option<_> = { + // It is fine to rewrite the order of the application from + // x ∩ (y ∩ z) + // to + // (y ∩ z) ∩ x + // due to the commutative property of intersection + (y.intersection(z)).and_then(|yz| yz.intersection(x)) + }; + prop_assert_eq!(left, right); + } + } +} diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs new file mode 100644 index 0000000000..99a5491fad --- /dev/null +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -0,0 +1,1112 @@ +//! Tests for verifying that the relay selector works as expected. + +use once_cell::sync::Lazy; +use std::collections::HashSet; +use talpid_types::net::{ + obfuscation::ObfuscatorConfig, + wireguard::PublicKey, + Endpoint, + TransportProtocol::{Tcp, Udp}, + TunnelType, +}; + +use mullvad_relay_selector::{ + query::{builder::RelayQueryBuilder, BridgeQuery, OpenVpnRelayQuery}, + Error, GetRelay, RelaySelector, RuntimeParameters, SelectorConfig, WireguardConfig, + RETRY_ORDER, +}; +use mullvad_types::{ + constraints::Constraint, + endpoint::MullvadEndpoint, + relay_constraints::{ + BridgeConstraints, BridgeState, GeographicLocationConstraint, Ownership, Providers, + SelectedObfuscation, TransportPort, + }, + relay_list::{ + BridgeEndpointData, OpenVpnEndpoint, OpenVpnEndpointData, Relay, RelayEndpointData, + RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData, WireguardEndpointData, + WireguardRelayEndpointData, + }, +}; + +static RELAYS: Lazy<RelayList> = Lazy::new(|| RelayList { + etag: None, + countries: vec![RelayListCountry { + name: "Sweden".to_string(), + code: "se".to_string(), + cities: vec![RelayListCity { + name: "Gothenburg".to_string(), + code: "got".to_string(), + latitude: 57.70887, + longitude: 11.97456, + relays: vec![ + Relay { + hostname: "se9-wireguard".to_string(), + ipv4_addr_in: "185.213.154.68".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), + include_in_country: true, + active: true, + owned: true, + provider: "provider0".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + }), + location: None, + }, + Relay { + hostname: "se10-wireguard".to_string(), + ipv4_addr_in: "185.213.154.69".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a10f".parse().unwrap()), + include_in_country: true, + active: true, + owned: false, + provider: "provider1".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + }), + location: None, + }, + Relay { + hostname: "se-got-001".to_string(), + ipv4_addr_in: "185.213.154.131".parse().unwrap(), + ipv6_addr_in: None, + include_in_country: true, + active: true, + owned: true, + provider: "provider2".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Openvpn, + location: None, + }, + Relay { + hostname: "se-got-002".to_string(), + ipv4_addr_in: "1.2.3.4".parse().unwrap(), + ipv6_addr_in: None, + include_in_country: true, + active: true, + owned: true, + provider: "provider0".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Openvpn, + location: None, + }, + Relay { + hostname: "se-got-br-001".to_string(), + ipv4_addr_in: "1.3.3.7".parse().unwrap(), + ipv6_addr_in: None, + include_in_country: true, + active: true, + owned: true, + provider: "provider3".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Bridge, + location: None, + }, + ], + }], + }], + openvpn: OpenVpnEndpointData { + ports: vec![ + OpenVpnEndpoint { + port: 1194, + protocol: Udp, + }, + OpenVpnEndpoint { + port: 443, + protocol: Tcp, + }, + OpenVpnEndpoint { + port: 80, + protocol: Tcp, + }, + ], + }, + bridge: BridgeEndpointData { + shadowsocks: vec![ + ShadowsocksEndpointData { + port: 443, + cipher: "aes-256-gcm".to_string(), + password: "mullvad".to_string(), + protocol: Tcp, + }, + ShadowsocksEndpointData { + port: 1234, + cipher: "aes-256-cfb".to_string(), + password: "mullvad".to_string(), + protocol: Udp, + }, + ShadowsocksEndpointData { + port: 1236, + cipher: "aes-256-gcm".to_string(), + password: "mullvad".to_string(), + protocol: Udp, + }, + ], + }, + wireguard: WireguardEndpointData { + port_ranges: vec![ + (53, 53), + (443, 443), + (4000, 33433), + (33565, 51820), + (52000, 60000), + ], + ipv4_gateway: "10.64.0.1".parse().unwrap(), + ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), + udp2tcp_ports: vec![], + }, +}); + +// Helper functions +fn unwrap_relay(get_result: GetRelay) -> Relay { + match get_result { + GetRelay::Wireguard { inner, .. } => match inner { + crate::WireguardConfig::Singlehop { exit } => exit, + crate::WireguardConfig::Multihop { exit, .. } => exit, + }, + GetRelay::OpenVpn { exit, .. } => exit, + GetRelay::Custom(custom) => { + panic!("Can not extract regular relay from custom relay: {custom}") + } + } +} + +fn unwrap_endpoint(get_result: GetRelay) -> MullvadEndpoint { + match get_result { + GetRelay::Wireguard { endpoint, .. } => MullvadEndpoint::Wireguard(endpoint), + GetRelay::OpenVpn { endpoint, .. } => MullvadEndpoint::OpenVpn(endpoint), + GetRelay::Custom(custom) => { + panic!("Can not extract Mullvad endpoint from custom relay: {custom}") + } + } +} + +fn tunnel_type(relay: &Relay) -> TunnelType { + match relay.endpoint_data { + RelayEndpointData::Openvpn | RelayEndpointData::Bridge => TunnelType::OpenVpn, + RelayEndpointData::Wireguard(_) => TunnelType::Wireguard, + } +} + +fn default_relay_selector() -> RelaySelector { + RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()) +} + +/// This is not an actual test. Rather, it serves as a reminder that if [`RETRY_ORDER`] is modified, +/// the programmer should be made aware to update all external documents which rely on the retry order +/// to be correct. +/// +/// When all necessary changes have been made, feel free to update this test to mirror the new [`RETRY_ORDER`]. +#[test] +fn assert_retry_order() { + use talpid_types::net::{IpVersion, TransportProtocol}; + let expected_retry_order = vec![ + // 1 + RelayQueryBuilder::new().build(), + // 2 + RelayQueryBuilder::new().wireguard().port(443).build(), + // 3 + RelayQueryBuilder::new() + .wireguard() + .ip_version(IpVersion::V6) + .build(), + // 4 + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(TransportProtocol::Tcp) + .port(443) + .build(), + // 5 + RelayQueryBuilder::new().wireguard().udp2tcp().build(), + // 6 + RelayQueryBuilder::new() + .wireguard() + .udp2tcp() + .ip_version(IpVersion::V6) + .build(), + // 7 + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(TransportProtocol::Tcp) + .bridge() + .build(), + ]; + + assert!( + *RETRY_ORDER == expected_retry_order, + " + The relay selector's retry order has been modified! + Make sure to update `docs/relay-selector.md` with these changes. + Lastly, you may go ahead and fix this test to reflect the new retry order. + " + ); +} + +/// Test whether the relay selector seems to respect the order as defined by [`RETRY_ORDER`]. +#[test] +fn test_retry_order() { + // In order to for the relay queries defined by `RETRY_ORDER` to always take precedence, + // the user settings need to be 'neutral' on the type of relay that it wants to connect to. + // A default `SelectorConfig` *should* have this property, but a more robust way to guarantee + // this would be to create a neutral relay query and supply it to the relay selector at every + // call to the `get_relay` function. + let relay_selector = default_relay_selector(); + for (retry_attempt, query) in RETRY_ORDER.iter().enumerate() { + let relay = relay_selector + .get_relay(retry_attempt, RuntimeParameters { ipv6: true }) + .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())); + assert_eq!( + tunnel_type, + query.tunnel_protocol.unwrap_or(TunnelType::Wireguard), + "Retry attempt {retry_attempt} yielded an unexpected tunnel type" + ); + // Then perform some protocol-specific probing as well. + match relay { + GetRelay::Wireguard { + endpoint, + obfuscator, + .. + } => { + assert!(query + .wireguard_constraints + .ip_version + .matches_eq(&match endpoint.peer.endpoint.ip() { + std::net::IpAddr::V4(_) => talpid_types::net::IpVersion::V4, + std::net::IpAddr::V6(_) => talpid_types::net::IpVersion::V6, + })); + assert!(query + .wireguard_constraints + .port + .matches_eq(&endpoint.peer.endpoint.port())); + assert!(match query.wireguard_constraints.obfuscation { + SelectedObfuscation::Auto => true, + SelectedObfuscation::Off => obfuscator.is_none(), + SelectedObfuscation::Udp2Tcp => obfuscator.is_some(), + }); + } + GetRelay::OpenVpn { + endpoint, bridge, .. + } => { + if BridgeQuery::should_use_bridge(&query.openvpn_constraints.bridge_settings) { + assert!(bridge.is_some(), "Relay selector should have selected a bridge for query {query:?}, but bridge was `None`"); + }; + assert!(query + .openvpn_constraints + .port + .map(|transport_port| transport_port.port.matches_eq(&endpoint.address.port())) + .unwrap_or(true), + "The query {query:?} defined a port to use, but the chosen relay endpoint did not match that port number. + Expected: {expected} + Actual: {actual}", + expected = query.openvpn_constraints.port.unwrap().port.unwrap(), actual = endpoint.address.port() + ); + + assert!(query.openvpn_constraints.port.map(|transport_port| transport_port.protocol == endpoint.protocol).unwrap_or(true), + "The query {query:?} defined a transport protocol to use, but the chosen relay endpoint did not match that transport protocol. + Expected: {expected} + Actual: {actual}", + expected = query.openvpn_constraints.port.unwrap().protocol, actual = endpoint.protocol + ); + } + GetRelay::Custom(_) => unreachable!(), + } + } +} + +/// Verify that Wireguard is preferred if the tunnel type is set to auto. +#[test] +fn prefer_wireguard_when_auto() { + // Turn on bridge state. This is only relevant when selecting OpenVPN relays, but turning + // this configuration option should not prompt the relay selector to prefer OpenVPN. + let config = SelectorConfig { + bridge_state: BridgeState::On, + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + for _ in 0..100 { + let query = RelayQueryBuilder::new().build(); + let relay = relay_selector.get_relay_by_query(query).unwrap(); + let tunnel_type = tunnel_type(&unwrap_relay(relay)); + assert_eq!(tunnel_type, TunnelType::Wireguard); + } +} + +/// If a Wireguard relay is only specified by it's hostname (and not tunnel type), the relay selector should +/// still return a relay of the correct tunnel type (Wireguard). +#[test] +fn test_prefer_wireguard_if_location_supports_it() { + let relay_selector = default_relay_selector(); + let query = RelayQueryBuilder::new() + .location(GeographicLocationConstraint::hostname( + "se", + "got", + "se9-wireguard", + )) + .build(); + + for _ in 0..RETRY_ORDER.len() { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + let tunnel_typ = tunnel_type(&unwrap_relay(relay)); + assert_eq!(tunnel_typ, TunnelType::Wireguard); + } +} + +/// If an OpenVPN relay is only specified by it's hostname (and not tunnel type), the relay selector should +/// still return a relay of the correct tunnel type (OpenVPN). +#[test] +fn test_prefer_openvpn_if_location_supports_it() { + let relay_selector = default_relay_selector(); + let query = RelayQueryBuilder::new() + .location(GeographicLocationConstraint::hostname( + "se", + "got", + "se-got-001", + )) + .build(); + + for _ in 0..RETRY_ORDER.len() { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + let tunnel_typ = tunnel_type(&unwrap_relay(relay)); + assert_eq!(tunnel_typ, TunnelType::OpenVpn); + } +} + +/// Assert that the relay selector does *not* return a multihop configuration where the exit and entry relay are +/// the same, even if the constraints would allow for it. Also verify that the relay selector is smart enough to +/// pick either the entry or exit relay first depending on which one ends up yielding a valid configuration. +#[test] +fn test_wireguard_entry() { + // Define a relay list containing exactly two Wireguard relays in Gothenburg. + let relays = RelayList { + etag: None, + countries: vec![RelayListCountry { + name: "Sweden".to_string(), + code: "se".to_string(), + cities: vec![RelayListCity { + name: "Gothenburg".to_string(), + code: "got".to_string(), + latitude: 57.70887, + longitude: 11.97456, + relays: vec![ + Relay { + hostname: "se9-wireguard".to_string(), + ipv4_addr_in: "185.213.154.68".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), + include_in_country: true, + active: true, + owned: true, + provider: "provider0".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + }), + location: None, + }, + Relay { + hostname: "se10-wireguard".to_string(), + ipv4_addr_in: "185.213.154.69".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a10f".parse().unwrap()), + include_in_country: true, + active: true, + owned: false, + provider: "provider1".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + }), + location: None, + }, + ], + }], + }], + openvpn: OpenVpnEndpointData { ports: vec![] }, + bridge: BridgeEndpointData { + shadowsocks: vec![], + }, + wireguard: WireguardEndpointData { + port_ranges: vec![ + (53, 53), + (443, 443), + (4000, 33433), + (33565, 51820), + (52000, 60000), + ], + ipv4_gateway: "10.64.0.1".parse().unwrap(), + ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), + udp2tcp_ports: vec![], + }, + }; + + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relays); + let specific_hostname = "se10-wireguard"; + let specific_location = GeographicLocationConstraint::hostname("se", "got", specific_hostname); + let general_location = GeographicLocationConstraint::city("se", "got"); + + // general_location candidates: [se-09-wireguard, se-10-wireguard] + // specific_location candidates: [se-10-wireguard] + for _ in 0..100 { + // Because the entry location constraint is more specific than the exit loation constraint, + // the entry location should always become `specific_location` + let query = RelayQueryBuilder::new() + .wireguard() + .location(general_location.clone()) + .multihop() + .entry(specific_location.clone()) + .build(); + + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { exit, entry }, + .. + } => { + assert_eq!(entry.hostname, specific_hostname); + assert_ne!(exit.hostname, entry.hostname); + assert_ne!(exit.ipv4_addr_in, entry.ipv4_addr_in); + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } + } + + // general_location candidates: [se-09-wireguard, se-10-wireguard] + // specific_location candidates: [se-10-wireguard] + for _ in 0..100 { + // Because the exit location constraint is more specific than the entry loation constraint, + // the exit location should always become `specific_location` + let query = RelayQueryBuilder::new() + .wireguard() + .location(specific_location.clone()) + .multihop() + .entry(general_location.clone()) + .build(); + + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { exit, entry }, + .. + } => { + assert_eq!(exit.hostname, specific_hostname); + assert_ne!(exit.hostname, entry.hostname); + assert_ne!(exit.ipv4_addr_in, entry.ipv4_addr_in); + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } + } +} + +/// If a Wireguard multihop constraint has the same entry and exit relay, the relay selector +/// should fail to come up with a valid configuration. +/// +/// If instead the entry and exit relay are distinct, and assuming that the relays exist, the relay +/// selector should instead always return a valid configuration. +#[test] +fn test_wireguard_entry_hostname_collision() { + let relay_selector = default_relay_selector(); + // Define two distinct Wireguard relays. + let host1 = GeographicLocationConstraint::hostname("se", "got", "se9-wireguard"); + let host2 = GeographicLocationConstraint::hostname("se", "got", "se10-wireguard"); + + let invalid_multihop_query = RelayQueryBuilder::new().wireguard() + // Here we set `host1` to be the exit relay + .location(host1.clone()) + .multihop() + // .. and here we set `host1` to also be the entry relay! + .entry(host1.clone()) + .build(); + + // Assert that the same host cannot be used for entry and exit + assert!(relay_selector + .get_relay_by_query(invalid_multihop_query) + .is_err()); + + let valid_multihop_query = RelayQueryBuilder::new().wireguard() + .location(host1) + .multihop() + // We correct the erroneous query by setting `host2` as the entry relay + .entry(host2) + .build(); + + // Assert that the new query succeeds when the entry and exit hosts differ + assert!(relay_selector + .get_relay_by_query(valid_multihop_query) + .is_ok()) +} + +/// Test that the relay selector: +/// * returns an OpenVPN relay given a constraint of a valid transport protocol + port combo +/// * does *not* return an OpenVPN relay given a constraint of an *invalid* transport protocol + port combo +#[test] +fn test_openvpn_constraints() { + let relay_selector = default_relay_selector(); + const ACTUAL_TCP_PORT: u16 = 443; + const ACTUAL_UDP_PORT: u16 = 1194; + const NON_EXISTENT_PORT: u16 = 1337; + + // Test all combinations of constraints, and whether they should + // match some relay + let constraint_combinations = [ + (RelayQueryBuilder::new().openvpn().build(), true), + ( + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Udp) + .build(), + true, + ), + ( + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Tcp) + .build(), + true, + ), + ( + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Udp) + .port(ACTUAL_UDP_PORT) + .build(), + true, + ), + ( + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Udp) + .port(NON_EXISTENT_PORT) + .build(), + false, + ), + ( + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Tcp) + .port(ACTUAL_TCP_PORT) + .build(), + true, + ), + ( + RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Tcp) + .port(NON_EXISTENT_PORT) + .build(), + false, + ), + ]; + + let matches_constraints = + |endpoint: Endpoint, constraints: &OpenVpnRelayQuery| match constraints.port { + Constraint::Any => (), + Constraint::Only(TransportPort { protocol, port }) => { + assert_eq!(endpoint.protocol, protocol); + match port { + Constraint::Any => (), + Constraint::Only(port) => assert_eq!(port, endpoint.address.port()), + } + } + }; + + for (query, should_match) in constraint_combinations.into_iter() { + for _ in 0..100 { + let relay: Result<_, Error> = relay_selector.get_relay_by_query(query.clone()); + if !should_match { + relay.expect_err("Unexpected relay"); + } else { + match relay.expect("Expected to find a relay") { + GetRelay::OpenVpn { endpoint, .. } => { + matches_constraints(endpoint, &query.openvpn_constraints); + }, + wrong_relay => panic!("Relay selector should have picked an OpenVPN relay, instead chose {wrong_relay:?}") + }; + } + } + } +} + +/// Construct a query for multihop configuration and assert that the relay selector picks an accompanying entry relay. +#[test] +fn test_selecting_wireguard_location_will_consider_multihop() { + let relay_selector = default_relay_selector(); + + for _ in 0..100 { + let query = RelayQueryBuilder::new().wireguard().multihop().build(); + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + assert!(matches!( + relay, + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { .. }, + .. + } + )) + } +} + +/// Construct a query for multihop configuration, but the tunnel protocol is forcefully set to Any. +/// If a Wireguard relay is chosen, the relay selector should also pick an accompanying entry relay. +#[test] +fn test_selecting_any_relay_will_consider_multihop() { + let relay_selector = default_relay_selector(); + let mut query = RelayQueryBuilder::new().wireguard().multihop().build(); + query.tunnel_protocol = Constraint::Any; + + for _ in 0..100 { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + assert!(matches!(relay, GetRelay::Wireguard { inner: WireguardConfig::Multihop { .. }, .. }), + "Relay selector should have picked a Wireguard relay with multihop, instead chose {relay:?}" + ); + } +} + +/// Construct a query for a Wireguard configuration where UDP2TCP obfuscation is selected and multihop is explicitly +/// turned off. Assert that the relay selector always return an obfuscator configuration. +#[test] +fn test_selecting_wireguard_endpoint_with_udp2tcp_obfuscation() { + let relay_selector = default_relay_selector(); + let mut query = RelayQueryBuilder::new().wireguard().udp2tcp().build(); + query.wireguard_constraints.use_multihop = Constraint::Only(false); + + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + obfuscator, + inner: WireguardConfig::Singlehop { .. }, + .. + } => { + assert!(obfuscator.is_some_and(|obfuscator| matches!( + obfuscator.config, + ObfuscatorConfig::Udp2Tcp { .. } + ))) + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } +} + +/// Construct a query for a Wireguard configuration where UDP2TCP obfuscation is set to "Auto" and multihop is +/// explicitly turned off. Assert that the relay selector does *not* return an obfuscator config. +/// +/// # Note +/// This is a highly specific test which details how the relay selector should behave at the time of writing this test. +/// The cost (in latency primarily) of using obfuscation is deemed to be too high to enable it as an auto-configuration. +#[test] +fn test_selecting_wireguard_endpoint_with_auto_obfuscation() { + let relay_selector = default_relay_selector(); + let mut query = RelayQueryBuilder::new().wireguard().build(); + query.wireguard_constraints.obfuscation = SelectedObfuscation::Auto; + + for _ in 0..100 { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + match relay { + GetRelay::Wireguard { obfuscator, .. } => { + assert!(obfuscator.is_none()); + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } + } +} + +/// Construct a query for a Wireguard configuration with UDP2TCP obfuscation, and make sure that +/// all configurations contain a valid port. +#[test] +fn test_selected_wireguard_endpoints_use_correct_port_ranges() { + const TCP2UDP_PORTS: [u16; 3] = [80, 443, 5001]; + let relay_selector = default_relay_selector(); + // Note that we do *not* specify any port here! + let query = RelayQueryBuilder::new().wireguard().udp2tcp().build(); + + for _ in 0..1000 { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + match relay { + GetRelay::Wireguard { + obfuscator, + inner: WireguardConfig::Singlehop { .. }, + .. + } => { + let Some(obfuscator) = obfuscator else { + panic!("Relay selector should have picked an obfuscator") + }; + assert!(match obfuscator.config { + ObfuscatorConfig::Udp2Tcp { endpoint } => + TCP2UDP_PORTS.contains(&endpoint.port()), + }) + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + }; + } +} + +/// Verify that any query which sets an explicit [`Ownership`] is respected by the relay selector. +#[test] +fn test_ownership() { + let relay_selector = default_relay_selector(); + + for _ in 0..100 { + // Construct an arbitrary query for owned relays. + let query = RelayQueryBuilder::new() + .ownership(Ownership::MullvadOwned) + .build(); + let relay = relay_selector.get_relay_by_query(query).unwrap(); + // Check that the relay is owned by Mullvad. + assert!(unwrap_relay(relay).owned); + } + + for _ in 0..100 { + // Construct an arbitrary query for rented relays. + let query = RelayQueryBuilder::new() + .ownership(Ownership::Rented) + .build(); + let relay = relay_selector.get_relay_by_query(query).unwrap(); + // Check that the relay is rented. + assert!(!unwrap_relay(relay).owned); + } +} + +/// Verify that server and port selection varies between retry attempts. +#[test] +fn test_load_balancing() { + const ATTEMPTS: usize = 100; + let relay_selector = default_relay_selector(); + let location = GeographicLocationConstraint::country("se"); + for query in [ + RelayQueryBuilder::new().location(location.clone()).build(), + RelayQueryBuilder::new() + .wireguard() + .location(location.clone()) + .build(), + RelayQueryBuilder::new() + .openvpn() + .location(location) + .build(), + ] { + // Collect the range of unique relay ports and IP addresses over a large number of queries. + let (ports, ips): (HashSet<u16>, HashSet<std::net::IpAddr>) = std::iter::repeat(query.clone()) + .take(ATTEMPTS) + // Execute the query + .map(|query| relay_selector.get_relay_by_query(query).unwrap()) + // Perform some plumbing .. + .map(unwrap_endpoint) + .map(|endpoint| endpoint.to_endpoint().address) + // Extract the selected relay's port + IP address + .map(|endpoint| (endpoint.port(), endpoint.ip())) + .unzip(); + + assert!( + ports.len() > 1, + "expected more than 1 port, got {ports:?}, for tunnel protocol {tunnel_protocol:?}", + tunnel_protocol = query.tunnel_protocol, + ); + assert!( + ips.len() > 1, + "expected more than 1 server, got {ips:?}, for tunnel protocol {tunnel_protocol:?}", + tunnel_protocol = query.tunnel_protocol, + ); + } +} + +/// Construct a query for a relay with specific providers and verify that every chosen relay has +/// the correct associated provider. +#[test] +fn test_providers() { + const EXPECTED_PROVIDERS: [&str; 2] = ["provider0", "provider2"]; + let providers = Providers::new(EXPECTED_PROVIDERS).unwrap(); + let relay_selector = default_relay_selector(); + + for _attempt in 0..100 { + let query = RelayQueryBuilder::new() + .providers(providers.clone()) + .build(); + let relay = relay_selector.get_relay_by_query(query).unwrap(); + + match &relay { + GetRelay::Wireguard { .. } => { + let exit = unwrap_relay(relay); + assert!( + EXPECTED_PROVIDERS.contains(&exit.provider.as_str()), + "cannot find provider {provider} in {EXPECTED_PROVIDERS:?}", + provider = exit.provider + ) + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + }; + } +} + +/// Verify that bridges are automatically used when bridge mode is set +/// to automatic. +#[test] +fn test_openvpn_auto_bridge() { + let relay_selector = default_relay_selector(); + let retry_order = [ + // This attempt should not use bridge + RelayQueryBuilder::new().openvpn().build(), + // This attempt should use a bridge + RelayQueryBuilder::new().openvpn().bridge().build(), + ]; + + for (retry_attempt, query) in retry_order + .iter() + .cycle() + .enumerate() + .take(100 * retry_order.len()) + { + let relay = relay_selector + .get_relay_with_custom_params(retry_attempt, &retry_order, RuntimeParameters::default()) + .unwrap(); + match relay { + GetRelay::OpenVpn { bridge, .. } => { + if BridgeQuery::should_use_bridge(&query.openvpn_constraints.bridge_settings) { + assert!(bridge.is_some()) + } else { + assert!(bridge.is_none()) + } + } + wrong_relay => panic!( + "Relay selector should have picked an OpenVPN relay, instead chose {wrong_relay:?}" + ), + } + } +} + +/// Ensure that `include_in_country` is ignored if all relays have it set to false (i.e., some +/// relay is returned). Also ensure that `include_in_country` is respected if some relays +/// have it set to true (i.e., that relay is never returned) +#[test] +fn test_include_in_country() { + let mut relay_list = RelayList { + etag: None, + countries: vec![RelayListCountry { + name: "Sweden".to_string(), + code: "se".to_string(), + cities: vec![RelayListCity { + name: "Gothenburg".to_string(), + code: "got".to_string(), + latitude: 57.70887, + longitude: 11.97456, + relays: vec![ + Relay { + hostname: "se9-wireguard".to_string(), + ipv4_addr_in: "185.213.154.68".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), + include_in_country: false, + active: true, + owned: true, + provider: "31173".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + }), + location: None, + }, + Relay { + hostname: "se10-wireguard".to_string(), + ipv4_addr_in: "185.213.154.69".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a10f".parse().unwrap()), + include_in_country: false, + active: true, + owned: false, + provider: "31173".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + }), + location: None, + }, + ], + }], + }], + openvpn: OpenVpnEndpointData { + ports: vec![ + OpenVpnEndpoint { + port: 1194, + protocol: Udp, + }, + OpenVpnEndpoint { + port: 443, + protocol: Tcp, + }, + OpenVpnEndpoint { + port: 80, + protocol: Tcp, + }, + ], + }, + bridge: BridgeEndpointData { + shadowsocks: vec![], + }, + wireguard: WireguardEndpointData { + port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)], + ipv4_gateway: "10.64.0.1".parse().unwrap(), + ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), + udp2tcp_ports: vec![], + }, + }; + + // 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()) + .is_ok()); + + // If include_in_country is true for some relay, it must always be selected. + relay_list.countries[0].cities[0].relays[0].include_in_country = true; + let expected_hostname = relay_list.countries[0].cities[0].relays[0].hostname.clone(); + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list); + let relay = unwrap_relay( + relay_selector + .get_relay(0, RuntimeParameters::default()) + .expect("expected match"), + ); + + assert!( + matches!(relay, Relay { ref hostname, .. } if hostname == &expected_hostname), + "found {relay:?}, expected {expected_hostname:?}", + ) +} + +/// Verify that the relay selector ignores bridge state when WireGuard should be used. +#[test] +fn ignore_bridge_state_when_wireguard_is_used() { + // Note: The location implies a Wireguard relay. + let location = GeographicLocationConstraint::hostname("se", "got", "se10-wireguard"); + // .. while the query otherwise does not. + let query = RelayQueryBuilder::new().location(location).build(); + let config = SelectorConfig { + bridge_state: BridgeState::On, + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + for _ in 0..100 { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + let tunnel_type = tunnel_type(&unwrap_relay(relay)); + assert_eq!(tunnel_type, TunnelType::Wireguard); + } +} + +/// Handle bridge setting when falling back on OpenVPN +#[test] +fn openvpn_handle_bridge_settings() { + // First, construct a query to choose an OpenVPN relay to talk to over UDP. + let mut query = RelayQueryBuilder::new() + .openvpn() + .transport_protocol(Udp) + .build(); + + let config = SelectorConfig { + bridge_state: BridgeState::On, + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + // Assert that the resulting relay uses UDP. + match relay { + GetRelay::OpenVpn { endpoint, .. } => { + assert_eq!(endpoint.protocol, Udp); + } + wrong_relay => panic!( + "Relay selector should have picked an OpenVPN relay, instead chose {wrong_relay:?}" + ), + } + // Tweaking the query just slightly to try to enable bridge mode, while sill using UDP, + // should fail. + query.openvpn_constraints.bridge_settings = + Constraint::Only(BridgeQuery::Normal(BridgeConstraints::default())); + let relay = relay_selector.get_relay_by_query(query.clone()); + assert!(relay.is_err()); + + // Correcting the query to use TCP, the relay selector should yield a valid relay + bridge + query.openvpn_constraints.port = Constraint::Only(TransportPort { + protocol: Tcp, + port: Constraint::default(), + }); + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + match relay { + GetRelay::OpenVpn { + endpoint, bridge, .. + } => { + assert!(bridge.is_some()); + assert_eq!(endpoint.protocol, Tcp); + } + wrong_relay => panic!( + "Relay selector should have picked an OpenVPN relay, instead chose {wrong_relay:?}" + ), + }; +} + +/// Verify that the relay selector correctly gives back an OpenVPN relay + bridge when the user's +/// settings indicate that bridge mode is on, but the transport protocol is set to auto. Note that +/// it is only valid to use TCP with bridges. Trying to use UDP over bridges is not allowed, and +/// the relay selector should fail to select a relay in these cases. +#[test] +fn openvpn_bridge_with_automatic_transport_protocol() { + // Enable bridge mode. + let config = SelectorConfig { + bridge_state: BridgeState::On, + ..SelectorConfig::default() + }; + let relay_selector = RelaySelector::from_list(config, RELAYS.clone()); + + // First, construct a query to choose an OpenVPN relay and bridge. + let mut query = RelayQueryBuilder::new().openvpn().bridge().build(); + // Forcefully modify the transport protocol, as the builder will ensure that the transport + // protocol is set to TCP. + query.openvpn_constraints.port = Constraint::Any; + + for _ in 0..100 { + let relay = relay_selector.get_relay_by_query(query.clone()).unwrap(); + // Assert that the relay selector is able to cope with the transport protocol being set to + // auto. + match relay { + GetRelay::OpenVpn { endpoint, .. } => { + assert_eq!(endpoint.protocol, Tcp); + } + wrong_relay => panic!( + "Relay selector should have picked an OpenVPN relay, instead chose {wrong_relay:?}" + ), + } + } + + // Modify the query slightly to forcefully use UDP. This should not be allowed! + let query = RelayQueryBuilder::new() + .openvpn() + .bridge() + .transport_protocol(Udp) + .build(); + for _ in 0..100 { + let relay = relay_selector.get_relay_by_query(query.clone()); + assert!(relay.is_err()) + } +} |
