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:
parent
66f2127aed
commit
707ecf44bd
@ -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
17
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -16,15 +16,15 @@
|
||||
# Relay selector
|
||||
|
||||
The relay selector's main purpose is to pick a single Mullvad relay from a list of relays taking
|
||||
into account certain user-configurable criteria. Relays can be filtered by their _location_
|
||||
into account certain user-configurable criteria. Relays can be filtered by their _location_
|
||||
(country, city, hostname), by the protocols and ports they support (transport protocol, tunnel
|
||||
protocol, port), and by other constraints. The constraints are user specified and stored in the
|
||||
protocol, port), and by other constraints. The constraints are user specified and stored in the
|
||||
settings. The default value for location constraints restricts relay selection to relays from Sweden.
|
||||
The default protocol constraints default to _Auto_, which implies specific behavior.
|
||||
|
||||
Generally, the filtering process consists of going through each relay in our relay list and
|
||||
removing relay and endpoint combinations that do not match the constraints outlined above. The
|
||||
filtering process produces a list of relays that only contain matching endpoints. Of all the relays
|
||||
filtering process produces a list of relays that only contain matching endpoints. Of all the relays
|
||||
that match the constraints, one is selected and a random matching endpoint is selected from that
|
||||
relay.
|
||||
|
||||
@ -47,42 +47,29 @@ 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
|
||||
|
||||
To select a single relay from the set of filtered relays, the relay selector uses a roulette wheel
|
||||
selection algorithm using the weights that are assigned to each relay. The higher the weight is
|
||||
selection algorithm using the weights that are assigned to each relay. The higher the weight is
|
||||
relatively to other relays, the higher the likelihood that a given relay will be picked. Once a
|
||||
relay is picked, then a random endpoint that matches the constraints from the relay is picked.
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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)]
|
||||
|
@ -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 {
|
||||
|
@ -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)]
|
||||
|
@ -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)) => {
|
||||
|
@ -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)]
|
||||
|
@ -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},
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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};
|
||||
|
||||
// ======================================================
|
||||
|
@ -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};
|
||||
|
||||
// ======================================================
|
||||
|
@ -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};
|
||||
|
||||
// ======================================================
|
||||
|
@ -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};
|
||||
|
||||
// ======================================================
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,127 +140,129 @@ 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)) => {
|
||||
self.last_generated_relays = None;
|
||||
custom_relay
|
||||
// TODO: generate proxy settings for custom tunnels
|
||||
.to_tunnel_parameters(self.tunnel_options.clone(), None)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to resolve hostname for custom tunnel config: {}", e);
|
||||
Error::ResolveCustomHostname
|
||||
})
|
||||
}
|
||||
Ok((SelectedRelay::Normal(constraints), bridge, obfuscator)) => {
|
||||
self.create_tunnel_parameters(
|
||||
&constraints.exit_relay,
|
||||
&constraints.entry_relay,
|
||||
constraints.endpoint,
|
||||
bridge,
|
||||
obfuscator,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(mullvad_relay_selector::Error::NoBridge) => Err(Error::NoBridgeAvailable),
|
||||
Err(_error) => Err(Error::NoRelayAvailable),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "android", allow(unused_variables))]
|
||||
async fn create_tunnel_parameters(
|
||||
async fn generate(
|
||||
&mut self,
|
||||
relay: &Relay,
|
||||
entry_relay: &Option<Relay>,
|
||||
endpoint: MullvadEndpoint,
|
||||
bridge: Option<SelectedBridge>,
|
||||
obfuscator: Option<SelectedObfuscator>,
|
||||
retry_attempt: u32,
|
||||
ipv6: bool,
|
||||
) -> Result<TunnelParameters, Error> {
|
||||
let data = self.device().await?;
|
||||
match endpoint {
|
||||
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"))]
|
||||
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),
|
||||
};
|
||||
|
||||
GetRelay::OpenVpn {
|
||||
endpoint,
|
||||
exit,
|
||||
bridge,
|
||||
} => {
|
||||
let bridge_relay = bridge.as_ref().and_then(|bridge| bridge.relay());
|
||||
self.last_generated_relays = Some(LastSelectedRelays::OpenVpn {
|
||||
relay: relay.clone(),
|
||||
bridge: bridge_relay,
|
||||
relay: exit.clone(),
|
||||
bridge: bridge_relay.cloned(),
|
||||
});
|
||||
|
||||
Ok(openvpn::TunnelParameters {
|
||||
config: openvpn::ConnectionConfig::new(
|
||||
endpoint,
|
||||
data.account_token,
|
||||
"-".to_string(),
|
||||
),
|
||||
options: self.tunnel_options.openvpn.clone(),
|
||||
generic_options: self.tunnel_options.generic.clone(),
|
||||
proxy: bridge_settings,
|
||||
#[cfg(target_os = "linux")]
|
||||
fwmark: mullvad_types::TUNNEL_FWMARK,
|
||||
}
|
||||
.into())
|
||||
let bridge_settings = bridge.as_ref().map(|bridge| bridge.settings());
|
||||
Ok(self.create_openvpn_tunnel_parameters(endpoint, data, bridge_settings.cloned()))
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
MullvadEndpoint::OpenVpn(endpoint) => {
|
||||
unreachable!("OpenVPN is not supported on Android");
|
||||
}
|
||||
MullvadEndpoint::Wireguard(endpoint) => {
|
||||
let tunnel_ipv4 = data.device.wg_data.addresses.ipv4_address.ip();
|
||||
let tunnel_ipv6 = data.device.wg_data.addresses.ipv6_address.ip();
|
||||
let tunnel = wireguard::TunnelConfig {
|
||||
private_key: data.device.wg_data.private_key,
|
||||
addresses: vec![IpAddr::from(tunnel_ipv4), IpAddr::from(tunnel_ipv6)],
|
||||
};
|
||||
// FIXME: Used for debugging purposes during the migration to same IP. Remove when
|
||||
// the migration is over.
|
||||
if tunnel_ipv4 == *SAME_IP_V4 || tunnel_ipv6 == *SAME_IP_V6 {
|
||||
log::debug!("Same IP is being used");
|
||||
} else {
|
||||
log::debug!("Same IP is NOT being used");
|
||||
}
|
||||
|
||||
GetRelay::Wireguard {
|
||||
endpoint,
|
||||
obfuscator,
|
||||
inner,
|
||||
} => {
|
||||
let (obfuscator_relay, obfuscator_config) = match obfuscator {
|
||||
Some(obfuscator) => (Some(obfuscator.relay), Some(obfuscator.config)),
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let (wg_entry, wg_exit) = match inner {
|
||||
WireguardConfig::Singlehop { exit } => (None, exit),
|
||||
WireguardConfig::Multihop { exit, entry } => (Some(entry), exit),
|
||||
};
|
||||
self.last_generated_relays = Some(LastSelectedRelays::WireGuard {
|
||||
wg_entry: entry_relay.clone(),
|
||||
wg_exit: relay.clone(),
|
||||
wg_entry,
|
||||
wg_exit,
|
||||
obfuscator: obfuscator_relay,
|
||||
});
|
||||
|
||||
Ok(wireguard::TunnelParameters {
|
||||
connection: wireguard::ConnectionConfig {
|
||||
tunnel,
|
||||
peer: endpoint.peer,
|
||||
exit_peer: endpoint.exit_peer,
|
||||
ipv4_gateway: endpoint.ipv4_gateway,
|
||||
ipv6_gateway: Some(endpoint.ipv6_gateway),
|
||||
#[cfg(target_os = "linux")]
|
||||
fwmark: Some(mullvad_types::TUNNEL_FWMARK),
|
||||
},
|
||||
options: self
|
||||
.tunnel_options
|
||||
.wireguard
|
||||
.clone()
|
||||
.into_talpid_tunnel_options(),
|
||||
generic_options: self.tunnel_options.generic.clone(),
|
||||
obfuscation: obfuscator_config,
|
||||
}
|
||||
.into())
|
||||
Ok(self.create_wireguard_tunnel_parameters(endpoint, data, obfuscator_config))
|
||||
}
|
||||
GetRelay::Custom(custom_relay) => {
|
||||
self.last_generated_relays = None;
|
||||
custom_relay
|
||||
// TODO: generate proxy settings for custom tunnels
|
||||
.to_tunnel_parameters(self.tunnel_options.clone(), None)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to resolve hostname for custom tunnel config: {}", e);
|
||||
Error::ResolveCustomHostname
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn create_openvpn_tunnel_parameters(
|
||||
&self,
|
||||
endpoint: Endpoint,
|
||||
data: PrivateAccountAndDevice,
|
||||
bridge_settings: Option<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()
|
||||
}
|
||||
|
||||
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 {
|
||||
private_key: data.device.wg_data.private_key,
|
||||
addresses: vec![IpAddr::from(tunnel_ipv4), IpAddr::from(tunnel_ipv6)],
|
||||
};
|
||||
// FIXME: Used for debugging purposes during the migration to same IP. Remove when
|
||||
// the migration is over.
|
||||
if tunnel_ipv4 == *SAME_IP_V4 || tunnel_ipv6 == *SAME_IP_V6 {
|
||||
log::debug!("Same IP is being used");
|
||||
} else {
|
||||
log::debug!("Same IP is NOT being used");
|
||||
}
|
||||
|
||||
wireguard::TunnelParameters {
|
||||
connection: wireguard::ConnectionConfig {
|
||||
tunnel,
|
||||
peer: endpoint.peer,
|
||||
exit_peer: endpoint.exit_peer,
|
||||
ipv4_gateway: endpoint.ipv4_gateway,
|
||||
ipv6_gateway: Some(endpoint.ipv6_gateway),
|
||||
#[cfg(target_os = "linux")]
|
||||
fwmark: Some(mullvad_types::TUNNEL_FWMARK),
|
||||
},
|
||||
options: self
|
||||
.tunnel_options
|
||||
.wireguard
|
||||
.clone()
|
||||
.into_talpid_tunnel_options(),
|
||||
generic_options: self.tunnel_options.generic.clone(),
|
||||
obfuscation: obfuscator_config,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn device(&self) -> Result<PrivateAccountAndDevice, Error> {
|
||||
self.account_manager
|
||||
.data()
|
||||
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
4
mullvad-relay-selector/src/constants.rs
Normal file
4
mullvad-relay-selector/src/constants.rs
Normal 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];
|
66
mullvad-relay-selector/src/error.rs
Normal file
66
mullvad-relay-selector/src/error.rs
Normal 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
@ -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
|
||||
}
|
||||
}
|
283
mullvad-relay-selector/src/relay_selector/detailer.rs
Normal file
283
mullvad-relay-selector/src/relay_selector/detailer.rs
Normal 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
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
124
mullvad-relay-selector/src/relay_selector/helpers.rs
Normal file
124
mullvad-relay-selector/src/relay_selector/helpers.rs
Normal 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(),
|
||||
}
|
||||
}
|
186
mullvad-relay-selector/src/relay_selector/matcher.rs
Normal file
186
mullvad-relay-selector/src/relay_selector/matcher.rs
Normal 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))
|
||||
}
|
||||
}
|
978
mullvad-relay-selector/src/relay_selector/mod.rs
Normal file
978
mullvad-relay-selector/src/relay_selector/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
189
mullvad-relay-selector/src/relay_selector/parsed_relays.rs
Normal file
189
mullvad-relay-selector/src/relay_selector/parsed_relays.rs
Normal 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
|
||||
}
|
||||
}
|
916
mullvad-relay-selector/src/relay_selector/query.rs
Normal file
916
mullvad-relay-selector/src/relay_selector/query.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
1112
mullvad-relay-selector/tests/relay_selector.rs
Normal file
1112
mullvad-relay-selector/tests/relay_selector.rs
Normal file
File diff suppressed because it is too large
Load Diff
165
mullvad-types/src/constraints/constraint.rs
Normal file
165
mullvad-types/src/constraints/constraint.rs
Normal 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)
|
||||
}
|
||||
}
|
35
mullvad-types/src/constraints/mod.rs
Normal file
35
mullvad-types/src/constraints/mod.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
.location
|
||||
.as_ref()
|
||||
.map_or(false, |loc| loc.country_code == *country)
|
||||
&& (ignore_include_in_country || relay.include_in_country)
|
||||
}
|
||||
GeographicLocationConstraint::Country(ref country) => relay
|
||||
.location
|
||||
.as_ref()
|
||||
.map_or(false, |loc| loc.country_code == *country),
|
||||
GeographicLocationConstraint::City(ref country, ref city) => {
|
||||
relay.location.as_ref().map_or(false, |loc| {
|
||||
loc.country_code == *country && loc.city_code == *city
|
||||
@ -532,34 +294,6 @@ impl GeographicLocationConstraint {
|
||||
}
|
||||
}
|
||||
|
||||
impl Constraint<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,
|
||||
|
@ -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")]
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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))
|
||||
|
@ -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>>>>;
|
||||
}
|
||||
|
||||
|
@ -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" ] }
|
||||
|
@ -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")]
|
||||
|
@ -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
11
test/Cargo.lock
generated
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
)),
|
||||
..
|
||||
|
@ -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,19 +299,17 @@ pub async fn test_multihop(
|
||||
rpc: ServiceClient,
|
||||
mut mullvad_client: MullvadProxyClient,
|
||||
) -> Result<(), Error> {
|
||||
let wireguard_constraints = WireguardConstraints {
|
||||
use_multihop: true,
|
||||
..Default::default()
|
||||
};
|
||||
let relay_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)
|
||||
.await
|
||||
.expect("failed to update relay settings");
|
||||
set_relay_settings(
|
||||
&mut mullvad_client,
|
||||
RelaySettings::Normal(relay_constraints),
|
||||
)
|
||||
.await
|
||||
.expect("failed to update relay settings");
|
||||
|
||||
//
|
||||
// Connect
|
||||
@ -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");
|
||||
|
||||
|
@ -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()
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user