From c731c0fffd228fc5b462d6cdae04e5acb4d0e633 Mon Sep 17 00:00:00 2001 From: Markus Pettersson Date: Fri, 16 Feb 2024 16:24:33 +0100 Subject: [PATCH] Refactor `mullvad-relay-selector` Implement a system for choosing appropriate relay(s) built on 'queries'. 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. Decrust `mullvad-relay-selector` by splitting it up 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. --- Cargo.lock | 17 +- Cargo.toml | 2 + docs/relay-selector.md | 49 +- mullvad-cli/src/cmds/bridge.rs | 5 +- mullvad-cli/src/cmds/custom_list.rs | 3 +- mullvad-cli/src/cmds/debug.rs | 5 +- mullvad-cli/src/cmds/obfuscation.rs | 5 +- mullvad-cli/src/cmds/relay.rs | 9 +- mullvad-cli/src/cmds/relay_constraints.rs | 3 +- mullvad-cli/src/cmds/tunnel.rs | 2 +- mullvad-daemon/src/custom_list.rs | 7 +- mullvad-daemon/src/device/mod.rs | 24 +- mullvad-daemon/src/lib.rs | 8 +- mullvad-daemon/src/migrations/v1.rs | 2 +- mullvad-daemon/src/migrations/v4.rs | 2 +- mullvad-daemon/src/migrations/v5.rs | 2 +- mullvad-daemon/src/migrations/v6.rs | 2 +- mullvad-daemon/src/relay_list/mod.rs | 214 +- mullvad-daemon/src/relay_list/updater.rs | 201 -- mullvad-daemon/src/settings/mod.rs | 13 +- mullvad-daemon/src/tunnel.rs | 223 +- .../types/conversions/relay_constraints.rs | 5 +- mullvad-relay-selector/Cargo.toml | 4 +- mullvad-relay-selector/src/constants.rs | 4 + mullvad-relay-selector/src/error.rs | 66 + mullvad-relay-selector/src/lib.rs | 2413 +---------------- mullvad-relay-selector/src/matcher.rs | 341 --- .../src/relay_selector/detailer.rs | 283 ++ .../src/relay_selector/helpers.rs | 124 + .../src/relay_selector/matcher.rs | 184 ++ .../src/relay_selector/mod.rs | 925 +++++++ .../src/relay_selector/parsed_relays.rs | 189 ++ .../src/relay_selector/query.rs | 855 ++++++ .../tests/relay_selector.rs | 1104 ++++++++ mullvad-types/src/constraints/constraint.rs | 165 ++ mullvad-types/src/constraints/mod.rs | 35 + mullvad-types/src/endpoint.rs | 9 - mullvad-types/src/lib.rs | 1 + mullvad-types/src/relay_constraints.rs | 356 +-- mullvad-types/src/relay_list.rs | 51 + mullvad-types/src/settings/mod.rs | 3 +- talpid-future/Cargo.toml | 2 +- talpid-wireguard/Cargo.toml | 3 +- test/Cargo.lock | 12 + test/test-manager/src/tests/helpers.rs | 3 +- test/test-manager/src/tests/install.rs | 4 +- test/test-manager/src/tests/tunnel.rs | 50 +- test/test-manager/src/tests/tunnel_state.rs | 8 +- 48 files changed, 4507 insertions(+), 3490 deletions(-) delete mode 100644 mullvad-daemon/src/relay_list/updater.rs create mode 100644 mullvad-relay-selector/src/constants.rs create mode 100644 mullvad-relay-selector/src/error.rs delete mode 100644 mullvad-relay-selector/src/matcher.rs create mode 100644 mullvad-relay-selector/src/relay_selector/detailer.rs create mode 100644 mullvad-relay-selector/src/relay_selector/helpers.rs create mode 100644 mullvad-relay-selector/src/relay_selector/matcher.rs create mode 100644 mullvad-relay-selector/src/relay_selector/mod.rs create mode 100644 mullvad-relay-selector/src/relay_selector/parsed_relays.rs create mode 100644 mullvad-relay-selector/src/relay_selector/query.rs create mode 100644 mullvad-relay-selector/tests/relay_selector.rs create mode 100644 mullvad-types/src/constraints/constraint.rs create mode 100644 mullvad-types/src/constraints/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6ab3fc3104a9..db8f407efb1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1619,6 +1619,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1936,7 +1945,7 @@ dependencies = [ "clap", "clap_complete", "futures", - "itertools", + "itertools 0.10.5", "mullvad-management-interface", "mullvad-types", "mullvad-version", @@ -2102,9 +2111,11 @@ version = "0.0.0" dependencies = [ "chrono", "ipnetwork", + "itertools 0.12.1", "log", "mullvad-types", "once_cell", + "proptest", "rand 0.8.5", "serde_json", "talpid-types", @@ -2829,7 +2840,7 @@ checksum = "30d3e647e9eb04ddfef78dfee2d5b3fefdf94821c84b710a3d8ebc89ede8b164" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -2850,7 +2861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56075c27b20ae524d00f247b8a4dc333e5784f889fe63099f8e626bc8d73486c" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.51", diff --git a/Cargo.toml b/Cargo.toml index 81efa9b85c17..cfd5e238e9d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,8 @@ chrono = { version = "0.4.26", default-features = false} clap = { version = "4.4.18", features = ["cargo", "derive"] } once_cell = "1.13" +# Test dependencies +proptest = "1.4" [profile.release] opt-level = 3 diff --git a/docs/relay-selector.md b/docs/relay-selector.md index 6c49b8a47f5d..9b7d26e30e93 100644 --- a/docs/relay-selector.md +++ b/docs/relay-selector.md @@ -16,15 +16,15 @@ # Relay selector The relay selector's main purpose is to pick a single Mullvad relay from a list of relays taking -into account certain user-configurable criteria. Relays can be filtered by their _location_ +into account certain user-configurable criteria. Relays can be filtered by their _location_ (country, city, hostname), by the protocols and ports they support (transport protocol, tunnel -protocol, port), and by other constraints. The constraints are user specified and stored in the +protocol, port), and by other constraints. The constraints are user specified and stored in the settings. The default value for location constraints restricts relay selection to relays from Sweden. The default protocol constraints default to _Auto_, which implies specific behavior. Generally, the filtering process consists of going through each relay in our relay list and removing relay and endpoint combinations that do not match the constraints outlined above. The -filtering process produces a list of relays that only contain matching endpoints. Of all the relays +filtering process produces a list of relays that only contain matching endpoints. Of all the relays that match the constraints, one is selected and a random matching endpoint is selected from that relay. @@ -49,40 +49,27 @@ Endpoints may be filtered by: Whilst all user selected constraints are always honored, when the user hasn't selected any specific constraints, following default ones will take effect: -- If no tunnel protocol is specified, the first three connection attempts will use WireGuard. All - remaining attempts will use OpenVPN. If no specific constraints are set: - - The first two attempts will connect to a Wireguard server, first on a random port, and then port - 53. - - The third attempt will connect to a Wireguard server on port 80 with _udp2tcp_. - - Remaining attempts will connect to OpenVPN servers, first over UDP on two random ports, and then - over TCP on port 443. Remaining attempts alternate between TCP and UDP on random ports. +- The first three connection attempts will use Wireguard. + - The first attempt will connect to a Wireguard relay on a random port. + - The second attempt will connect to a Wireguard relay on port 443 + - The third attempt will connect to a Wireguard relay over IPv6 (if IPv6 is configured on the host) +- The fourth-to-seventh attempt will alternate between Wireguard and OpenVPN + - The fifth attempt will connect to an OpenVPN relay over TCP on port 443 + - The sixth attempt will connect to a Wireguard relay on a random port using [UDP2TCP obfuscation](https://github.com/mullvad/udp-over-tcp) + - The seventh attempt will connect to a Wireguard relay over IPv6 on a random port using UDP2TCP obfuscation (if IPv6 is configured on the host) + - The eighth attempt will connect to an OpenVPN relay over a bridge -- If the tunnel protocol is specified as WireGuard and obfuscation mode is set to _Auto_: - - First two attempts will be used without _udp2tcp_, using a random port on first attempt, and - port 53 on second attempt. - - Next two attempts will use _udp2tcp_ on ports 80 and 5001 respectively. - - The above steps repeat ad infinitum. +If no tunnel has been established after exhausting this list of attempts, the relay selector will +loop back to the first default constraint and continue its search from there. - If obfuscation is turned on, connections will alternate between port 80 and port 5001 using - _udp2tcp_ all of the time. - - If obfuscation is turned _off_, WireGuard connections will first alternate between using - a random port and port 53, e.g. first attempt using port 22151, second 53, third - 26107, fourth attempt using port 53, and so on. - - If the user has specified a specific port for either _udp2tcp_ or WireGuard, it will override the - port selection, but it will not change the connection type described above (WireGuard or WireGuard - over _udp2tcp_). - -- If no OpenVPN tunnel constraints are specified, then the first two attempts at selecting a tunnel - will try to select UDP endpoints on any port, and the third and fourth attempts will filter for - TCP endpoints on port 443. Any subsequent filtering attempts will alternate between TCP and UDP on - any port. +Any default constraint that is incompatible with user specified constraints will simply not be +considered. Conversely, all default constraints which do not conflict with user specified constraints +will be used in the search for a working tunnel endpoint on repeated connection failures. ## Selecting tunnel endpoint between filtered relays To select a single relay from the set of filtered relays, the relay selector uses a roulette wheel -selection algorithm using the weights that are assigned to each relay. The higher the weight is +selection algorithm using the weights that are assigned to each relay. The higher the weight is relatively to other relays, the higher the likelihood that a given relay will be picked. Once a relay is picked, then a random endpoint that matches the constraints from the relay is picked. diff --git a/mullvad-cli/src/cmds/bridge.rs b/mullvad-cli/src/cmds/bridge.rs index 861d73feb2ac..48458c9c1e9b 100644 --- a/mullvad-cli/src/cmds/bridge.rs +++ b/mullvad-cli/src/cmds/bridge.rs @@ -2,9 +2,10 @@ use anyhow::{bail, Result}; use clap::Subcommand; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ + constraints::Constraint, relay_constraints::{ - BridgeConstraintsFormatter, BridgeState, BridgeType, Constraint, LocationConstraint, - Ownership, Provider, Providers, + BridgeConstraintsFormatter, BridgeState, BridgeType, LocationConstraint, Ownership, + Provider, Providers, }, relay_list::RelayEndpointData, }; diff --git a/mullvad-cli/src/cmds/custom_list.rs b/mullvad-cli/src/cmds/custom_list.rs index 3dc4445f8f2c..c1ab653a0d01 100644 --- a/mullvad-cli/src/cmds/custom_list.rs +++ b/mullvad-cli/src/cmds/custom_list.rs @@ -3,8 +3,7 @@ use anyhow::{anyhow, bail, Result}; use clap::Subcommand; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ - relay_constraints::{Constraint, GeographicLocationConstraint}, - relay_list::RelayList, + constraints::Constraint, relay_constraints::GeographicLocationConstraint, relay_list::RelayList, }; #[derive(Subcommand, Debug)] diff --git a/mullvad-cli/src/cmds/debug.rs b/mullvad-cli/src/cmds/debug.rs index 832e6eaa8e54..d67194b05855 100644 --- a/mullvad-cli/src/cmds/debug.rs +++ b/mullvad-cli/src/cmds/debug.rs @@ -1,6 +1,9 @@ use anyhow::Result; use mullvad_management_interface::MullvadProxyClient; -use mullvad_types::relay_constraints::{Constraint, RelayConstraints, RelaySettings}; +use mullvad_types::{ + constraints::Constraint, + relay_constraints::{RelayConstraints, RelaySettings}, +}; #[derive(clap::Subcommand, Debug)] pub enum DebugCommands { diff --git a/mullvad-cli/src/cmds/obfuscation.rs b/mullvad-cli/src/cmds/obfuscation.rs index b2aaaa1f6e62..91d3320bd791 100644 --- a/mullvad-cli/src/cmds/obfuscation.rs +++ b/mullvad-cli/src/cmds/obfuscation.rs @@ -1,8 +1,9 @@ use anyhow::Result; use clap::Subcommand; use mullvad_management_interface::MullvadProxyClient; -use mullvad_types::relay_constraints::{ - Constraint, ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings, +use mullvad_types::{ + constraints::Constraint, + relay_constraints::{ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings}, }; #[derive(Subcommand, Debug)] diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 3c14456518ed..7ef60d758cc9 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -3,10 +3,11 @@ use clap::Subcommand; use itertools::Itertools; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ + constraints::{Constraint, Match}, location::{CountryCode, Location}, relay_constraints::{ - Constraint, GeographicLocationConstraint, LocationConstraint, LocationConstraintFormatter, - Match, OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelayOverride, + GeographicLocationConstraint, LocationConstraint, LocationConstraintFormatter, + OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelayOverride, RelaySettings, TransportPort, WireguardConstraints, }, relay_list::{RelayEndpointData, RelayListCountry}, @@ -318,7 +319,7 @@ impl Relay { print_option!( "Multihop state", - if constraints.wireguard_constraints.use_multihop { + if constraints.wireguard_constraints.multihop() { "enabled" } else { "disabled" @@ -679,7 +680,7 @@ impl Relay { wireguard_constraints.ip_version = ipv; } if let Some(use_multihop) = use_multihop { - wireguard_constraints.use_multihop = *use_multihop; + wireguard_constraints.use_multihop(*use_multihop); } match entry_location { Some(EntryArgs::Location(location_args)) => { diff --git a/mullvad-cli/src/cmds/relay_constraints.rs b/mullvad-cli/src/cmds/relay_constraints.rs index 4e09e5b880a0..97555997fcd1 100644 --- a/mullvad-cli/src/cmds/relay_constraints.rs +++ b/mullvad-cli/src/cmds/relay_constraints.rs @@ -1,7 +1,8 @@ use clap::Args; use mullvad_types::{ + constraints::Constraint, location::{CityCode, CountryCode, Hostname}, - relay_constraints::{Constraint, GeographicLocationConstraint, LocationConstraint}, + relay_constraints::{GeographicLocationConstraint, LocationConstraint}, }; #[derive(Args, Debug, Clone)] diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 92e1c89d5ca8..19d5c1a3c956 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap::Subcommand; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ - relay_constraints::Constraint, + constraints::Constraint, wireguard::{QuantumResistantState, RotationInterval, DEFAULT_ROTATION_INTERVAL}, }; diff --git a/mullvad-daemon/src/custom_list.rs b/mullvad-daemon/src/custom_list.rs index 8a9573fcb059..c15fd14a701d 100644 --- a/mullvad-daemon/src/custom_list.rs +++ b/mullvad-daemon/src/custom_list.rs @@ -1,9 +1,8 @@ use crate::{new_selector_config, Daemon, Error, EventListener}; use mullvad_types::{ + constraints::Constraint, custom_list::{CustomList, Id}, - relay_constraints::{ - BridgeState, Constraint, LocationConstraint, RelaySettings, ResolvedBridgeSettings, - }, + relay_constraints::{BridgeState, LocationConstraint, RelaySettings, ResolvedBridgeSettings}, }; use talpid_types::net::TunnelType; @@ -133,7 +132,7 @@ where { match endpoint.tunnel_type { TunnelType::Wireguard => { - if relay_settings.wireguard_constraints.use_multihop { + if relay_settings.wireguard_constraints.multihop() { if let Constraint::Only(LocationConstraint::CustomList { list_id }) = &relay_settings.wireguard_constraints.entry_location { diff --git a/mullvad-daemon/src/device/mod.rs b/mullvad-daemon/src/device/mod.rs index 0493976a4172..a126ec840cd1 100644 --- a/mullvad-daemon/src/device/mod.rs +++ b/mullvad-daemon/src/device/mod.rs @@ -1348,12 +1348,10 @@ impl TunnelStateChangeHandler { #[cfg(test)] mod test { use super::{Error, TunnelStateChangeHandler, WG_DEVICE_CHECK_THRESHOLD}; - use mullvad_relay_selector::RelaySelector; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; - use talpid_types::net::TunnelType; const TIMEOUT_ERROR: Error = Error::OtherRestError(mullvad_api::rest::Error::TimeoutError); @@ -1437,21 +1435,19 @@ mod test { ); } - /// Test whether the relay selector selects wireguard often enough, given no special - /// constraints, to verify that the device is valid + /// Test whether the relay selector selects wireguard often enough, given no special constraints. + /// A Wireguard relay must be used to verify that the device is valid. #[test] fn test_validates_by_default() { + use mullvad_relay_selector::{RelaySelector, SelectorConfig}; + use talpid_types::net::TunnelType; + // No special relay constraints / user settings are assumed + let config = SelectorConfig::default(); for attempt in 0.. { - let should_validate = - TunnelStateChangeHandler::should_check_validity_on_attempt(attempt); - let (_, _, tunnel_type) = - RelaySelector::preferred_tunnel_constraints(attempt.try_into().unwrap()); - assert_eq!( - tunnel_type, - TunnelType::Wireguard, - "failed on attempt {attempt}" - ); - if should_validate { + let typ = RelaySelector::would_return(attempt, &config).unwrap(); + assert_eq!(typ, TunnelType::Wireguard); + + if TunnelStateChangeHandler::should_check_validity_on_attempt(attempt) { // Now that we've triggered a device check, we can give up break; } diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 6ffd696890e4..c3c64ebacfa6 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -56,7 +56,7 @@ use mullvad_types::{ version::{AppVersion, AppVersionInfo}, wireguard::{PublicKey, QuantumResistantState, RotationInterval}, }; -use relay_list::updater::{self, RelayListUpdater, RelayListUpdaterHandle}; +use relay_list::{RelayListUpdater, RelayListUpdaterHandle, RELAYS_FILENAME}; use settings::SettingsPersister; #[cfg(target_os = "android")] use std::os::unix::io::RawFd; @@ -698,8 +698,8 @@ where let initial_selector_config = new_selector_config(&settings); let relay_selector = RelaySelector::new( initial_selector_config, - resource_dir.join(updater::RELAYS_FILENAME), - cache_dir.join(updater::RELAYS_FILENAME), + resource_dir.join(RELAYS_FILENAME), + cache_dir.join(RELAYS_FILENAME), ); let settings_relay_selector = relay_selector.clone(); @@ -1105,7 +1105,7 @@ where // Note that `Constraint::Any` corresponds to just IPv4 matches!( relay_constraints.wireguard_constraints.ip_version, - mullvad_types::relay_constraints::Constraint::Only(IpVersion::V6) + mullvad_types::constraints::Constraint::Only(IpVersion::V6) ) } else { false diff --git a/mullvad-daemon/src/migrations/v1.rs b/mullvad-daemon/src/migrations/v1.rs index c3013c7b2856..4205f23ebf7e 100644 --- a/mullvad-daemon/src/migrations/v1.rs +++ b/mullvad-daemon/src/migrations/v1.rs @@ -1,5 +1,5 @@ use super::Result; -use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion}; +use mullvad_types::{constraints::Constraint, settings::SettingsVersion}; use serde::{Deserialize, Serialize}; // ====================================================== diff --git a/mullvad-daemon/src/migrations/v4.rs b/mullvad-daemon/src/migrations/v4.rs index cd11056d4a1b..8e03daa51b30 100644 --- a/mullvad-daemon/src/migrations/v4.rs +++ b/mullvad-daemon/src/migrations/v4.rs @@ -1,5 +1,5 @@ use super::{Error, Result}; -use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion}; +use mullvad_types::{constraints::Constraint, settings::SettingsVersion}; use serde::{Deserialize, Serialize}; // ====================================================== diff --git a/mullvad-daemon/src/migrations/v5.rs b/mullvad-daemon/src/migrations/v5.rs index 351b427a8595..e458bb1a1d59 100644 --- a/mullvad-daemon/src/migrations/v5.rs +++ b/mullvad-daemon/src/migrations/v5.rs @@ -1,5 +1,5 @@ use super::{Error, Result}; -use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion}; +use mullvad_types::{constraints::Constraint, settings::SettingsVersion}; use serde::{Deserialize, Serialize}; // ====================================================== diff --git a/mullvad-daemon/src/migrations/v6.rs b/mullvad-daemon/src/migrations/v6.rs index 076dc1b71fe4..c481be8d928c 100644 --- a/mullvad-daemon/src/migrations/v6.rs +++ b/mullvad-daemon/src/migrations/v6.rs @@ -1,5 +1,5 @@ use super::{Error, Result}; -use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion}; +use mullvad_types::{constraints::Constraint, settings::SettingsVersion}; use serde::{Deserialize, Serialize}; // ====================================================== diff --git a/mullvad-daemon/src/relay_list/mod.rs b/mullvad-daemon/src/relay_list/mod.rs index 8936941035c6..2b4be3db5432 100644 --- a/mullvad-daemon/src/relay_list/mod.rs +++ b/mullvad-daemon/src/relay_list/mod.rs @@ -1,3 +1,213 @@ -//! Relay list +//! Relay list updater -pub mod updater; +use futures::{ + channel::mpsc, + future::{Fuse, FusedFuture}, + Future, FutureExt, SinkExt, StreamExt, +}; +use std::{ + path::{Path, PathBuf}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use tokio::fs::File; + +use mullvad_api::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle, RelayListProxy}; +use mullvad_relay_selector::RelaySelector; +use mullvad_types::relay_list::RelayList; +use talpid_future::retry::{retry_future, ExponentialBackoff, Jittered}; +use talpid_types::ErrorExt; + +/// How often the updater should wake up to check the cache of the in-memory cache of relays. +/// This check is very cheap. The only reason to not have it very often is because if downloading +/// constantly fails it will try very often and fill the logs etc. +const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 15); +/// How old the cached relays need to be to trigger an update +const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60); + +const DOWNLOAD_RETRY_STRATEGY: Jittered = Jittered::jitter( + ExponentialBackoff::new(Duration::from_secs(16), 8) + .max_delay(Some(Duration::from_secs(2 * 60 * 60))), +); + +/// Where the relay list is cached on disk. +pub(crate) const RELAYS_FILENAME: &str = "relays.json"; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Downloader already shut down")] + DownloaderShutdown, + + #[error("Mullvad relay selector error")] + RelaySelector(#[from] mullvad_relay_selector::Error), +} + +#[derive(Clone)] +pub struct RelayListUpdaterHandle { + tx: mpsc::Sender<()>, +} + +impl RelayListUpdaterHandle { + pub async fn update(&mut self) { + if let Err(error) = self + .tx + .send(()) + .await + .map_err(|_| Error::DownloaderShutdown) + { + log::error!( + "{}", + error.display_chain_with_msg("Unable to send update command to relay list updater") + ); + } + } +} + +pub struct RelayListUpdater { + api_client: RelayListProxy, + cache_path: PathBuf, + relay_selector: RelaySelector, + on_update: Box, + last_check: SystemTime, + api_availability: ApiAvailabilityHandle, +} + +impl RelayListUpdater { + pub fn spawn( + selector: RelaySelector, + api_handle: MullvadRestHandle, + cache_dir: &Path, + on_update: impl Fn(&RelayList) + Send + 'static, + ) -> RelayListUpdaterHandle { + let (tx, cmd_rx) = mpsc::channel(1); + let api_availability = api_handle.availability.clone(); + let api_client = RelayListProxy::new(api_handle); + let updater = RelayListUpdater { + api_client, + cache_path: cache_dir.join(RELAYS_FILENAME), + relay_selector: selector, + on_update: Box::new(on_update), + last_check: UNIX_EPOCH, + api_availability, + }; + + tokio::spawn(updater.run(cmd_rx)); + + RelayListUpdaterHandle { tx } + } + + async fn run(mut self, mut cmd_rx: mpsc::Receiver<()>) { + let mut download_future = Box::pin(Fuse::terminated()); + loop { + let next_check = tokio::time::sleep(UPDATE_CHECK_INTERVAL).fuse(); + tokio::pin!(next_check); + + futures::select! { + _check_update = next_check => { + if download_future.is_terminated() && self.should_update() { + let tag = self.relay_selector.etag(); + download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse()); + self.last_check = SystemTime::now(); + } + }, + + new_relay_list = download_future => { + self.consume_new_relay_list(new_relay_list).await; + }, + + cmd = cmd_rx.next() => { + match cmd { + Some(()) => { + let tag = self.relay_selector.etag(); + download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse()); + self.last_check = SystemTime::now(); + }, + None => { + log::trace!("Relay list updater shutting down"); + return; + } + } + } + + }; + } + } + + async fn consume_new_relay_list( + &mut self, + result: Result, mullvad_api::Error>, + ) { + match result { + Ok(Some(relay_list)) => { + if let Err(err) = self.update_cache(relay_list).await { + log::error!("Failed to update relay list cache: {}", err); + } + } + Ok(None) => log::debug!("Relay list is up-to-date"), + Err(error) => log::error!( + "{}", + error.display_chain_with_msg("Failed to fetch new relay list") + ), + } + } + + /// Returns true if the current parsed_relays is older than UPDATE_INTERVAL + fn should_update(&mut self) -> bool { + let last_check = std::cmp::max(self.relay_selector.last_updated(), self.last_check); + match SystemTime::now().duration_since(last_check) { + Ok(duration) => duration >= UPDATE_INTERVAL, + // If the clock is skewed we have no idea by how much or when the last update + // actually was, better download again to get in sync and get a `last_updated` + // timestamp corresponding to the new time. + Err(_) => true, + } + } + + fn download_relay_list( + api_handle: ApiAvailabilityHandle, + proxy: RelayListProxy, + tag: Option, + ) -> impl Future, mullvad_api::Error>> + 'static { + let download_futures = move || { + let available = api_handle.wait_background(); + let req = proxy.relay_list(tag.clone()); + async move { + available.await?; + req.await.map_err(mullvad_api::Error::from) + } + }; + + retry_future( + download_futures, + |result| result.is_err(), + DOWNLOAD_RETRY_STRATEGY, + ) + } + + async fn update_cache(&mut self, new_relay_list: RelayList) -> Result<(), Error> { + if let Err(error) = Self::cache_relays(&self.cache_path, &new_relay_list).await { + log::error!( + "{}", + error.display_chain_with_msg("Failed to update relay cache on disk") + ); + } + + self.relay_selector.set_relays(new_relay_list.clone()); + (self.on_update)(&new_relay_list); + Ok(()) + } + + /// Write a `RelayList` to the cache file. + async fn cache_relays(cache_path: &Path, relays: &RelayList) -> Result<(), Error> { + log::debug!("Writing relays cache to {}", cache_path.display()); + let mut file = File::create(cache_path) + .await + .map_err(mullvad_relay_selector::Error::OpenRelayCache)?; + let bytes = + serde_json::to_vec_pretty(relays).map_err(mullvad_relay_selector::Error::Serialize)?; + let mut slice: &[u8] = bytes.as_slice(); + let _ = tokio::io::copy(&mut slice, &mut file) + .await + .map_err(mullvad_relay_selector::Error::WriteRelayCache)?; + Ok(()) + } +} diff --git a/mullvad-daemon/src/relay_list/updater.rs b/mullvad-daemon/src/relay_list/updater.rs deleted file mode 100644 index 317dbd45cdc5..000000000000 --- a/mullvad-daemon/src/relay_list/updater.rs +++ /dev/null @@ -1,201 +0,0 @@ -use futures::{ - channel::mpsc, - future::{Fuse, FusedFuture}, - Future, FutureExt, SinkExt, StreamExt, -}; -use std::{ - path::{Path, PathBuf}, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; -use tokio::fs::File; - -use mullvad_api::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle, RelayListProxy}; -use mullvad_relay_selector::{Error, RelaySelector}; -use mullvad_types::relay_list::RelayList; -use talpid_future::retry::{retry_future, ExponentialBackoff, Jittered}; -use talpid_types::ErrorExt; - -/// How often the updater should wake up to check the cache of the in-memory cache of relays. -/// This check is very cheap. The only reason to not have it very often is because if downloading -/// constantly fails it will try very often and fill the logs etc. -const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 15); -/// How old the cached relays need to be to trigger an update -const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60); - -const DOWNLOAD_RETRY_STRATEGY: Jittered = Jittered::jitter( - ExponentialBackoff::new(Duration::from_secs(16), 8) - .max_delay(Some(Duration::from_secs(2 * 60 * 60))), -); - -/// Where the relay list is cached on disk. -pub(crate) const RELAYS_FILENAME: &str = "relays.json"; - -#[derive(Clone)] -pub struct RelayListUpdaterHandle { - tx: mpsc::Sender<()>, -} - -impl RelayListUpdaterHandle { - pub async fn update(&mut self) { - if let Err(error) = self - .tx - .send(()) - .await - .map_err(|_| Error::DownloaderShutDown) - { - log::error!( - "{}", - error.display_chain_with_msg("Unable to send update command to relay list updater") - ); - } - } -} - -pub struct RelayListUpdater { - api_client: RelayListProxy, - cache_path: PathBuf, - relay_selector: RelaySelector, - on_update: Box, - last_check: SystemTime, - api_availability: ApiAvailabilityHandle, -} - -impl RelayListUpdater { - pub fn spawn( - selector: RelaySelector, - api_handle: MullvadRestHandle, - cache_dir: &Path, - on_update: impl Fn(&RelayList) + Send + 'static, - ) -> RelayListUpdaterHandle { - let (tx, cmd_rx) = mpsc::channel(1); - let api_availability = api_handle.availability.clone(); - let api_client = RelayListProxy::new(api_handle); - let updater = RelayListUpdater { - api_client, - cache_path: cache_dir.join(RELAYS_FILENAME), - relay_selector: selector, - on_update: Box::new(on_update), - last_check: UNIX_EPOCH, - api_availability, - }; - - tokio::spawn(updater.run(cmd_rx)); - - RelayListUpdaterHandle { tx } - } - - async fn run(mut self, mut cmd_rx: mpsc::Receiver<()>) { - let mut download_future = Box::pin(Fuse::terminated()); - loop { - let next_check = tokio::time::sleep(UPDATE_CHECK_INTERVAL).fuse(); - tokio::pin!(next_check); - - futures::select! { - _check_update = next_check => { - if download_future.is_terminated() && self.should_update() { - let tag = self.relay_selector.etag(); - download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse()); - self.last_check = SystemTime::now(); - } - }, - - new_relay_list = download_future => { - self.consume_new_relay_list(new_relay_list).await; - }, - - cmd = cmd_rx.next() => { - match cmd { - Some(()) => { - let tag = self.relay_selector.etag(); - download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse()); - self.last_check = SystemTime::now(); - }, - None => { - log::trace!("Relay list updater shutting down"); - return; - } - } - } - - }; - } - } - - async fn consume_new_relay_list( - &mut self, - result: Result, mullvad_api::Error>, - ) { - match result { - Ok(Some(relay_list)) => { - if let Err(err) = self.update_cache(relay_list).await { - log::error!("Failed to update relay list cache: {}", err); - } - } - Ok(None) => log::debug!("Relay list is up-to-date"), - Err(error) => log::error!( - "{}", - error.display_chain_with_msg("Failed to fetch new relay list") - ), - } - } - - /// Returns true if the current parsed_relays is older than UPDATE_INTERVAL - fn should_update(&mut self) -> bool { - let last_check = std::cmp::max(self.relay_selector.last_updated(), self.last_check); - match SystemTime::now().duration_since(last_check) { - Ok(duration) => duration >= UPDATE_INTERVAL, - // If the clock is skewed we have no idea by how much or when the last update - // actually was, better download again to get in sync and get a `last_updated` - // timestamp corresponding to the new time. - Err(_) => true, - } - } - - fn download_relay_list( - api_handle: ApiAvailabilityHandle, - proxy: RelayListProxy, - tag: Option, - ) -> impl Future, mullvad_api::Error>> + 'static { - let download_futures = move || { - let available = api_handle.wait_background(); - let req = proxy.relay_list(tag.clone()); - async move { - available.await?; - req.await.map_err(mullvad_api::Error::from) - } - }; - - retry_future( - download_futures, - |result| result.is_err(), - DOWNLOAD_RETRY_STRATEGY, - ) - } - - async fn update_cache(&mut self, new_relay_list: RelayList) -> Result<(), Error> { - if let Err(error) = Self::cache_relays(&self.cache_path, &new_relay_list).await { - log::error!( - "{}", - error.display_chain_with_msg("Failed to update relay cache on disk") - ); - } - - self.relay_selector.set_relays(new_relay_list.clone()); - (self.on_update)(&new_relay_list); - Ok(()) - } - - /// Write a `RelayList` to the cache file. - async fn cache_relays(cache_path: &Path, relays: &RelayList) -> Result<(), Error> { - log::debug!("Writing relays cache to {}", cache_path.display()); - let mut file = File::create(cache_path) - .await - .map_err(Error::OpenRelayCache)?; - let bytes = serde_json::to_vec_pretty(relays).map_err(Error::Serialize)?; - let mut slice: &[u8] = bytes.as_slice(); - let _ = tokio::io::copy(&mut slice, &mut file) - .await - .map_err(Error::WriteRelayCache)?; - Ok(()) - } -} diff --git a/mullvad-daemon/src/settings/mod.rs b/mullvad-daemon/src/settings/mod.rs index abd6ea2e0aea..b1d9c2b8e628 100644 --- a/mullvad-daemon/src/settings/mod.rs +++ b/mullvad-daemon/src/settings/mod.rs @@ -376,16 +376,13 @@ impl<'a> Display for SettingsSummary<'a> { write!(f, ", wg ip version: {ip_version}")?; } - let multihop = matches!( - relay_settings, + let multihop = match relay_settings { RelaySettings::Normal(RelayConstraints { - wireguard_constraints: WireguardConstraints { - use_multihop: true, - .. - }, + wireguard_constraints, .. - }) - ); + }) => wireguard_constraints.multihop(), + _ => false, + }; write!( f, diff --git a/mullvad-daemon/src/tunnel.rs b/mullvad-daemon/src/tunnel.rs index 20948381736d..775e04d45c96 100644 --- a/mullvad-daemon/src/tunnel.rs +++ b/mullvad-daemon/src/tunnel.rs @@ -8,20 +8,25 @@ use std::{ use tokio::sync::Mutex; -use mullvad_relay_selector::{RelaySelector, SelectedBridge, SelectedObfuscator, SelectedRelay}; +#[cfg(not(target_os = "android"))] +use mullvad_relay_selector::{GetRelay, RelaySelector, WireguardConfig}; +#[cfg(target_os = "android")] +use mullvad_relay_selector::{GetRelay, RelaySelector, WireguardConfig}; use mullvad_types::{ - endpoint::MullvadEndpoint, location::GeoIpLocation, relay_list::Relay, settings::TunnelOptions, + endpoint::MullvadWireguardEndpoint, location::GeoIpLocation, relay_list::Relay, + settings::TunnelOptions, }; use once_cell::sync::Lazy; use talpid_core::tunnel_state_machine::TunnelParametersGenerator; -use talpid_types::{ - net::{wireguard, TunnelParameters}, - tunnel::ParameterGenerationError, - ErrorExt, +#[cfg(not(target_os = "android"))] +use talpid_types::net::{ + obfuscation::ObfuscatorConfig, openvpn, proxy::CustomProxy, wireguard, Endpoint, + TunnelParameters, }; +#[cfg(target_os = "android")] +use talpid_types::net::{obfuscation::ObfuscatorConfig, wireguard, TunnelParameters}; -#[cfg(not(target_os = "android"))] -use talpid_types::net::openvpn; +use talpid_types::{tunnel::ParameterGenerationError, ErrorExt}; use crate::device::{AccountManagerHandle, PrivateAccountAndDevice}; @@ -139,124 +144,122 @@ impl ParametersGenerator { impl InnerParametersGenerator { async fn generate(&mut self, retry_attempt: u32) -> Result { - let _data = self.device().await?; - match self.relay_selector.get_relay(retry_attempt) { - Ok((SelectedRelay::Custom(custom_relay), _bridge, _obfsucator)) => { - self.last_generated_relays = None; - custom_relay - // TODO: generate proxy settings for custom tunnels - .to_tunnel_parameters(self.tunnel_options.clone(), None) - .map_err(|e| { - log::error!("Failed to resolve hostname for custom tunnel config: {}", e); - Error::ResolveCustomHostname - }) - } - Ok((SelectedRelay::Normal(constraints), bridge, obfuscator)) => { - self.create_tunnel_parameters( - &constraints.exit_relay, - &constraints.entry_relay, - constraints.endpoint, - bridge, - obfuscator, - ) - .await - } - Err(mullvad_relay_selector::Error::NoBridge) => Err(Error::NoBridgeAvailable), - Err(_error) => Err(Error::NoRelayAvailable), - } - } - - #[cfg_attr(target_os = "android", allow(unused_variables))] - async fn create_tunnel_parameters( - &mut self, - relay: &Relay, - entry_relay: &Option, - endpoint: MullvadEndpoint, - bridge: Option, - obfuscator: Option, - ) -> Result { let data = self.device().await?; - match endpoint { - #[cfg(not(target_os = "android"))] - MullvadEndpoint::OpenVpn(endpoint) => { - let (bridge_settings, bridge_relay) = match bridge { - Some(SelectedBridge::Normal(bridge)) => { - (Some(bridge.settings), Some(bridge.relay)) - } - Some(SelectedBridge::Custom(settings)) => (Some(settings), None), - None => (None, None), - }; + let selected_relay = self + .relay_selector + .get_relay(retry_attempt as usize) + .map_err(|err| match err { + mullvad_relay_selector::Error::NoBridge => Error::NoBridgeAvailable, + _ => Error::NoRelayAvailable, + })?; + match selected_relay { + #[cfg(not(target_os = "android"))] + GetRelay::OpenVpn { + endpoint, + exit, + bridge, + } => { + let bridge_relay = bridge.as_ref().and_then(|bridge| bridge.relay()); self.last_generated_relays = Some(LastSelectedRelays::OpenVpn { - relay: relay.clone(), - bridge: bridge_relay, + relay: exit.clone(), + bridge: bridge_relay.cloned(), }); - - Ok(openvpn::TunnelParameters { - config: openvpn::ConnectionConfig::new( - endpoint, - data.account_token, - "-".to_string(), - ), - options: self.tunnel_options.openvpn.clone(), - generic_options: self.tunnel_options.generic.clone(), - proxy: bridge_settings, - #[cfg(target_os = "linux")] - fwmark: mullvad_types::TUNNEL_FWMARK, - } - .into()) - } - #[cfg(target_os = "android")] - MullvadEndpoint::OpenVpn(endpoint) => { - unreachable!("OpenVPN is not supported on Android"); + let bridge_settings = bridge.as_ref().map(|bridge| bridge.settings()); + Ok(self.create_openvpn_tunnel_parameters(endpoint, data, bridge_settings.cloned())) } - MullvadEndpoint::Wireguard(endpoint) => { - let tunnel_ipv4 = data.device.wg_data.addresses.ipv4_address.ip(); - let tunnel_ipv6 = data.device.wg_data.addresses.ipv6_address.ip(); - let tunnel = wireguard::TunnelConfig { - private_key: data.device.wg_data.private_key, - addresses: vec![IpAddr::from(tunnel_ipv4), IpAddr::from(tunnel_ipv6)], - }; - // FIXME: Used for debugging purposes during the migration to same IP. Remove when - // the migration is over. - if tunnel_ipv4 == *SAME_IP_V4 || tunnel_ipv6 == *SAME_IP_V6 { - log::debug!("Same IP is being used"); - } else { - log::debug!("Same IP is NOT being used"); - } - + GetRelay::Wireguard { + endpoint, + obfuscator, + inner, + } => { let (obfuscator_relay, obfuscator_config) = match obfuscator { Some(obfuscator) => (Some(obfuscator.relay), Some(obfuscator.config)), None => (None, None), }; + let (wg_entry, wg_exit) = match inner { + WireguardConfig::Singlehop { exit } => (None, exit), + WireguardConfig::Multihop { exit, entry } => (Some(entry), exit), + }; self.last_generated_relays = Some(LastSelectedRelays::WireGuard { - wg_entry: entry_relay.clone(), - wg_exit: relay.clone(), + wg_entry, + wg_exit, obfuscator: obfuscator_relay, }); - Ok(wireguard::TunnelParameters { - connection: wireguard::ConnectionConfig { - tunnel, - peer: endpoint.peer, - exit_peer: endpoint.exit_peer, - ipv4_gateway: endpoint.ipv4_gateway, - ipv6_gateway: Some(endpoint.ipv6_gateway), - #[cfg(target_os = "linux")] - fwmark: Some(mullvad_types::TUNNEL_FWMARK), - }, - options: self - .tunnel_options - .wireguard - .clone() - .into_talpid_tunnel_options(), - generic_options: self.tunnel_options.generic.clone(), - obfuscation: obfuscator_config, - } - .into()) + Ok(self.create_wireguard_tunnel_parameters(endpoint, data, obfuscator_config)) } + GetRelay::Custom(custom_relay) => { + self.last_generated_relays = None; + custom_relay + // TODO: generate proxy settings for custom tunnels + .to_tunnel_parameters(self.tunnel_options.clone(), None) + .map_err(|e| { + log::error!("Failed to resolve hostname for custom tunnel config: {}", e); + Error::ResolveCustomHostname + }) + } + } + } + + #[cfg(not(target_os = "android"))] + fn create_openvpn_tunnel_parameters( + &self, + endpoint: Endpoint, + data: PrivateAccountAndDevice, + bridge_settings: Option, + ) -> TunnelParameters { + openvpn::TunnelParameters { + config: openvpn::ConnectionConfig::new(endpoint, data.account_token, "-".to_string()), + options: self.tunnel_options.openvpn.clone(), + generic_options: self.tunnel_options.generic.clone(), + proxy: bridge_settings, + #[cfg(target_os = "linux")] + fwmark: mullvad_types::TUNNEL_FWMARK, + } + .into() + } + + fn create_wireguard_tunnel_parameters( + &self, + endpoint: MullvadWireguardEndpoint, + data: PrivateAccountAndDevice, + obfuscator_config: Option, + ) -> TunnelParameters { + let tunnel_ipv4 = data.device.wg_data.addresses.ipv4_address.ip(); + let tunnel_ipv6 = data.device.wg_data.addresses.ipv6_address.ip(); + let tunnel = wireguard::TunnelConfig { + private_key: data.device.wg_data.private_key, + addresses: vec![IpAddr::from(tunnel_ipv4), IpAddr::from(tunnel_ipv6)], + }; + // FIXME: Used for debugging purposes during the migration to same IP. Remove when + // the migration is over. + if tunnel_ipv4 == *SAME_IP_V4 || tunnel_ipv6 == *SAME_IP_V6 { + log::debug!("Same IP is being used"); + } else { + log::debug!("Same IP is NOT being used"); } + + wireguard::TunnelParameters { + connection: wireguard::ConnectionConfig { + tunnel, + peer: endpoint.peer, + exit_peer: endpoint.exit_peer, + ipv4_gateway: endpoint.ipv4_gateway, + ipv6_gateway: Some(endpoint.ipv6_gateway), + #[cfg(target_os = "linux")] + fwmark: Some(mullvad_types::TUNNEL_FWMARK), + }, + options: self + .tunnel_options + .wireguard + .clone() + .into_talpid_tunnel_options(), + generic_options: self.tunnel_options.generic.clone(), + obfuscation: obfuscator_config, + } + .into() } async fn device(&self) -> Result { diff --git a/mullvad-management-interface/src/types/conversions/relay_constraints.rs b/mullvad-management-interface/src/types/conversions/relay_constraints.rs index cdae59d60f38..47d097abe916 100644 --- a/mullvad-management-interface/src/types/conversions/relay_constraints.rs +++ b/mullvad-management-interface/src/types/conversions/relay_constraints.rs @@ -1,7 +1,6 @@ use crate::types::{conversions::net::try_tunnel_type_from_i32, proto, FromProtobufTypeError}; use mullvad_types::{ - custom_list::Id, - relay_constraints::{Constraint, GeographicLocationConstraint}, + constraints::Constraint, custom_list::Id, relay_constraints::GeographicLocationConstraint, }; use std::str::FromStr; use talpid_types::net::proxy::CustomProxy; @@ -254,7 +253,7 @@ impl From for proto::RelaySetti .ip_version .option() .map(|ipv| i32::from(proto::IpVersion::from(ipv))), - use_multihop: constraints.wireguard_constraints.use_multihop, + use_multihop: constraints.wireguard_constraints.multihop(), entry_location: constraints .wireguard_constraints .entry_location diff --git a/mullvad-relay-selector/Cargo.toml b/mullvad-relay-selector/Cargo.toml index c5f83018eaaa..ec8943df0b5b 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 000000000000..5e6b51119566 --- /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 000000000000..f988be1b8931 --- /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), + /// 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), +} + +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 b255ef91053f..17b781519a5d 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, +mod constants; +mod error; +#[cfg_attr(target_os = "android", allow(unused))] +mod relay_selector; + +// Re-exports +pub use error::Error; +pub use relay_selector::detailer; +pub use relay_selector::{ + query, GetRelay, RelaySelector, SelectedBridge, SelectedObfuscator, SelectorConfig, + WireguardConfig, RETRY_ORDER, }; -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 = Constraint::Only(51820); -const WIREGUARD_EXIT_IP_VERSION: Constraint = 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, -} - -impl ParsedRelays { - /// Return a flat iterator with all relays - pub fn relays(&self) -> impl Iterator + 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 { - 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, - resource_path: impl AsRef, - overrides: &[RelayOverride], - ) -> Result { - // 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, overrides: &[RelayOverride]) -> Result { - 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, -} - -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>, - parsed_relays: Arc>, -} - -impl RelaySelector { - /// Returns a new `RelaySelector` backed by relays cached on disk. - pub fn new( - config: SelectorConfig, - resource_path: impl AsRef, - cache_path: impl AsRef, - ) -> 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::::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 { - 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, - Option, - ), - 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 { - #[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 { - 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 = { - 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 { - 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, - exit_locations: Constraint, - custom_lists: &CustomListsSettings, - ) -> Result { - 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 { - 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 = 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 { - 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 { - 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, - ) -> Result<(Relay, MullvadWireguardEndpoint), Error> { - let matching_relays: Vec = { - 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, 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 { - 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>( - &self, - constraints: &InternalBridgeConstraints, - location: Option, - 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 = { - 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 = 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, 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 { - 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 { - 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 { - 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, - providers: &Constraint, - ownership: Constraint, - ) -> (Constraint, 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)); - - (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, 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 { - // 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, 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( - &self, - matcher: &RelayMatcher, - ) -> Result { - let matching_relays: Vec = { - 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 { - 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, -} - -#[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, Error> { - self.get_obfuscator_inner(&self.config.lock().unwrap(), relay, endpoint, retry_attempt) - } - } - - static RELAYS: Lazy = 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:?}", - ) - } -} diff --git a/mullvad-relay-selector/src/matcher.rs b/mullvad-relay-selector/src/matcher.rs deleted file mode 100644 index e02d8abc450b..000000000000 --- 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 { - /// 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, - pub providers: Constraint, - pub ownership: Constraint, - pub endpoint_matcher: T, -} - -impl RelayMatcher { - 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 { - RelayMatcher { - endpoint_matcher: self.endpoint_matcher.wireguard, - locations: self.locations, - providers: self.providers, - ownership: self.ownership, - } - } -} - -impl RelayMatcher { - pub fn set_peer(&mut self, peer: Relay) { - self.endpoint_matcher.peer = Some(peer); - } -} - -impl RelayMatcher { - /// 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 + Clone>( - &self, - relays: R, - ) -> Vec { - 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 { - 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; -} - -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 { - 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 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, -} - -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 { - #[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, - pub port: Constraint, - pub ip_version: Constraint, - - 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 { - 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 { - 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 { - 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 { - 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 { - 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 000000000000..b0e6e3b0eac5 --- /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), + #[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 { + 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 { + 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 { + /// 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 { + 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 { + 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`: 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 { + 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 { + // 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, + data: &OpenVpnEndpointData, + exit: &Relay, +) -> Result { + 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, + data: &OpenVpnEndpointData, + exit: &Relay, +) -> Result { + 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, + 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 000000000000..5ad5bf7ab471 --- /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, + 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( + 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 { + 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, + udp2tcp_ports: &[u16], + relay: Relay, + endpoint: &MullvadWireguardEndpoint, +) -> Option { + 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, + udp2tcp_ports: &[u16], +) -> Option { + 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 000000000000..e937b159390e --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -0,0 +1,184 @@ +//! 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 + Clone>( + query: &RelayQuery, + relays: R, + custom_lists: &CustomListsSettings, +) -> Vec { + 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 + Clone>( + constraints: &InternalBridgeConstraints, + relays: R, + custom_lists: &CustomListsSettings, +) -> Vec { + 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>, + relay: &Relay, +) -> bool { + filter.matches(relay) +} + +/// Returns whether `relay` satisfy the ownership constraint posed by `filter`. +pub fn filter_on_ownership(filter: &Constraint, relay: &Relay) -> bool { + filter.matches(relay) +} + +/// Returns whether `relay` satisfy the providers constraint posed by `filter`. +pub fn filter_on_providers(filter: &Constraint, 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 is a Wireguard relay. +pub const fn filter_tunnel_type(filter: &Constraint, relay: &Relay) -> bool { + match filter { + Constraint::Any => true, + Constraint::Only(typ) => match typ { + 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, + custom_lists: &'a CustomListsSettings, + ) -> Constraint> { + 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>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().copied() + } +} + +impl Match 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 000000000000..cf4830d2afc9 --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -0,0 +1,925 @@ +//! 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.ms` +pub static RETRY_ORDER: Lazy> = 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>, + parsed_relays: Arc>, +} + +#[derive(Clone)] +pub struct SelectorConfig { + // Normal relay settings + pub relay_settings: RelaySettings, + pub custom_lists: CustomListsSettings, + pub relay_overrides: Vec, + // Wireguard specific data + pub obfuscation_settings: ObfuscationSettings, + // OpenVPN specific data + pub bridge_state: BridgeState, + pub bridge_settings: BridgeSettings, +} + +/// 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 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 superflous. Also, there exists no mapping to [`RelayQueryBuilder`]. +#[derive(Debug, Clone)] +enum SpecializedSelectorConfig<'a> { + // This variant implements `From 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, + inner: WireguardConfig, + }, + #[cfg(not(target_os = "android"))] + OpenVpn { + endpoint: Endpoint, + exit: Relay, + bridge: Option, + }, + 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> 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, + cache_path: impl AsRef, + ) -> 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::::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 { + 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 { + 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 { + 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`]. + /// + /// [`RETRY_ORDER`]: crate::RETRY_ORDER + pub fn get_relay(&self, retry_attempt: usize) -> Result { + self.get_relay_with_order(&RETRY_ORDER, retry_attempt) + } + + /// 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 { + match SpecializedSelectorConfig::from(config) { + // This case is not really interesting + SpecializedSelectorConfig::Custom(_) => None, + SpecializedSelectorConfig::Normal(config) => Some( + Self::pick_and_merge_query( + &RETRY_ORDER, + RelayQuery::from(config), + connection_attempt, + ) + .tunnel_protocol + .unwrap_or(TunnelType::Wireguard), + ), + } + } + + /// Returns a random relay and relay endpoint matching the current constraints defined by + /// `retry_order` corresponsing to `retry_attempt`. + pub fn get_relay_with_order( + &self, + retry_order: &[RelayQuery], + retry_attempt: usize, + ) -> Result { + 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_order, user_preferences, retry_attempt); + 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. + fn pick_and_merge_query( + retry_order: &[RelayQuery], + user_preferences: RelayQuery, + retry_attempt: usize, + ) -> RelayQuery { + retry_order + .iter() + .cycle() + .filter_map(|constraint| constraint.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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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, 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 { + 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 { + 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, 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 { + 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>( + parsed_relays: &ParsedRelays, + constraints: &InternalBridgeConstraints, + location: Option, + 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>( + relays: Vec, + location: T, + ) -> Option { + /// 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 = 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 { + use std::ops::Not; + if query.location.is_any() { + return None; + } + + let matching_locations: Vec = + 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 { + // 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 000000000000..7b53b529ee88 --- /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, +} + +impl ParsedRelays { + /// Return a flat iterator with all relays + pub fn relays(&self) -> impl Iterator + 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 { + 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, + resource_path: impl AsRef, + overrides: &[RelayOverride], + ) -> Result { + // 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, overrides: &[RelayOverride]) -> Result { + 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 000000000000..5a6206b4dc34 --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -0,0 +1,855 @@ +//! 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, + pub providers: Constraint, + pub ownership: Constraint, + pub tunnel_protocol: Constraint, + 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 + 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 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, + pub ip_version: Constraint, + pub use_multihop: Constraint, + pub entry_location: Constraint, + pub obfuscation: SelectedObfuscation, + pub udp2tcp_port: Constraint, +} + +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 + 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 + 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 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, + pub bridge_settings: Constraint, +} + +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 + 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), +} + +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) -> 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 + 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 + 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 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 + where + Self: PartialEq, + Self: Sized; +} + +impl Intersection for Constraint { + /// 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) -> Option> { + use Constraint::*; + match (self, other) { + (Any, Any) => Some(Any), + (Only(t), Any) | (Any, Only(t)) => Some(Only(t)), + // Pick any of `left` or `right` if they are the same. + (Only(left), Only(right)) if left == right => Some(Only(left)), + _ => None, + } + } +} + +#[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 { + 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 RelayQueryBuilder { + /// 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 { + /// 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 { + RelayQueryBuilder { + query: RelayQuery::new(), + protocol: Any, + } + } + /// Set the VPN protocol for this [`RelayQueryBuilder`] to Wireguard. + pub fn wireguard(mut self) -> RelayQueryBuilder> { + 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> { + 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: Multihop, + obfuscation: Obfuscation, + } + + // This impl-block is quantified over all configurations + impl RelayQueryBuilder> { + /// 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 RelayQueryBuilder> { + /// Enable multihop. + /// + /// To configure the entry relay, see [`RelayQueryBuilder::entry`]. + pub fn multihop(mut self) -> RelayQueryBuilder> { + 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 RelayQueryBuilder> { + /// 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 RelayQueryBuilder> { + /// Enable `UDP2TCP` obufscation. This will in turn enable the option to configure the + /// `UDP2TCP` port. + pub fn udp2tcp( + mut self, + ) -> RelayQueryBuilder> { + 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 RelayQueryBuilder> { + /// 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 { + transport_port: TransportPort, + bridge_settings: Bridge, + } + + // This impl-block is quantified over all configurations + impl RelayQueryBuilder> { + /// 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> { + 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 RelayQueryBuilder> { + /// Configure what port to use when connecting to a relay. + pub fn port(mut self, port: u16) -> RelayQueryBuilder> { + 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 RelayQueryBuilder> { + /// Enable Bridges. This also sets the transport protocol to TCP and resets any + /// previous port settings. + pub fn bridge( + mut self, + ) -> RelayQueryBuilder> { + 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 RelayQueryBuilder> { + /// 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( + base_strategy: impl Strategy + 'static, + ) -> impl Strategy> + where + T: core::fmt::Debug + std::clone::Clone + 'static, + { + prop_oneof![any(), only(base_strategy),] + } + + pub fn only( + base_strategy: impl Strategy + 'static, + ) -> impl Strategy> + where + T: core::fmt::Debug + std::clone::Clone + 'static, + { + base_strategy.prop_map(Constraint::Only) + } + + pub fn any() -> impl Strategy> + where + T: core::fmt::Debug + std::clone::Clone + 'static, + { + Just(Constraint::Any) + } + + proptest! { + #[test] + fn identity(x in only(proptest::arbitrary::any::())) { + // 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::())) { + // 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::()), + y in constraint(proptest::arbitrary::any::())) { + // Commutativity law + // x ∩ y = y ∩ x + prop_assert_eq!(x.intersection(y), y.intersection(x)) + } + + #[test] + fn associativity(x in constraint(proptest::arbitrary::any::()), + y in constraint(proptest::arbitrary::any::()), + z in constraint(proptest::arbitrary::any::())) + { + // 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 000000000000..4b66f4e4208c --- /dev/null +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -0,0 +1,1104 @@ +//! 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, 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 = 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) + .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) + ); + // 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, HashSet) = 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_order(&retry_order, retry_attempt) + .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).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).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()) + } +} diff --git a/mullvad-types/src/constraints/constraint.rs b/mullvad-types/src/constraints/constraint.rs new file mode 100644 index 000000000000..c1669e33ae02 --- /dev/null +++ b/mullvad-types/src/constraints/constraint.rs @@ -0,0 +1,165 @@ +//! General constraints. + +#[cfg(target_os = "android")] +use jnix::{FromJava, IntoJava}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +/// Limits the set of [`crate::relay_list::Relay`]s that a `RelaySelector` may select. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(target_os = "android", derive(FromJava, IntoJava))] +#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] +#[cfg_attr(target_os = "android", jnix(bounds = "T: android.os.Parcelable"))] +pub enum Constraint { + Any, + Only(T), +} + +impl fmt::Display for Constraint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Constraint::Any => "any".fmt(f), + Constraint::Only(value) => fmt::Display::fmt(value, f), + } + } +} + +impl Constraint { + pub fn unwrap(self) -> T { + match self { + Constraint::Any => panic!("called `Constraint::unwrap()` on an `Any` value"), + Constraint::Only(value) => value, + } + } + + pub fn unwrap_or(self, other: T) -> T { + match self { + Constraint::Any => other, + Constraint::Only(value) => value, + } + } + + pub fn or(self, other: Constraint) -> Constraint { + match self { + Constraint::Any => other, + Constraint::Only(value) => Constraint::Only(value), + } + } + + pub fn map U>(self, f: F) -> Constraint { + match self { + Constraint::Any => Constraint::Any, + Constraint::Only(value) => Constraint::Only(f(value)), + } + } + + pub const fn is_any(&self) -> bool { + match self { + Constraint::Any => true, + Constraint::Only(_value) => false, + } + } + + pub const fn is_only(&self) -> bool { + !self.is_any() + } + + pub const fn as_ref(&self) -> Constraint<&T> { + match self { + Constraint::Any => Constraint::Any, + Constraint::Only(ref value) => Constraint::Only(value), + } + } + + pub fn option(self) -> Option { + match self { + Constraint::Any => None, + Constraint::Only(value) => Some(value), + } + } + + /// Returns true if the constraint is an `Only` and the value inside of it matches a predicate. + pub fn is_only_and(self, f: impl FnOnce(T) -> bool) -> bool { + self.option().is_some_and(f) + } +} + +impl Constraint { + pub fn matches_eq(&self, other: &T) -> bool { + match self { + Constraint::Any => true, + Constraint::Only(ref value) => value == other, + } + } +} + +// Using the default attribute fails on Android +#[allow(clippy::derivable_impls)] +impl Default for Constraint { + fn default() -> Self { + Constraint::Any + } +} + +impl From> for Constraint { + fn from(value: Option) -> Self { + match value { + Some(value) => Constraint::Only(value), + None => Constraint::Any, + } + } +} + +impl Copy for Constraint {} + +impl FromStr for Constraint { + type Err = T::Err; + + fn from_str(value: &str) -> Result { + if value.eq_ignore_ascii_case("any") { + return Ok(Self::Any); + } + Ok(Self::Only(T::from_str(value)?)) + } +} + +// Clap + +#[cfg(feature = "clap")] +#[derive(fmt::Debug, Clone)] +pub struct ConstraintParser(T); + +#[cfg(feature = "clap")] +impl clap::builder::ValueParserFactory + for Constraint +where + ::Parser: Sync + Send + Clone, +{ + type Parser = ConstraintParser; + + fn value_parser() -> Self::Parser { + ConstraintParser(T::value_parser()) + } +} + +#[cfg(feature = "clap")] +impl clap::builder::TypedValueParser for ConstraintParser +where + T::Value: fmt::Debug, +{ + type Value = Constraint; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + if value.eq_ignore_ascii_case("any") { + return Ok(Constraint::Any); + } + self.0.parse_ref(cmd, arg, value).map(Constraint::Only) + } +} diff --git a/mullvad-types/src/constraints/mod.rs b/mullvad-types/src/constraints/mod.rs new file mode 100644 index 000000000000..4caa31e901c0 --- /dev/null +++ b/mullvad-types/src/constraints/mod.rs @@ -0,0 +1,35 @@ +//! Constrain yourself. + +mod constraint; + +// Re-export bits & pieces from `constraints.rs` as needed +pub use constraint::Constraint; + +/// A limited variant of Sets. +pub trait Set { + fn is_subset(&self, other: &T) -> bool; +} + +pub trait Match { + fn matches(&self, other: &T) -> bool; +} +impl, U> Match for Constraint { + fn matches(&self, other: &U) -> bool { + match *self { + Constraint::Any => true, + Constraint::Only(ref value) => value.matches(other), + } + } +} + +impl, U> Set> for Constraint { + fn is_subset(&self, other: &Constraint) -> bool { + match self { + Constraint::Any => other.is_any(), + Constraint::Only(ref constraint) => match other { + Constraint::Only(ref other_constraint) => constraint.is_subset(other_constraint), + _ => true, + }, + } + } +} diff --git a/mullvad-types/src/endpoint.rs b/mullvad-types/src/endpoint.rs index 28319b72fcbd..283ceb6ee338 100644 --- a/mullvad-types/src/endpoint.rs +++ b/mullvad-types/src/endpoint.rs @@ -29,13 +29,4 @@ impl MullvadEndpoint { ), } } - - pub fn unwrap_wireguard(&self) -> &MullvadWireguardEndpoint { - match self { - Self::Wireguard(endpoint) => endpoint, - other => { - panic!("Expected WireGuard enum variant but got {other:?}"); - } - } - } } diff --git a/mullvad-types/src/lib.rs b/mullvad-types/src/lib.rs index 8b8bd35fc408..231f80300b51 100644 --- a/mullvad-types/src/lib.rs +++ b/mullvad-types/src/lib.rs @@ -1,6 +1,7 @@ pub mod access_method; pub mod account; pub mod auth_failed; +pub mod constraints; pub mod custom_list; pub mod device; pub mod endpoint; diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index d0a42e230359..7b027050e82f 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -2,6 +2,7 @@ //! updated as well. use crate::{ + constraints::{Constraint, Match, Set}, custom_list::{CustomListsSettings, Id}, location::{CityCode, CountryCode, Hostname}, relay_list::Relay, @@ -18,186 +19,6 @@ use std::{ }; use talpid_types::net::{proxy::CustomProxy, IpVersion, TransportProtocol, TunnelType}; -pub trait Match { - fn matches(&self, other: &T) -> bool; -} - -pub trait Set { - fn is_subset(&self, other: &T) -> bool; -} - -/// Limits the set of [`crate::relay_list::Relay`]s that a `RelaySelector` may select. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -#[cfg_attr(target_os = "android", derive(FromJava, IntoJava))] -#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] -#[cfg_attr(target_os = "android", jnix(bounds = "T: android.os.Parcelable"))] -pub enum Constraint { - Any, - Only(T), -} - -impl fmt::Display for Constraint { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - Constraint::Any => "any".fmt(f), - Constraint::Only(value) => fmt::Display::fmt(value, f), - } - } -} - -impl Constraint { - pub fn unwrap(self) -> T { - match self { - Constraint::Any => panic!("called `Constraint::unwrap()` on an `Any` value"), - Constraint::Only(value) => value, - } - } - - pub fn unwrap_or(self, other: T) -> T { - match self { - Constraint::Any => other, - Constraint::Only(value) => value, - } - } - - pub fn or(self, other: Constraint) -> Constraint { - match self { - Constraint::Any => other, - Constraint::Only(value) => Constraint::Only(value), - } - } - - pub fn map U>(self, f: F) -> Constraint { - match self { - Constraint::Any => Constraint::Any, - Constraint::Only(value) => Constraint::Only(f(value)), - } - } - - pub fn is_any(&self) -> bool { - match self { - Constraint::Any => true, - Constraint::Only(_value) => false, - } - } - - pub fn is_only(&self) -> bool { - !self.is_any() - } - - pub fn as_ref(&self) -> Constraint<&T> { - match self { - Constraint::Any => Constraint::Any, - Constraint::Only(ref value) => Constraint::Only(value), - } - } - - pub fn option(self) -> Option { - match self { - Constraint::Any => None, - Constraint::Only(value) => Some(value), - } - } -} - -impl Constraint { - pub fn matches_eq(&self, other: &T) -> bool { - match self { - Constraint::Any => true, - Constraint::Only(ref value) => value == other, - } - } -} - -// Using the default attribute fails on Android -#[allow(clippy::derivable_impls)] -impl Default for Constraint { - fn default() -> Self { - Constraint::Any - } -} - -impl Copy for Constraint {} - -impl, U> Match for Constraint { - fn matches(&self, other: &U) -> bool { - match *self { - Constraint::Any => true, - Constraint::Only(ref value) => value.matches(other), - } - } -} - -impl, U> Set> for Constraint { - fn is_subset(&self, other: &Constraint) -> bool { - match self { - Constraint::Any => other.is_any(), - Constraint::Only(ref constraint) => match other { - Constraint::Only(ref other_constraint) => constraint.is_subset(other_constraint), - _ => true, - }, - } - } -} - -impl From> for Constraint { - fn from(value: Option) -> Self { - match value { - Some(value) => Constraint::Only(value), - None => Constraint::Any, - } - } -} - -impl FromStr for Constraint { - type Err = T::Err; - - fn from_str(value: &str) -> Result { - if value.eq_ignore_ascii_case("any") { - return Ok(Self::Any); - } - Ok(Self::Only(T::from_str(value)?)) - } -} - -#[cfg(feature = "clap")] -impl clap::builder::ValueParserFactory - for Constraint -where - ::Parser: Sync + Send + Clone, -{ - type Parser = ConstraintParser; - - fn value_parser() -> Self::Parser { - ConstraintParser(T::value_parser()) - } -} - -#[cfg(feature = "clap")] -#[derive(fmt::Debug, Clone)] -pub struct ConstraintParser(T); - -#[cfg(feature = "clap")] -impl clap::builder::TypedValueParser for ConstraintParser -where - T::Value: fmt::Debug, -{ - type Value = Constraint; - - fn parse_ref( - &self, - cmd: &clap::Command, - arg: Option<&clap::Arg>, - value: &std::ffi::OsStr, - ) -> Result { - if value.eq_ignore_ascii_case("any") { - return Ok(Constraint::Any); - } - self.0.parse_ref(cmd, arg, value).map(Constraint::Only) - } -} - /// Specifies a specific endpoint or [`RelayConstraints`] to use when `mullvad-daemon` selects a /// relay. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] @@ -264,36 +85,9 @@ pub enum LocationConstraint { CustomList { list_id: Id }, } -#[derive(Debug, Clone)] -pub enum ResolvedLocationConstraint { - Location(GeographicLocationConstraint), - Locations(Vec), -} - -impl ResolvedLocationConstraint { - pub fn from_constraint( - location: Constraint, - custom_lists: &CustomListsSettings, - ) -> Constraint { - match location { - Constraint::Any => Constraint::Any, - Constraint::Only(LocationConstraint::Location(location)) => { - Constraint::Only(Self::Location(location)) - } - Constraint::Only(LocationConstraint::CustomList { list_id }) => custom_lists - .iter() - .find(|list| list.id == list_id) - .map(|custom_list| { - Constraint::Only(Self::Locations( - custom_list.locations.iter().cloned().collect(), - )) - }) - .unwrap_or_else(|| { - log::warn!("Resolved non-existent custom list"); - Constraint::Only(ResolvedLocationConstraint::Locations(vec![])) - }), - } - } +pub struct LocationConstraintFormatter<'a> { + pub constraint: &'a LocationConstraint, + pub custom_lists: &'a CustomListsSettings, } impl From for LocationConstraint { @@ -302,61 +96,6 @@ impl From for LocationConstraint { } } -impl Set> for Constraint { - fn is_subset(&self, other: &Self) -> bool { - match self { - Constraint::Any => other.is_any(), - Constraint::Only(ResolvedLocationConstraint::Location(location)) => match other { - Constraint::Any => true, - Constraint::Only(ResolvedLocationConstraint::Location(other_location)) => { - location.is_subset(other_location) - } - Constraint::Only(ResolvedLocationConstraint::Locations(other_locations)) => { - other_locations - .iter() - .any(|other_location| location.is_subset(other_location)) - } - }, - Constraint::Only(ResolvedLocationConstraint::Locations(locations)) => match other { - Constraint::Any => true, - Constraint::Only(ResolvedLocationConstraint::Location(other_location)) => locations - .iter() - .all(|location| location.is_subset(other_location)), - Constraint::Only(ResolvedLocationConstraint::Locations(other_locations)) => { - for location in locations { - if !other_locations - .iter() - .any(|other_location| location.is_subset(other_location)) - { - return false; - } - } - true - } - }, - } - } -} - -impl Constraint { - pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { - match self { - Constraint::Any => true, - Constraint::Only(ResolvedLocationConstraint::Location(location)) => { - location.matches_with_opts(relay, ignore_include_in_country) - } - Constraint::Only(ResolvedLocationConstraint::Locations(locations)) => locations - .iter() - .any(|loc| loc.matches_with_opts(relay, ignore_include_in_country)), - } - } -} - -pub struct LocationConstraintFormatter<'a> { - pub constraint: &'a LocationConstraint, - pub custom_lists: &'a CustomListsSettings, -} - impl<'a> fmt::Display for LocationConstraintFormatter<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.constraint { @@ -507,15 +246,38 @@ pub enum GeographicLocationConstraint { } impl GeographicLocationConstraint { - pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { + /// Create a new [`GeographicLocationConstraint`] given a country. + pub fn country(country: impl Into) -> Self { + GeographicLocationConstraint::Country(country.into()) + } + + /// Create a new [`GeographicLocationConstraint`] given a country and city. + pub fn city(country: impl Into, city: impl Into) -> Self { + GeographicLocationConstraint::City(country.into(), city.into()) + } + + /// Create a new [`GeographicLocationConstraint`] given a country, city and hostname. + pub fn hostname( + country: impl Into, + city: impl Into, + hostname: impl Into, + ) -> Self { + GeographicLocationConstraint::Hostname(country.into(), city.into(), hostname.into()) + } + + // TODO(markus): Document + pub fn is_country(&self) -> bool { + matches!(self, GeographicLocationConstraint::Country(_)) + } +} + +impl Match for GeographicLocationConstraint { + fn matches(&self, relay: &Relay) -> bool { match self { - GeographicLocationConstraint::Country(ref country) => { - relay - .location - .as_ref() - .map_or(false, |loc| loc.country_code == *country) - && (ignore_include_in_country || relay.include_in_country) - } + GeographicLocationConstraint::Country(ref country) => relay + .location + .as_ref() + .map_or(false, |loc| loc.country_code == *country), GeographicLocationConstraint::City(ref country, ref city) => { relay.location.as_ref().map_or(false, |loc| { loc.country_code == *country && loc.city_code == *city @@ -532,34 +294,6 @@ impl GeographicLocationConstraint { } } -impl Constraint> { - pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { - match self { - Constraint::Only(constraint) => constraint - .iter() - .any(|loc| loc.matches_with_opts(relay, ignore_include_in_country)), - Constraint::Any => true, - } - } -} - -impl Constraint { - pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { - match self { - Constraint::Only(constraint) => { - constraint.matches_with_opts(relay, ignore_include_in_country) - } - Constraint::Any => true, - } - } -} - -impl Match for GeographicLocationConstraint { - fn matches(&self, relay: &Relay) -> bool { - self.matches_with_opts(relay, false) - } -} - impl Set for GeographicLocationConstraint { /// Returns whether `self` is equal to or a subset of `other`. fn is_subset(&self, other: &Self) -> bool { @@ -667,9 +401,11 @@ pub struct Providers { pub struct NoProviders(()); impl Providers { - pub fn new(providers: impl Iterator) -> Result { + pub fn new( + providers: impl IntoIterator>, + ) -> Result { let providers = Providers { - providers: providers.collect(), + providers: providers.into_iter().map(Into::into).collect(), }; if providers.providers.is_empty() { return Err(NoProviders(())); @@ -768,6 +504,18 @@ pub struct WireguardConstraints { pub entry_location: Constraint, } +impl WireguardConstraints { + /// Enable or disable multihop. + pub fn use_multihop(&mut self, multihop: bool) { + self.use_multihop = multihop + } + + /// Check if multihop is enabled. + pub fn multihop(&self) -> bool { + self.use_multihop + } +} + pub struct WireguardConstraintsFormatter<'a> { pub constraints: &'a WireguardConstraints, pub custom_lists: &'a CustomListsSettings, @@ -782,7 +530,7 @@ impl<'a> fmt::Display for WireguardConstraintsFormatter<'a> { if let Constraint::Only(ip_version) = self.constraints.ip_version { write!(f, ", {},", ip_version)?; } - if self.constraints.use_multihop { + if self.constraints.multihop() { let location = self.constraints.entry_location.as_ref().map(|location| { LocationConstraintFormatter { constraint: location, diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 55649b38c13e..5711b812c133 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -106,6 +106,57 @@ pub struct Relay { pub location: Option, } +impl PartialEq for Relay { + /// Hostnames are assumed to be unique per relay, i.e. a relay can be uniquely identified by its hostname. + /// + /// # Example + /// + /// ```rust + /// # use mullvad_types::{relay_list::Relay, relay_list::{RelayEndpointData, WireguardRelayEndpointData}}; + /// # use talpid_types::net::wireguard::PublicKey; + /// + /// let relay = Relay { + /// hostname: "se9-wireguard".to_string(), + /// ipv4_addr_in: "185.213.154.68".parse().unwrap(), + /// # ipv6_addr_in: None, + /// # 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, + /// }; + /// + /// let mut different_relay = relay.clone(); + /// // Modify the relay's IPv4 address - should not matter for the equality check + /// different_relay.ipv4_addr_in = "1.3.3.7".parse().unwrap(); + /// assert_eq!(relay, different_relay); + /// + /// // What matter's for the equality check is the hostname of the relay + /// different_relay.hostname = "dk-cph-wg-001".to_string(); + /// assert_ne!(relay, different_relay); + /// ``` + fn eq(&self, other: &Self) -> bool { + self.hostname == other.hostname + } +} + +/// Hostnames are assumed to be unique per relay, i.e. a relay can be uniquely identified by its hostname. +impl Eq for Relay {} + +/// Hostnames are assumed to be unique per relay, i.e. a relay can be uniquely identified by its hostname. +impl std::hash::Hash for Relay { + fn hash(&self, state: &mut H) { + self.hostname.hash(state) + } +} + /// Specifies the type of a relay or relay-specific endpoint data. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index c0048a7e6214..2a4adbb06951 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -1,8 +1,9 @@ use crate::{ access_method, + constraints::Constraint, custom_list::CustomListsSettings, relay_constraints::{ - BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint, LocationConstraint, + BridgeSettings, BridgeState, GeographicLocationConstraint, LocationConstraint, ObfuscationSettings, RelayConstraints, RelayOverride, RelaySettings, RelaySettingsFormatter, SelectedObfuscation, WireguardConstraints, }, diff --git a/talpid-future/Cargo.toml b/talpid-future/Cargo.toml index 1245fe1e2a86..9baa0245d186 100644 --- a/talpid-future/Cargo.toml +++ b/talpid-future/Cargo.toml @@ -15,5 +15,5 @@ rand = "0.8.5" talpid-time = { path = "../talpid-time" } [dev-dependencies] -proptest = "1.4" +proptest = { workspace = true } tokio = { workspace = true, features = [ "test-util", "macros" ] } diff --git a/talpid-wireguard/Cargo.toml b/talpid-wireguard/Cargo.toml index 5770fdb51f20..1fc6e13b3a40 100644 --- a/talpid-wireguard/Cargo.toml +++ b/talpid-wireguard/Cargo.toml @@ -81,5 +81,4 @@ features = [ ] [dev-dependencies] -proptest = "1.4" -tokio = { workspace = true, features = ["time", "test-util"] } \ No newline at end of file +proptest = { workspace = true } diff --git a/test/Cargo.lock b/test/Cargo.lock index b974ef9217ce..817d5e59313a 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -1390,6 +1390,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1744,8 +1753,10 @@ version = "0.0.0" dependencies = [ "chrono", "ipnetwork 0.16.0", + "itertools 0.12.1", "log", "mullvad-types", + "once_cell", "rand 0.8.5", "serde_json", "talpid-types", @@ -3042,6 +3053,7 @@ dependencies = [ "base64 0.13.1", "ipnetwork 0.16.0", "jnix", + "log", "serde", "thiserror", "x25519-dalek", diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index 108da125f612..2b23b93ca73a 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -5,9 +5,10 @@ use crate::network_monitor::{ use futures::StreamExt; use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; use mullvad_types::{ + constraints::Constraint, location::Location, relay_constraints::{ - BridgeSettings, Constraint, GeographicLocationConstraint, LocationConstraint, RelaySettings, + BridgeSettings, GeographicLocationConstraint, LocationConstraint, RelaySettings, }, relay_list::{Relay, RelayList}, states::TunnelState, diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs index 6d6dd204b236..63be2c43cecb 100644 --- a/test/test-manager/src/tests/install.rs +++ b/test/test-manager/src/tests/install.rs @@ -5,7 +5,7 @@ use super::helpers::{ use super::{Error, TestContext}; use mullvad_management_interface::MullvadProxyClient; -use mullvad_types::relay_constraints; +use mullvad_types::{constraints::Constraint, relay_constraints}; use test_macro::test_function; use test_rpc::meta::Os; use test_rpc::{mullvad_daemon::ServiceStatus, ServiceClient}; @@ -142,7 +142,7 @@ pub async fn test_upgrade_app(ctx: TestContext, rpc: ServiceClient) -> Result<() let relay_location_was_preserved = match &settings.relay_settings { relay_constraints::RelaySettings::Normal(relay_constraints::RelayConstraints { location: - relay_constraints::Constraint::Only(relay_constraints::LocationConstraint::Location( + Constraint::Only(relay_constraints::LocationConstraint::Location( relay_constraints::GeographicLocationConstraint::Country(country), )), .. diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index 3bbb83244300..d2d2a0fe301c 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -5,12 +5,16 @@ use super::{config::TEST_CONFIG, Error, TestContext}; use crate::network_monitor::{start_packet_monitor, MonitorOptions}; use mullvad_management_interface::MullvadProxyClient; -use mullvad_types::relay_constraints::{ - self, BridgeConstraints, BridgeSettings, BridgeType, Constraint, OpenVpnConstraints, - RelayConstraints, RelaySettings, SelectedObfuscation, TransportPort, - Udp2TcpObfuscationSettings, WireguardConstraints, +use mullvad_relay_selector::query::builder::RelayQueryBuilder; +use mullvad_types::{ + constraints::Constraint, + relay_constraints::{ + self, BridgeConstraints, BridgeSettings, BridgeType, OpenVpnConstraints, RelayConstraints, + RelaySettings, SelectedObfuscation, TransportPort, Udp2TcpObfuscationSettings, + WireguardConstraints, + }, + wireguard, }; -use mullvad_types::wireguard; use std::net::SocketAddr; use talpid_types::net::{ proxy::{CustomProxy, Socks5Local, Socks5Remote}, @@ -294,19 +298,17 @@ pub async fn test_multihop( rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, ) -> Result<(), Error> { - let wireguard_constraints = WireguardConstraints { - use_multihop: true, - ..Default::default() - }; - - let relay_settings = RelaySettings::Normal(RelayConstraints { - wireguard_constraints, - ..Default::default() - }); + let relay_constraints = RelayQueryBuilder::new() + .wireguard() + .multihop() + .into_constraint(); - set_relay_settings(&mut mullvad_client, relay_settings) - .await - .expect("failed to update relay settings"); + set_relay_settings( + &mut mullvad_client, + RelaySettings::Normal(relay_constraints), + ) + .await + .expect("failed to update relay settings"); // // Connect @@ -555,15 +557,13 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel( .await .expect("Failed to enable obfuscation"); + let relay_constraints = RelayQueryBuilder::new() + .wireguard() + .multihop() + .into_constraint(); + mullvad_client - .set_relay_settings(relay_constraints::RelaySettings::Normal(RelayConstraints { - wireguard_constraints: WireguardConstraints { - use_multihop: true, - ..Default::default() - }, - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - ..Default::default() - })) + .set_relay_settings(RelaySettings::Normal(relay_constraints)) .await .expect("Failed to update relay settings"); diff --git a/test/test-manager/src/tests/tunnel_state.rs b/test/test-manager/src/tests/tunnel_state.rs index 407328e0297e..bce72a779e97 100644 --- a/test/test-manager/src/tests/tunnel_state.rs +++ b/test/test-manager/src/tests/tunnel_state.rs @@ -12,9 +12,9 @@ use crate::{ use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ + constraints::Constraint, relay_constraints::{ - Constraint, GeographicLocationConstraint, LocationConstraint, RelayConstraints, - RelaySettings, + GeographicLocationConstraint, LocationConstraint, RelayConstraints, RelaySettings, }, relay_list::{Relay, RelayEndpointData}, states::TunnelState, @@ -291,8 +291,8 @@ pub async fn test_error_state( log::info!("Enter error state"); let relay_settings = RelaySettings::Normal(RelayConstraints { - location: Constraint::Only(LocationConstraint::Location( - GeographicLocationConstraint::Country("xx".to_string()), + location: Constraint::Only(LocationConstraint::from( + GeographicLocationConstraint::country("xx"), )), ..Default::default() });