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