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/src/lib.rs | |
| 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/src/lib.rs')
| -rw-r--r-- | mullvad-relay-selector/src/lib.rs | 2413 |
1 files changed, 11 insertions, 2402 deletions
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, +}; |
