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.
This commit is contained in:
Markus Pettersson 2024-02-16 16:24:33 +01:00
parent 66f2127aed
commit 707ecf44bd
No known key found for this signature in database
GPG Key ID: FBF42EDCDB3DEF72
51 changed files with 4650 additions and 3479 deletions

View File

@ -38,6 +38,9 @@ Line wrap the file at 100 chars. Th
### Changed
- Change default obfuscation setting to `auto`.
- Migrate obfuscation settings for existing users from `off` to `auto`.
- Change [default retry connection attempts][`relay selector defaults`].
[`relay selector defaults`]: docs/relay-selector.md#default-constraints-for-tunnel-endpoints
#### Android
- Migrate to Compose Navigation which also improves screen transition animations.

17
Cargo.lock generated
View File

@ -1618,6 +1618,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
@ -1935,7 +1944,7 @@ dependencies = [
"clap",
"clap_complete",
"futures",
"itertools",
"itertools 0.10.5",
"mullvad-management-interface",
"mullvad-types",
"mullvad-version",
@ -2101,9 +2110,11 @@ version = "0.0.0"
dependencies = [
"chrono",
"ipnetwork",
"itertools 0.12.1",
"log",
"mullvad-types",
"once_cell",
"proptest",
"rand 0.8.5",
"serde_json",
"talpid-types",
@ -2828,7 +2839,7 @@ checksum = "30d3e647e9eb04ddfef78dfee2d5b3fefdf94821c84b710a3d8ebc89ede8b164"
dependencies = [
"bytes",
"heck",
"itertools",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -2849,7 +2860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56075c27b20ae524d00f247b8a4dc333e5784f889fe63099f8e626bc8d73486c"
dependencies = [
"anyhow",
"itertools",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.51",

View File

@ -68,6 +68,8 @@ chrono = { version = "0.4.26", default-features = false}
clap = { version = "4.4.18", features = ["cargo", "derive"] }
once_cell = "1.13"
# Test dependencies
proptest = "1.4"
[profile.release]
opt-level = 3

View File

@ -47,37 +47,24 @@ Endpoints may be filtered by:
### Default constraints for tunnel endpoints
Whilst all user selected constraints are always honored, when the user hasn't selected any specific
constraints, following default ones will take effect:
constraints the following default ones will take effect:
- If no tunnel protocol is specified, the first three connection attempts will use WireGuard. All
remaining attempts will use OpenVPN. If no specific constraints are set:
- The first two attempts will connect to a Wireguard server, first on a random port, and then port
53.
- The third attempt will connect to a Wireguard server on port 80 with _udp2tcp_.
- Remaining attempts will connect to OpenVPN servers, first over UDP on two random ports, and then
over TCP on port 443. Remaining attempts alternate between TCP and UDP on random ports.
- The first three connection attempts will use Wireguard
- The first attempt will connect to a Wireguard relay on a random port
- The second attempt will connect to a Wireguard relay on port 443
- The third attempt will connect to a Wireguard relay over IPv6 (if IPv6 is configured on the host) on a random port
- The fourth-to-seventh attempt will alternate between Wireguard and OpenVPN
- The fourth attempt will connect to an OpenVPN relay over TCP on port 443
- The fifth attempt will connect to a Wireguard relay on a random port using [UDP2TCP obfuscation](https://github.com/mullvad/udp-over-tcp)
- The sixth attempt will connect to a Wireguard relay over IPv6 on a random port using UDP2TCP obfuscation (if IPv6 is configured on the host)
- The seventh attempt will connect to an OpenVPN relay over a bridge on a random port
- If the tunnel protocol is specified as WireGuard and obfuscation mode is set to _Auto_:
- First two attempts will be used without _udp2tcp_, using a random port on first attempt, and
port 53 on second attempt.
- Next two attempts will use _udp2tcp_ on ports 80 and 5001 respectively.
- The above steps repeat ad infinitum.
If no tunnel has been established after exhausting this list of attempts, the relay selector will
loop back to the first default constraint and continue its search from there.
If obfuscation is turned on, connections will alternate between port 80 and port 5001 using
_udp2tcp_ all of the time.
If obfuscation is turned _off_, WireGuard connections will first alternate between using
a random port and port 53, e.g. first attempt using port 22151, second 53, third
26107, fourth attempt using port 53, and so on.
If the user has specified a specific port for either _udp2tcp_ or WireGuard, it will override the
port selection, but it will not change the connection type described above (WireGuard or WireGuard
over _udp2tcp_).
- If no OpenVPN tunnel constraints are specified, then the first two attempts at selecting a tunnel
will try to select UDP endpoints on any port, and the third and fourth attempts will filter for
TCP endpoints on port 443. Any subsequent filtering attempts will alternate between TCP and UDP on
any port.
Any default constraint that is incompatible with user specified constraints will simply not be
considered. Conversely, all default constraints which do not conflict with user specified constraints
will be used in the search for a working tunnel endpoint on repeated connection failures.
## Selecting tunnel endpoint between filtered relays

View File

@ -2,9 +2,10 @@ use anyhow::{bail, Result};
use clap::Subcommand;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{
constraints::Constraint,
relay_constraints::{
BridgeConstraintsFormatter, BridgeState, BridgeType, Constraint, LocationConstraint,
Ownership, Provider, Providers,
BridgeConstraintsFormatter, BridgeState, BridgeType, LocationConstraint, Ownership,
Provider, Providers,
},
relay_list::RelayEndpointData,
};

View File

@ -3,8 +3,7 @@ use anyhow::{anyhow, bail, Result};
use clap::Subcommand;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{
relay_constraints::{Constraint, GeographicLocationConstraint},
relay_list::RelayList,
constraints::Constraint, relay_constraints::GeographicLocationConstraint, relay_list::RelayList,
};
#[derive(Subcommand, Debug)]

View File

@ -1,6 +1,9 @@
use anyhow::Result;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::relay_constraints::{Constraint, RelayConstraints, RelaySettings};
use mullvad_types::{
constraints::Constraint,
relay_constraints::{RelayConstraints, RelaySettings},
};
#[derive(clap::Subcommand, Debug)]
pub enum DebugCommands {

View File

@ -1,8 +1,9 @@
use anyhow::Result;
use clap::Subcommand;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::relay_constraints::{
Constraint, ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings,
use mullvad_types::{
constraints::Constraint,
relay_constraints::{ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings},
};
#[derive(Subcommand, Debug)]

View File

@ -3,10 +3,11 @@ use clap::Subcommand;
use itertools::Itertools;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{
constraints::{Constraint, Match},
location::{CountryCode, Location},
relay_constraints::{
Constraint, GeographicLocationConstraint, LocationConstraint, LocationConstraintFormatter,
Match, OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelayOverride,
GeographicLocationConstraint, LocationConstraint, LocationConstraintFormatter,
OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelayOverride,
RelaySettings, TransportPort, WireguardConstraints,
},
relay_list::{RelayEndpointData, RelayListCountry},
@ -318,7 +319,7 @@ impl Relay {
print_option!(
"Multihop state",
if constraints.wireguard_constraints.use_multihop {
if constraints.wireguard_constraints.multihop() {
"enabled"
} else {
"disabled"
@ -679,7 +680,7 @@ impl Relay {
wireguard_constraints.ip_version = ipv;
}
if let Some(use_multihop) = use_multihop {
wireguard_constraints.use_multihop = *use_multihop;
wireguard_constraints.use_multihop(*use_multihop);
}
match entry_location {
Some(EntryArgs::Location(location_args)) => {

View File

@ -1,7 +1,8 @@
use clap::Args;
use mullvad_types::{
constraints::Constraint,
location::{CityCode, CountryCode, Hostname},
relay_constraints::{Constraint, GeographicLocationConstraint, LocationConstraint},
relay_constraints::{GeographicLocationConstraint, LocationConstraint},
};
#[derive(Args, Debug, Clone)]

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Subcommand;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{
relay_constraints::Constraint,
constraints::Constraint,
wireguard::{QuantumResistantState, RotationInterval, DEFAULT_ROTATION_INTERVAL},
};

View File

@ -1,9 +1,8 @@
use crate::{new_selector_config, Daemon, Error, EventListener};
use mullvad_types::{
constraints::Constraint,
custom_list::{CustomList, Id},
relay_constraints::{
BridgeState, Constraint, LocationConstraint, RelaySettings, ResolvedBridgeSettings,
},
relay_constraints::{BridgeState, LocationConstraint, RelaySettings, ResolvedBridgeSettings},
};
use talpid_types::net::TunnelType;
@ -133,7 +132,7 @@ where
{
match endpoint.tunnel_type {
TunnelType::Wireguard => {
if relay_settings.wireguard_constraints.use_multihop {
if relay_settings.wireguard_constraints.multihop() {
if let Constraint::Only(LocationConstraint::CustomList { list_id }) =
&relay_settings.wireguard_constraints.entry_location
{

View File

@ -56,7 +56,7 @@ use mullvad_types::{
version::{AppVersion, AppVersionInfo},
wireguard::{PublicKey, QuantumResistantState, RotationInterval},
};
use relay_list::updater::{self, RelayListUpdater, RelayListUpdaterHandle};
use relay_list::{RelayListUpdater, RelayListUpdaterHandle, RELAYS_FILENAME};
use settings::SettingsPersister;
#[cfg(target_os = "android")]
use std::os::unix::io::RawFd;
@ -698,8 +698,8 @@ where
let initial_selector_config = new_selector_config(&settings);
let relay_selector = RelaySelector::new(
initial_selector_config,
resource_dir.join(updater::RELAYS_FILENAME),
cache_dir.join(updater::RELAYS_FILENAME),
resource_dir.join(RELAYS_FILENAME),
cache_dir.join(RELAYS_FILENAME),
);
let settings_relay_selector = relay_selector.clone();
@ -1105,7 +1105,7 @@ where
// Note that `Constraint::Any` corresponds to just IPv4
matches!(
relay_constraints.wireguard_constraints.ip_version,
mullvad_types::relay_constraints::Constraint::Only(IpVersion::V6)
mullvad_types::constraints::Constraint::Only(IpVersion::V6)
)
} else {
false

View File

@ -1,5 +1,5 @@
use super::Result;
use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion};
use mullvad_types::{constraints::Constraint, settings::SettingsVersion};
use serde::{Deserialize, Serialize};
// ======================================================

View File

@ -1,5 +1,5 @@
use super::{Error, Result};
use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion};
use mullvad_types::{constraints::Constraint, settings::SettingsVersion};
use serde::{Deserialize, Serialize};
// ======================================================

View File

@ -1,5 +1,5 @@
use super::{Error, Result};
use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion};
use mullvad_types::{constraints::Constraint, settings::SettingsVersion};
use serde::{Deserialize, Serialize};
// ======================================================

View File

@ -1,5 +1,5 @@
use super::{Error, Result};
use mullvad_types::{relay_constraints::Constraint, settings::SettingsVersion};
use mullvad_types::{constraints::Constraint, settings::SettingsVersion};
use serde::{Deserialize, Serialize};
// ======================================================

View File

@ -1,3 +1,213 @@
//! Relay list
//! Relay list updater
pub mod updater;
use futures::{
channel::mpsc,
future::{Fuse, FusedFuture},
Future, FutureExt, SinkExt, StreamExt,
};
use std::{
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::fs::File;
use mullvad_api::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle, RelayListProxy};
use mullvad_relay_selector::RelaySelector;
use mullvad_types::relay_list::RelayList;
use talpid_future::retry::{retry_future, ExponentialBackoff, Jittered};
use talpid_types::ErrorExt;
/// How often the updater should wake up to check the cache of the in-memory cache of relays.
/// This check is very cheap. The only reason to not have it very often is because if downloading
/// constantly fails it will try very often and fill the logs etc.
const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 15);
/// How old the cached relays need to be to trigger an update
const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60);
const DOWNLOAD_RETRY_STRATEGY: Jittered<ExponentialBackoff> = Jittered::jitter(
ExponentialBackoff::new(Duration::from_secs(16), 8)
.max_delay(Some(Duration::from_secs(2 * 60 * 60))),
);
/// Where the relay list is cached on disk.
pub(crate) const RELAYS_FILENAME: &str = "relays.json";
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Downloader already shut down")]
DownloaderShutdown,
#[error("Mullvad relay selector error")]
RelaySelector(#[from] mullvad_relay_selector::Error),
}
#[derive(Clone)]
pub struct RelayListUpdaterHandle {
tx: mpsc::Sender<()>,
}
impl RelayListUpdaterHandle {
pub async fn update(&mut self) {
if let Err(error) = self
.tx
.send(())
.await
.map_err(|_| Error::DownloaderShutdown)
{
log::error!(
"{}",
error.display_chain_with_msg("Unable to send update command to relay list updater")
);
}
}
}
pub struct RelayListUpdater {
api_client: RelayListProxy,
cache_path: PathBuf,
relay_selector: RelaySelector,
on_update: Box<dyn Fn(&RelayList) + Send + 'static>,
last_check: SystemTime,
api_availability: ApiAvailabilityHandle,
}
impl RelayListUpdater {
pub fn spawn(
selector: RelaySelector,
api_handle: MullvadRestHandle,
cache_dir: &Path,
on_update: impl Fn(&RelayList) + Send + 'static,
) -> RelayListUpdaterHandle {
let (tx, cmd_rx) = mpsc::channel(1);
let api_availability = api_handle.availability.clone();
let api_client = RelayListProxy::new(api_handle);
let updater = RelayListUpdater {
api_client,
cache_path: cache_dir.join(RELAYS_FILENAME),
relay_selector: selector,
on_update: Box::new(on_update),
last_check: UNIX_EPOCH,
api_availability,
};
tokio::spawn(updater.run(cmd_rx));
RelayListUpdaterHandle { tx }
}
async fn run(mut self, mut cmd_rx: mpsc::Receiver<()>) {
let mut download_future = Box::pin(Fuse::terminated());
loop {
let next_check = tokio::time::sleep(UPDATE_CHECK_INTERVAL).fuse();
tokio::pin!(next_check);
futures::select! {
_check_update = next_check => {
if download_future.is_terminated() && self.should_update() {
let tag = self.relay_selector.etag();
download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse());
self.last_check = SystemTime::now();
}
},
new_relay_list = download_future => {
self.consume_new_relay_list(new_relay_list).await;
},
cmd = cmd_rx.next() => {
match cmd {
Some(()) => {
let tag = self.relay_selector.etag();
download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse());
self.last_check = SystemTime::now();
},
None => {
log::trace!("Relay list updater shutting down");
return;
}
}
}
};
}
}
async fn consume_new_relay_list(
&mut self,
result: Result<Option<RelayList>, mullvad_api::Error>,
) {
match result {
Ok(Some(relay_list)) => {
if let Err(err) = self.update_cache(relay_list).await {
log::error!("Failed to update relay list cache: {}", err);
}
}
Ok(None) => log::debug!("Relay list is up-to-date"),
Err(error) => log::error!(
"{}",
error.display_chain_with_msg("Failed to fetch new relay list")
),
}
}
/// Returns true if the current parsed_relays is older than UPDATE_INTERVAL
fn should_update(&mut self) -> bool {
let last_check = std::cmp::max(self.relay_selector.last_updated(), self.last_check);
match SystemTime::now().duration_since(last_check) {
Ok(duration) => duration >= UPDATE_INTERVAL,
// If the clock is skewed we have no idea by how much or when the last update
// actually was, better download again to get in sync and get a `last_updated`
// timestamp corresponding to the new time.
Err(_) => true,
}
}
fn download_relay_list(
api_handle: ApiAvailabilityHandle,
proxy: RelayListProxy,
tag: Option<String>,
) -> impl Future<Output = Result<Option<RelayList>, mullvad_api::Error>> + 'static {
let download_futures = move || {
let available = api_handle.wait_background();
let req = proxy.relay_list(tag.clone());
async move {
available.await?;
req.await.map_err(mullvad_api::Error::from)
}
};
retry_future(
download_futures,
|result| result.is_err(),
DOWNLOAD_RETRY_STRATEGY,
)
}
async fn update_cache(&mut self, new_relay_list: RelayList) -> Result<(), Error> {
if let Err(error) = Self::cache_relays(&self.cache_path, &new_relay_list).await {
log::error!(
"{}",
error.display_chain_with_msg("Failed to update relay cache on disk")
);
}
self.relay_selector.set_relays(new_relay_list.clone());
(self.on_update)(&new_relay_list);
Ok(())
}
/// Write a `RelayList` to the cache file.
async fn cache_relays(cache_path: &Path, relays: &RelayList) -> Result<(), Error> {
log::debug!("Writing relays cache to {}", cache_path.display());
let mut file = File::create(cache_path)
.await
.map_err(mullvad_relay_selector::Error::OpenRelayCache)?;
let bytes =
serde_json::to_vec_pretty(relays).map_err(mullvad_relay_selector::Error::Serialize)?;
let mut slice: &[u8] = bytes.as_slice();
let _ = tokio::io::copy(&mut slice, &mut file)
.await
.map_err(mullvad_relay_selector::Error::WriteRelayCache)?;
Ok(())
}
}

View File

@ -1,201 +0,0 @@
use futures::{
channel::mpsc,
future::{Fuse, FusedFuture},
Future, FutureExt, SinkExt, StreamExt,
};
use std::{
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::fs::File;
use mullvad_api::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle, RelayListProxy};
use mullvad_relay_selector::{Error, RelaySelector};
use mullvad_types::relay_list::RelayList;
use talpid_future::retry::{retry_future, ExponentialBackoff, Jittered};
use talpid_types::ErrorExt;
/// How often the updater should wake up to check the cache of the in-memory cache of relays.
/// This check is very cheap. The only reason to not have it very often is because if downloading
/// constantly fails it will try very often and fill the logs etc.
const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 15);
/// How old the cached relays need to be to trigger an update
const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60);
const DOWNLOAD_RETRY_STRATEGY: Jittered<ExponentialBackoff> = Jittered::jitter(
ExponentialBackoff::new(Duration::from_secs(16), 8)
.max_delay(Some(Duration::from_secs(2 * 60 * 60))),
);
/// Where the relay list is cached on disk.
pub(crate) const RELAYS_FILENAME: &str = "relays.json";
#[derive(Clone)]
pub struct RelayListUpdaterHandle {
tx: mpsc::Sender<()>,
}
impl RelayListUpdaterHandle {
pub async fn update(&mut self) {
if let Err(error) = self
.tx
.send(())
.await
.map_err(|_| Error::DownloaderShutDown)
{
log::error!(
"{}",
error.display_chain_with_msg("Unable to send update command to relay list updater")
);
}
}
}
pub struct RelayListUpdater {
api_client: RelayListProxy,
cache_path: PathBuf,
relay_selector: RelaySelector,
on_update: Box<dyn Fn(&RelayList) + Send + 'static>,
last_check: SystemTime,
api_availability: ApiAvailabilityHandle,
}
impl RelayListUpdater {
pub fn spawn(
selector: RelaySelector,
api_handle: MullvadRestHandle,
cache_dir: &Path,
on_update: impl Fn(&RelayList) + Send + 'static,
) -> RelayListUpdaterHandle {
let (tx, cmd_rx) = mpsc::channel(1);
let api_availability = api_handle.availability.clone();
let api_client = RelayListProxy::new(api_handle);
let updater = RelayListUpdater {
api_client,
cache_path: cache_dir.join(RELAYS_FILENAME),
relay_selector: selector,
on_update: Box::new(on_update),
last_check: UNIX_EPOCH,
api_availability,
};
tokio::spawn(updater.run(cmd_rx));
RelayListUpdaterHandle { tx }
}
async fn run(mut self, mut cmd_rx: mpsc::Receiver<()>) {
let mut download_future = Box::pin(Fuse::terminated());
loop {
let next_check = tokio::time::sleep(UPDATE_CHECK_INTERVAL).fuse();
tokio::pin!(next_check);
futures::select! {
_check_update = next_check => {
if download_future.is_terminated() && self.should_update() {
let tag = self.relay_selector.etag();
download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse());
self.last_check = SystemTime::now();
}
},
new_relay_list = download_future => {
self.consume_new_relay_list(new_relay_list).await;
},
cmd = cmd_rx.next() => {
match cmd {
Some(()) => {
let tag = self.relay_selector.etag();
download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse());
self.last_check = SystemTime::now();
},
None => {
log::trace!("Relay list updater shutting down");
return;
}
}
}
};
}
}
async fn consume_new_relay_list(
&mut self,
result: Result<Option<RelayList>, mullvad_api::Error>,
) {
match result {
Ok(Some(relay_list)) => {
if let Err(err) = self.update_cache(relay_list).await {
log::error!("Failed to update relay list cache: {}", err);
}
}
Ok(None) => log::debug!("Relay list is up-to-date"),
Err(error) => log::error!(
"{}",
error.display_chain_with_msg("Failed to fetch new relay list")
),
}
}
/// Returns true if the current parsed_relays is older than UPDATE_INTERVAL
fn should_update(&mut self) -> bool {
let last_check = std::cmp::max(self.relay_selector.last_updated(), self.last_check);
match SystemTime::now().duration_since(last_check) {
Ok(duration) => duration >= UPDATE_INTERVAL,
// If the clock is skewed we have no idea by how much or when the last update
// actually was, better download again to get in sync and get a `last_updated`
// timestamp corresponding to the new time.
Err(_) => true,
}
}
fn download_relay_list(
api_handle: ApiAvailabilityHandle,
proxy: RelayListProxy,
tag: Option<String>,
) -> impl Future<Output = Result<Option<RelayList>, mullvad_api::Error>> + 'static {
let download_futures = move || {
let available = api_handle.wait_background();
let req = proxy.relay_list(tag.clone());
async move {
available.await?;
req.await.map_err(mullvad_api::Error::from)
}
};
retry_future(
download_futures,
|result| result.is_err(),
DOWNLOAD_RETRY_STRATEGY,
)
}
async fn update_cache(&mut self, new_relay_list: RelayList) -> Result<(), Error> {
if let Err(error) = Self::cache_relays(&self.cache_path, &new_relay_list).await {
log::error!(
"{}",
error.display_chain_with_msg("Failed to update relay cache on disk")
);
}
self.relay_selector.set_relays(new_relay_list.clone());
(self.on_update)(&new_relay_list);
Ok(())
}
/// Write a `RelayList` to the cache file.
async fn cache_relays(cache_path: &Path, relays: &RelayList) -> Result<(), Error> {
log::debug!("Writing relays cache to {}", cache_path.display());
let mut file = File::create(cache_path)
.await
.map_err(Error::OpenRelayCache)?;
let bytes = serde_json::to_vec_pretty(relays).map_err(Error::Serialize)?;
let mut slice: &[u8] = bytes.as_slice();
let _ = tokio::io::copy(&mut slice, &mut file)
.await
.map_err(Error::WriteRelayCache)?;
Ok(())
}
}

View File

@ -376,16 +376,13 @@ impl<'a> Display for SettingsSummary<'a> {
write!(f, ", wg ip version: {ip_version}")?;
}
let multihop = matches!(
relay_settings,
let multihop = match relay_settings {
RelaySettings::Normal(RelayConstraints {
wireguard_constraints: WireguardConstraints {
use_multihop: true,
wireguard_constraints,
..
},
..
})
);
}) => wireguard_constraints.multihop(),
_ => false,
};
write!(
f,

View File

@ -8,20 +8,22 @@ use std::{
use tokio::sync::Mutex;
use mullvad_relay_selector::{RelaySelector, SelectedBridge, SelectedObfuscator, SelectedRelay};
use mullvad_relay_selector::{GetRelay, RelaySelector, RuntimeParameters, WireguardConfig};
use mullvad_types::{
endpoint::MullvadEndpoint, location::GeoIpLocation, relay_list::Relay, settings::TunnelOptions,
endpoint::MullvadWireguardEndpoint, location::GeoIpLocation, relay_list::Relay,
settings::TunnelOptions,
};
use once_cell::sync::Lazy;
use talpid_core::tunnel_state_machine::TunnelParametersGenerator;
use talpid_types::{
net::{wireguard, TunnelParameters},
tunnel::ParameterGenerationError,
ErrorExt,
};
#[cfg(not(target_os = "android"))]
use talpid_types::net::openvpn;
use talpid_types::net::{
obfuscation::ObfuscatorConfig, openvpn, proxy::CustomProxy, wireguard, Endpoint,
TunnelParameters,
};
#[cfg(target_os = "android")]
use talpid_types::net::{obfuscation::ObfuscatorConfig, wireguard, TunnelParameters};
use talpid_types::{tunnel::ParameterGenerationError, ErrorExt};
use crate::device::{AccountManagerHandle, PrivateAccountAndDevice};
@ -138,10 +140,58 @@ impl ParametersGenerator {
}
impl InnerParametersGenerator {
async fn generate(&mut self, retry_attempt: u32) -> Result<TunnelParameters, Error> {
let _data = self.device().await?;
match self.relay_selector.get_relay(retry_attempt) {
Ok((SelectedRelay::Custom(custom_relay), _bridge, _obfsucator)) => {
async fn generate(
&mut self,
retry_attempt: u32,
ipv6: bool,
) -> Result<TunnelParameters, Error> {
let data = self.device().await?;
let selected_relay = self
.relay_selector
.get_relay(retry_attempt as usize, RuntimeParameters { ipv6 })
.map_err(|err| match err {
mullvad_relay_selector::Error::NoBridge => Error::NoBridgeAvailable,
_ => Error::NoRelayAvailable,
})?;
match selected_relay {
#[cfg(not(target_os = "android"))]
GetRelay::OpenVpn {
endpoint,
exit,
bridge,
} => {
let bridge_relay = bridge.as_ref().and_then(|bridge| bridge.relay());
self.last_generated_relays = Some(LastSelectedRelays::OpenVpn {
relay: exit.clone(),
bridge: bridge_relay.cloned(),
});
let bridge_settings = bridge.as_ref().map(|bridge| bridge.settings());
Ok(self.create_openvpn_tunnel_parameters(endpoint, data, bridge_settings.cloned()))
}
GetRelay::Wireguard {
endpoint,
obfuscator,
inner,
} => {
let (obfuscator_relay, obfuscator_config) = match obfuscator {
Some(obfuscator) => (Some(obfuscator.relay), Some(obfuscator.config)),
None => (None, None),
};
let (wg_entry, wg_exit) = match inner {
WireguardConfig::Singlehop { exit } => (None, exit),
WireguardConfig::Multihop { exit, entry } => (Some(entry), exit),
};
self.last_generated_relays = Some(LastSelectedRelays::WireGuard {
wg_entry,
wg_exit,
obfuscator: obfuscator_relay,
});
Ok(self.create_wireguard_tunnel_parameters(endpoint, data, obfuscator_config))
}
GetRelay::Custom(custom_relay) => {
self.last_generated_relays = None;
custom_relay
// TODO: generate proxy settings for custom tunnels
@ -151,66 +201,33 @@ impl InnerParametersGenerator {
Error::ResolveCustomHostname
})
}
Ok((SelectedRelay::Normal(constraints), bridge, obfuscator)) => {
self.create_tunnel_parameters(
&constraints.exit_relay,
&constraints.entry_relay,
constraints.endpoint,
bridge,
obfuscator,
)
.await
}
Err(mullvad_relay_selector::Error::NoBridge) => Err(Error::NoBridgeAvailable),
Err(_error) => Err(Error::NoRelayAvailable),
}
}
#[cfg_attr(target_os = "android", allow(unused_variables))]
async fn create_tunnel_parameters(
&mut self,
relay: &Relay,
entry_relay: &Option<Relay>,
endpoint: MullvadEndpoint,
bridge: Option<SelectedBridge>,
obfuscator: Option<SelectedObfuscator>,
) -> Result<TunnelParameters, Error> {
let data = self.device().await?;
match endpoint {
#[cfg(not(target_os = "android"))]
MullvadEndpoint::OpenVpn(endpoint) => {
let (bridge_settings, bridge_relay) = match bridge {
Some(SelectedBridge::Normal(bridge)) => {
(Some(bridge.settings), Some(bridge.relay))
}
Some(SelectedBridge::Custom(settings)) => (Some(settings), None),
None => (None, None),
};
self.last_generated_relays = Some(LastSelectedRelays::OpenVpn {
relay: relay.clone(),
bridge: bridge_relay,
});
Ok(openvpn::TunnelParameters {
config: openvpn::ConnectionConfig::new(
endpoint,
data.account_token,
"-".to_string(),
),
fn create_openvpn_tunnel_parameters(
&self,
endpoint: Endpoint,
data: PrivateAccountAndDevice,
bridge_settings: Option<CustomProxy>,
) -> TunnelParameters {
openvpn::TunnelParameters {
config: openvpn::ConnectionConfig::new(endpoint, data.account_token, "-".to_string()),
options: self.tunnel_options.openvpn.clone(),
generic_options: self.tunnel_options.generic.clone(),
proxy: bridge_settings,
#[cfg(target_os = "linux")]
fwmark: mullvad_types::TUNNEL_FWMARK,
}
.into())
.into()
}
#[cfg(target_os = "android")]
MullvadEndpoint::OpenVpn(endpoint) => {
unreachable!("OpenVPN is not supported on Android");
}
MullvadEndpoint::Wireguard(endpoint) => {
fn create_wireguard_tunnel_parameters(
&self,
endpoint: MullvadWireguardEndpoint,
data: PrivateAccountAndDevice,
obfuscator_config: Option<ObfuscatorConfig>,
) -> TunnelParameters {
let tunnel_ipv4 = data.device.wg_data.addresses.ipv4_address.ip();
let tunnel_ipv6 = data.device.wg_data.addresses.ipv6_address.ip();
let tunnel = wireguard::TunnelConfig {
@ -225,18 +242,7 @@ impl InnerParametersGenerator {
log::debug!("Same IP is NOT being used");
}
let (obfuscator_relay, obfuscator_config) = match obfuscator {
Some(obfuscator) => (Some(obfuscator.relay), Some(obfuscator.config)),
None => (None, None),
};
self.last_generated_relays = Some(LastSelectedRelays::WireGuard {
wg_entry: entry_relay.clone(),
wg_exit: relay.clone(),
obfuscator: obfuscator_relay,
});
Ok(wireguard::TunnelParameters {
wireguard::TunnelParameters {
connection: wireguard::ConnectionConfig {
tunnel,
peer: endpoint.peer,
@ -254,9 +260,7 @@ impl InnerParametersGenerator {
generic_options: self.tunnel_options.generic.clone(),
obfuscation: obfuscator_config,
}
.into())
}
}
.into()
}
async fn device(&self) -> Result<PrivateAccountAndDevice, Error> {
@ -274,12 +278,13 @@ impl TunnelParametersGenerator for ParametersGenerator {
fn generate(
&mut self,
retry_attempt: u32,
ipv6: bool,
) -> Pin<Box<dyn Future<Output = Result<TunnelParameters, ParameterGenerationError>>>> {
let generator = self.0.clone();
Box::pin(async move {
let mut inner = generator.lock().await;
inner
.generate(retry_attempt)
.generate(retry_attempt, ipv6)
.await
.map_err(|error| match error {
Error::NoBridgeAvailable => ParameterGenerationError::NoMatchingBridgeRelay,

View File

@ -1,7 +1,6 @@
use crate::types::{conversions::net::try_tunnel_type_from_i32, proto, FromProtobufTypeError};
use mullvad_types::{
custom_list::Id,
relay_constraints::{Constraint, GeographicLocationConstraint},
constraints::Constraint, custom_list::Id, relay_constraints::GeographicLocationConstraint,
};
use std::str::FromStr;
use talpid_types::net::proxy::CustomProxy;
@ -254,7 +253,7 @@ impl From<mullvad_types::relay_constraints::RelaySettings> for proto::RelaySetti
.ip_version
.option()
.map(|ipv| i32::from(proto::IpVersion::from(ipv))),
use_multihop: constraints.wireguard_constraints.use_multihop,
use_multihop: constraints.wireguard_constraints.multihop(),
entry_location: constraints
.wireguard_constraints
.entry_location

View File

@ -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 }

View File

@ -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];

View File

@ -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))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}

View File

@ -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
}
},
}
}

View File

@ -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(),
}
}

View File

@ -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))
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,165 @@
//! General constraints.
#[cfg(target_os = "android")]
use jnix::{FromJava, IntoJava};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
/// Limits the set of [`crate::relay_list::Relay`]s that a `RelaySelector` may select.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(target_os = "android", derive(FromJava, IntoJava))]
#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))]
#[cfg_attr(target_os = "android", jnix(bounds = "T: android.os.Parcelable"))]
pub enum Constraint<T> {
Any,
Only(T),
}
impl<T: fmt::Display> fmt::Display for Constraint<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Constraint::Any => "any".fmt(f),
Constraint::Only(value) => fmt::Display::fmt(value, f),
}
}
}
impl<T> Constraint<T> {
pub fn unwrap(self) -> T {
match self {
Constraint::Any => panic!("called `Constraint::unwrap()` on an `Any` value"),
Constraint::Only(value) => value,
}
}
pub fn unwrap_or(self, other: T) -> T {
match self {
Constraint::Any => other,
Constraint::Only(value) => value,
}
}
pub fn or(self, other: Constraint<T>) -> Constraint<T> {
match self {
Constraint::Any => other,
Constraint::Only(value) => Constraint::Only(value),
}
}
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Constraint<U> {
match self {
Constraint::Any => Constraint::Any,
Constraint::Only(value) => Constraint::Only(f(value)),
}
}
pub const fn is_any(&self) -> bool {
match self {
Constraint::Any => true,
Constraint::Only(_value) => false,
}
}
pub const fn is_only(&self) -> bool {
!self.is_any()
}
pub const fn as_ref(&self) -> Constraint<&T> {
match self {
Constraint::Any => Constraint::Any,
Constraint::Only(ref value) => Constraint::Only(value),
}
}
pub fn option(self) -> Option<T> {
match self {
Constraint::Any => None,
Constraint::Only(value) => Some(value),
}
}
/// Returns true if the constraint is an `Only` and the value inside of it matches a predicate.
pub fn is_only_and(self, f: impl FnOnce(T) -> bool) -> bool {
self.option().is_some_and(f)
}
}
impl<T: PartialEq> Constraint<T> {
pub fn matches_eq(&self, other: &T) -> bool {
match self {
Constraint::Any => true,
Constraint::Only(ref value) => value == other,
}
}
}
// Using the default attribute fails on Android
#[allow(clippy::derivable_impls)]
impl<T> Default for Constraint<T> {
fn default() -> Self {
Constraint::Any
}
}
impl<T> From<Option<T>> for Constraint<T> {
fn from(value: Option<T>) -> Self {
match value {
Some(value) => Constraint::Only(value),
None => Constraint::Any,
}
}
}
impl<T: Copy> Copy for Constraint<T> {}
impl<T: fmt::Debug + Clone + FromStr> FromStr for Constraint<T> {
type Err = T::Err;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.eq_ignore_ascii_case("any") {
return Ok(Self::Any);
}
Ok(Self::Only(T::from_str(value)?))
}
}
// Clap
#[cfg(feature = "clap")]
#[derive(fmt::Debug, Clone)]
pub struct ConstraintParser<T>(T);
#[cfg(feature = "clap")]
impl<T: fmt::Debug + Clone + clap::builder::ValueParserFactory> clap::builder::ValueParserFactory
for Constraint<T>
where
<T as clap::builder::ValueParserFactory>::Parser: Sync + Send + Clone,
{
type Parser = ConstraintParser<T::Parser>;
fn value_parser() -> Self::Parser {
ConstraintParser(T::value_parser())
}
}
#[cfg(feature = "clap")]
impl<T: clap::builder::TypedValueParser> clap::builder::TypedValueParser for ConstraintParser<T>
where
T::Value: fmt::Debug,
{
type Value = Constraint<T::Value>;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
if value.eq_ignore_ascii_case("any") {
return Ok(Constraint::Any);
}
self.0.parse_ref(cmd, arg, value).map(Constraint::Only)
}
}

View File

@ -0,0 +1,35 @@
//! Constrain yourself.
mod constraint;
// Re-export bits & pieces from `constraints.rs` as needed
pub use constraint::Constraint;
/// A limited variant of Sets.
pub trait Set<T> {
fn is_subset(&self, other: &T) -> bool;
}
pub trait Match<T> {
fn matches(&self, other: &T) -> bool;
}
impl<T: Match<U>, U> Match<U> for Constraint<T> {
fn matches(&self, other: &U) -> bool {
match *self {
Constraint::Any => true,
Constraint::Only(ref value) => value.matches(other),
}
}
}
impl<T: Set<U>, U> Set<Constraint<U>> for Constraint<T> {
fn is_subset(&self, other: &Constraint<U>) -> bool {
match self {
Constraint::Any => other.is_any(),
Constraint::Only(ref constraint) => match other {
Constraint::Only(ref other_constraint) => constraint.is_subset(other_constraint),
_ => true,
},
}
}
}

View File

@ -29,13 +29,4 @@ impl MullvadEndpoint {
),
}
}
pub fn unwrap_wireguard(&self) -> &MullvadWireguardEndpoint {
match self {
Self::Wireguard(endpoint) => endpoint,
other => {
panic!("Expected WireGuard enum variant but got {other:?}");
}
}
}
}

View File

@ -1,6 +1,7 @@
pub mod access_method;
pub mod account;
pub mod auth_failed;
pub mod constraints;
pub mod custom_list;
pub mod device;
pub mod endpoint;

View File

@ -2,6 +2,7 @@
//! updated as well.
use crate::{
constraints::{Constraint, Match, Set},
custom_list::{CustomListsSettings, Id},
location::{CityCode, CountryCode, Hostname},
relay_list::Relay,
@ -18,186 +19,6 @@ use std::{
};
use talpid_types::net::{proxy::CustomProxy, IpVersion, TransportProtocol, TunnelType};
pub trait Match<T> {
fn matches(&self, other: &T) -> bool;
}
pub trait Set<T> {
fn is_subset(&self, other: &T) -> bool;
}
/// Limits the set of [`crate::relay_list::Relay`]s that a `RelaySelector` may select.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(target_os = "android", derive(FromJava, IntoJava))]
#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))]
#[cfg_attr(target_os = "android", jnix(bounds = "T: android.os.Parcelable"))]
pub enum Constraint<T> {
Any,
Only(T),
}
impl<T: fmt::Display> fmt::Display for Constraint<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Constraint::Any => "any".fmt(f),
Constraint::Only(value) => fmt::Display::fmt(value, f),
}
}
}
impl<T> Constraint<T> {
pub fn unwrap(self) -> T {
match self {
Constraint::Any => panic!("called `Constraint::unwrap()` on an `Any` value"),
Constraint::Only(value) => value,
}
}
pub fn unwrap_or(self, other: T) -> T {
match self {
Constraint::Any => other,
Constraint::Only(value) => value,
}
}
pub fn or(self, other: Constraint<T>) -> Constraint<T> {
match self {
Constraint::Any => other,
Constraint::Only(value) => Constraint::Only(value),
}
}
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Constraint<U> {
match self {
Constraint::Any => Constraint::Any,
Constraint::Only(value) => Constraint::Only(f(value)),
}
}
pub fn is_any(&self) -> bool {
match self {
Constraint::Any => true,
Constraint::Only(_value) => false,
}
}
pub fn is_only(&self) -> bool {
!self.is_any()
}
pub fn as_ref(&self) -> Constraint<&T> {
match self {
Constraint::Any => Constraint::Any,
Constraint::Only(ref value) => Constraint::Only(value),
}
}
pub fn option(self) -> Option<T> {
match self {
Constraint::Any => None,
Constraint::Only(value) => Some(value),
}
}
}
impl<T: PartialEq> Constraint<T> {
pub fn matches_eq(&self, other: &T) -> bool {
match self {
Constraint::Any => true,
Constraint::Only(ref value) => value == other,
}
}
}
// Using the default attribute fails on Android
#[allow(clippy::derivable_impls)]
impl<T> Default for Constraint<T> {
fn default() -> Self {
Constraint::Any
}
}
impl<T: Copy> Copy for Constraint<T> {}
impl<T: Match<U>, U> Match<U> for Constraint<T> {
fn matches(&self, other: &U) -> bool {
match *self {
Constraint::Any => true,
Constraint::Only(ref value) => value.matches(other),
}
}
}
impl<T: Set<U>, U> Set<Constraint<U>> for Constraint<T> {
fn is_subset(&self, other: &Constraint<U>) -> bool {
match self {
Constraint::Any => other.is_any(),
Constraint::Only(ref constraint) => match other {
Constraint::Only(ref other_constraint) => constraint.is_subset(other_constraint),
_ => true,
},
}
}
}
impl<T> From<Option<T>> for Constraint<T> {
fn from(value: Option<T>) -> Self {
match value {
Some(value) => Constraint::Only(value),
None => Constraint::Any,
}
}
}
impl<T: fmt::Debug + Clone + FromStr> FromStr for Constraint<T> {
type Err = T::Err;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.eq_ignore_ascii_case("any") {
return Ok(Self::Any);
}
Ok(Self::Only(T::from_str(value)?))
}
}
#[cfg(feature = "clap")]
impl<T: fmt::Debug + Clone + clap::builder::ValueParserFactory> clap::builder::ValueParserFactory
for Constraint<T>
where
<T as clap::builder::ValueParserFactory>::Parser: Sync + Send + Clone,
{
type Parser = ConstraintParser<T::Parser>;
fn value_parser() -> Self::Parser {
ConstraintParser(T::value_parser())
}
}
#[cfg(feature = "clap")]
#[derive(fmt::Debug, Clone)]
pub struct ConstraintParser<T>(T);
#[cfg(feature = "clap")]
impl<T: clap::builder::TypedValueParser> clap::builder::TypedValueParser for ConstraintParser<T>
where
T::Value: fmt::Debug,
{
type Value = Constraint<T::Value>;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
if value.eq_ignore_ascii_case("any") {
return Ok(Constraint::Any);
}
self.0.parse_ref(cmd, arg, value).map(Constraint::Only)
}
}
/// Specifies a specific endpoint or [`RelayConstraints`] to use when `mullvad-daemon` selects a
/// relay.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
@ -264,36 +85,9 @@ pub enum LocationConstraint {
CustomList { list_id: Id },
}
#[derive(Debug, Clone)]
pub enum ResolvedLocationConstraint {
Location(GeographicLocationConstraint),
Locations(Vec<GeographicLocationConstraint>),
}
impl ResolvedLocationConstraint {
pub fn from_constraint(
location: Constraint<LocationConstraint>,
custom_lists: &CustomListsSettings,
) -> Constraint<ResolvedLocationConstraint> {
match location {
Constraint::Any => Constraint::Any,
Constraint::Only(LocationConstraint::Location(location)) => {
Constraint::Only(Self::Location(location))
}
Constraint::Only(LocationConstraint::CustomList { list_id }) => custom_lists
.iter()
.find(|list| list.id == list_id)
.map(|custom_list| {
Constraint::Only(Self::Locations(
custom_list.locations.iter().cloned().collect(),
))
})
.unwrap_or_else(|| {
log::warn!("Resolved non-existent custom list");
Constraint::Only(ResolvedLocationConstraint::Locations(vec![]))
}),
}
}
pub struct LocationConstraintFormatter<'a> {
pub constraint: &'a LocationConstraint,
pub custom_lists: &'a CustomListsSettings,
}
impl From<GeographicLocationConstraint> for LocationConstraint {
@ -302,61 +96,6 @@ impl From<GeographicLocationConstraint> for LocationConstraint {
}
}
impl Set<Constraint<ResolvedLocationConstraint>> for Constraint<ResolvedLocationConstraint> {
fn is_subset(&self, other: &Self) -> bool {
match self {
Constraint::Any => other.is_any(),
Constraint::Only(ResolvedLocationConstraint::Location(location)) => match other {
Constraint::Any => true,
Constraint::Only(ResolvedLocationConstraint::Location(other_location)) => {
location.is_subset(other_location)
}
Constraint::Only(ResolvedLocationConstraint::Locations(other_locations)) => {
other_locations
.iter()
.any(|other_location| location.is_subset(other_location))
}
},
Constraint::Only(ResolvedLocationConstraint::Locations(locations)) => match other {
Constraint::Any => true,
Constraint::Only(ResolvedLocationConstraint::Location(other_location)) => locations
.iter()
.all(|location| location.is_subset(other_location)),
Constraint::Only(ResolvedLocationConstraint::Locations(other_locations)) => {
for location in locations {
if !other_locations
.iter()
.any(|other_location| location.is_subset(other_location))
{
return false;
}
}
true
}
},
}
}
}
impl Constraint<ResolvedLocationConstraint> {
pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool {
match self {
Constraint::Any => true,
Constraint::Only(ResolvedLocationConstraint::Location(location)) => {
location.matches_with_opts(relay, ignore_include_in_country)
}
Constraint::Only(ResolvedLocationConstraint::Locations(locations)) => locations
.iter()
.any(|loc| loc.matches_with_opts(relay, ignore_include_in_country)),
}
}
}
pub struct LocationConstraintFormatter<'a> {
pub constraint: &'a LocationConstraint,
pub custom_lists: &'a CustomListsSettings,
}
impl<'a> fmt::Display for LocationConstraintFormatter<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.constraint {
@ -507,15 +246,38 @@ pub enum GeographicLocationConstraint {
}
impl GeographicLocationConstraint {
pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool {
/// Create a new [`GeographicLocationConstraint`] given a country.
pub fn country(country: impl Into<String>) -> Self {
GeographicLocationConstraint::Country(country.into())
}
/// Create a new [`GeographicLocationConstraint`] given a country and city.
pub fn city(country: impl Into<String>, city: impl Into<String>) -> Self {
GeographicLocationConstraint::City(country.into(), city.into())
}
/// Create a new [`GeographicLocationConstraint`] given a country, city and hostname.
pub fn hostname(
country: impl Into<String>,
city: impl Into<String>,
hostname: impl Into<String>,
) -> Self {
GeographicLocationConstraint::Hostname(country.into(), city.into(), hostname.into())
}
// TODO(markus): Document
pub fn is_country(&self) -> bool {
matches!(self, GeographicLocationConstraint::Country(_))
}
}
impl Match<Relay> for GeographicLocationConstraint {
fn matches(&self, relay: &Relay) -> bool {
match self {
GeographicLocationConstraint::Country(ref country) => {
relay
GeographicLocationConstraint::Country(ref country) => relay
.location
.as_ref()
.map_or(false, |loc| loc.country_code == *country)
&& (ignore_include_in_country || relay.include_in_country)
}
.map_or(false, |loc| loc.country_code == *country),
GeographicLocationConstraint::City(ref country, ref city) => {
relay.location.as_ref().map_or(false, |loc| {
loc.country_code == *country && loc.city_code == *city
@ -532,34 +294,6 @@ impl GeographicLocationConstraint {
}
}
impl Constraint<Vec<GeographicLocationConstraint>> {
pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool {
match self {
Constraint::Only(constraint) => constraint
.iter()
.any(|loc| loc.matches_with_opts(relay, ignore_include_in_country)),
Constraint::Any => true,
}
}
}
impl Constraint<GeographicLocationConstraint> {
pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool {
match self {
Constraint::Only(constraint) => {
constraint.matches_with_opts(relay, ignore_include_in_country)
}
Constraint::Any => true,
}
}
}
impl Match<Relay> for GeographicLocationConstraint {
fn matches(&self, relay: &Relay) -> bool {
self.matches_with_opts(relay, false)
}
}
impl Set<GeographicLocationConstraint> for GeographicLocationConstraint {
/// Returns whether `self` is equal to or a subset of `other`.
fn is_subset(&self, other: &Self) -> bool {
@ -667,9 +401,11 @@ pub struct Providers {
pub struct NoProviders(());
impl Providers {
pub fn new(providers: impl Iterator<Item = Provider>) -> Result<Providers, NoProviders> {
pub fn new(
providers: impl IntoIterator<Item = impl Into<Provider>>,
) -> Result<Providers, NoProviders> {
let providers = Providers {
providers: providers.collect(),
providers: providers.into_iter().map(Into::into).collect(),
};
if providers.providers.is_empty() {
return Err(NoProviders(()));
@ -680,6 +416,11 @@ impl Providers {
pub fn into_vec(self) -> Vec<Provider> {
self.providers.into_iter().collect()
}
/// Access the underlying set of [providers][`Provider`]
pub fn providers(&self) -> &HashSet<Provider> {
&self.providers
}
}
impl Match<Relay> for Providers {
@ -768,6 +509,18 @@ pub struct WireguardConstraints {
pub entry_location: Constraint<LocationConstraint>,
}
impl WireguardConstraints {
/// Enable or disable multihop.
pub fn use_multihop(&mut self, multihop: bool) {
self.use_multihop = multihop
}
/// Check if multihop is enabled.
pub fn multihop(&self) -> bool {
self.use_multihop
}
}
pub struct WireguardConstraintsFormatter<'a> {
pub constraints: &'a WireguardConstraints,
pub custom_lists: &'a CustomListsSettings,
@ -782,7 +535,7 @@ impl<'a> fmt::Display for WireguardConstraintsFormatter<'a> {
if let Constraint::Only(ip_version) = self.constraints.ip_version {
write!(f, ", {},", ip_version)?;
}
if self.constraints.use_multihop {
if self.constraints.multihop() {
let location = self.constraints.entry_location.as_ref().map(|location| {
LocationConstraintFormatter {
constraint: location,

View File

@ -106,6 +106,57 @@ pub struct Relay {
pub location: Option<Location>,
}
impl PartialEq for Relay {
/// Hostnames are assumed to be unique per relay, i.e. a relay can be uniquely identified by its hostname.
///
/// # Example
///
/// ```rust
/// # use mullvad_types::{relay_list::Relay, relay_list::{RelayEndpointData, WireguardRelayEndpointData}};
/// # use talpid_types::net::wireguard::PublicKey;
///
/// let relay = Relay {
/// hostname: "se9-wireguard".to_string(),
/// ipv4_addr_in: "185.213.154.68".parse().unwrap(),
/// # ipv6_addr_in: None,
/// # include_in_country: true,
/// # active: true,
/// # owned: true,
/// # provider: "provider0".to_string(),
/// # weight: 1,
/// # endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
/// # public_key: PublicKey::from_base64(
/// # "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
/// # )
/// # .unwrap(),
/// # }),
/// # location: None,
/// };
///
/// let mut different_relay = relay.clone();
/// // Modify the relay's IPv4 address - should not matter for the equality check
/// different_relay.ipv4_addr_in = "1.3.3.7".parse().unwrap();
/// assert_eq!(relay, different_relay);
///
/// // What matter's for the equality check is the hostname of the relay
/// different_relay.hostname = "dk-cph-wg-001".to_string();
/// assert_ne!(relay, different_relay);
/// ```
fn eq(&self, other: &Self) -> bool {
self.hostname == other.hostname
}
}
/// Hostnames are assumed to be unique per relay, i.e. a relay can be uniquely identified by its hostname.
impl Eq for Relay {}
/// Hostnames are assumed to be unique per relay, i.e. a relay can be uniquely identified by its hostname.
impl std::hash::Hash for Relay {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.hostname.hash(state)
}
}
/// Specifies the type of a relay or relay-specific endpoint data.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]

View File

@ -1,8 +1,9 @@
use crate::{
access_method,
constraints::Constraint,
custom_list::CustomListsSettings,
relay_constraints::{
BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint, LocationConstraint,
BridgeSettings, BridgeState, GeographicLocationConstraint, LocationConstraint,
ObfuscationSettings, RelayConstraints, RelayOverride, RelaySettings,
RelaySettingsFormatter, SelectedObfuscation, WireguardConstraints,
},

View File

@ -69,7 +69,7 @@ impl ConnectingState {
match shared_values.runtime.block_on(
shared_values
.tunnel_parameters_generator
.generate(retry_attempt),
.generate(retry_attempt, shared_values.connectivity.has_ipv6()),
) {
Err(err) => {
ErrorState::enter(shared_values, ErrorStateCause::TunnelParameterError(err))

View File

@ -417,6 +417,7 @@ pub trait TunnelParametersGenerator: Send + 'static {
fn generate(
&mut self,
retry_attempt: u32,
ipv6: bool,
) -> Pin<Box<dyn Future<Output = Result<TunnelParameters, ParameterGenerationError>>>>;
}

View File

@ -15,5 +15,5 @@ rand = "0.8.5"
talpid-time = { path = "../talpid-time" }
[dev-dependencies]
proptest = "1.4"
proptest = { workspace = true }
tokio = { workspace = true, features = [ "test-util", "macros" ] }

View File

@ -555,6 +555,22 @@ impl Connectivity {
)
}
/// Whether IPv6 connectivity seems to be available on the host.
///
/// If IPv6 status is unknown, `false` is returned.
#[cfg(not(target_os = "android"))]
pub fn has_ipv6(&self) -> bool {
matches!(self, Connectivity::Status { ipv6: true, .. })
}
/// Whether IPv6 connectivity seems to be available on the host.
///
/// If IPv6 status is unknown, `false` is returned.
#[cfg(target_os = "android")]
pub fn has_ipv6(&self) -> bool {
self.is_online()
}
/// If the host does not have configured IPv6 routes, we have no way of
/// reaching the internet so we consider ourselves offline.
#[cfg(target_os = "android")]

View File

@ -81,5 +81,4 @@ features = [
]
[dev-dependencies]
proptest = "1.4"
tokio = { workspace = true, features = ["time", "test-util"] }
proptest = { workspace = true }

11
test/Cargo.lock generated
View File

@ -1455,6 +1455,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
@ -1806,8 +1815,10 @@ version = "0.0.0"
dependencies = [
"chrono",
"ipnetwork 0.16.0",
"itertools 0.12.1",
"log",
"mullvad-types",
"once_cell",
"rand 0.8.5",
"serde_json",
"talpid-types",

View File

@ -5,9 +5,10 @@ use crate::network_monitor::{
use futures::StreamExt;
use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient};
use mullvad_types::{
constraints::Constraint,
location::Location,
relay_constraints::{
BridgeSettings, Constraint, GeographicLocationConstraint, LocationConstraint, RelaySettings,
BridgeSettings, GeographicLocationConstraint, LocationConstraint, RelaySettings,
},
relay_list::{Relay, RelayList},
states::TunnelState,

View File

@ -5,7 +5,7 @@ use super::helpers::{
use super::{Error, TestContext};
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::relay_constraints;
use mullvad_types::{constraints::Constraint, relay_constraints};
use test_macro::test_function;
use test_rpc::meta::Os;
use test_rpc::{mullvad_daemon::ServiceStatus, ServiceClient};
@ -141,7 +141,7 @@ pub async fn test_upgrade_app(ctx: TestContext, rpc: ServiceClient) -> Result<()
let relay_location_was_preserved = match &settings.relay_settings {
relay_constraints::RelaySettings::Normal(relay_constraints::RelayConstraints {
location:
relay_constraints::Constraint::Only(relay_constraints::LocationConstraint::Location(
Constraint::Only(relay_constraints::LocationConstraint::Location(
relay_constraints::GeographicLocationConstraint::Country(country),
)),
..

View File

@ -6,12 +6,16 @@ use crate::network_monitor::{start_packet_monitor, MonitorOptions};
use crate::tests::helpers::login_with_retries;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::relay_constraints::{
self, BridgeConstraints, BridgeSettings, BridgeType, Constraint, OpenVpnConstraints,
RelayConstraints, RelaySettings, SelectedObfuscation, TransportPort,
Udp2TcpObfuscationSettings, WireguardConstraints,
use mullvad_relay_selector::query::builder::RelayQueryBuilder;
use mullvad_types::{
constraints::Constraint,
relay_constraints::{
self, BridgeConstraints, BridgeSettings, BridgeType, OpenVpnConstraints, RelayConstraints,
RelaySettings, SelectedObfuscation, TransportPort, Udp2TcpObfuscationSettings,
WireguardConstraints,
},
wireguard,
};
use mullvad_types::wireguard;
use std::net::SocketAddr;
use talpid_types::net::{
proxy::{CustomProxy, Socks5Local, Socks5Remote},
@ -295,17 +299,15 @@ pub async fn test_multihop(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> Result<(), Error> {
let wireguard_constraints = WireguardConstraints {
use_multihop: true,
..Default::default()
};
let relay_constraints = RelayQueryBuilder::new()
.wireguard()
.multihop()
.into_constraint();
let relay_settings = RelaySettings::Normal(RelayConstraints {
wireguard_constraints,
..Default::default()
});
set_relay_settings(&mut mullvad_client, relay_settings)
set_relay_settings(
&mut mullvad_client,
RelaySettings::Normal(relay_constraints),
)
.await
.expect("failed to update relay settings");
@ -556,15 +558,13 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel(
.await
.expect("Failed to enable obfuscation");
let relay_constraints = RelayQueryBuilder::new()
.wireguard()
.multihop()
.into_constraint();
mullvad_client
.set_relay_settings(relay_constraints::RelaySettings::Normal(RelayConstraints {
wireguard_constraints: WireguardConstraints {
use_multihop: true,
..Default::default()
},
tunnel_protocol: Constraint::Only(TunnelType::Wireguard),
..Default::default()
}))
.set_relay_settings(RelaySettings::Normal(relay_constraints))
.await
.expect("Failed to update relay settings");

View File

@ -12,9 +12,9 @@ use crate::{
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{
constraints::Constraint,
relay_constraints::{
Constraint, GeographicLocationConstraint, LocationConstraint, RelayConstraints,
RelaySettings,
GeographicLocationConstraint, LocationConstraint, RelayConstraints, RelaySettings,
},
relay_list::{Relay, RelayEndpointData},
states::TunnelState,
@ -275,8 +275,8 @@ pub async fn test_error_state(
log::info!("Enter error state");
let relay_settings = RelaySettings::Normal(RelayConstraints {
location: Constraint::Only(LocationConstraint::Location(
GeographicLocationConstraint::Country("xx".to_string()),
location: Constraint::Only(LocationConstraint::from(
GeographicLocationConstraint::country("xx"),
)),
..Default::default()
});