Skip to content

Commit

Permalink
Refactor mullvad-relay-selector
Browse files Browse the repository at this point in the history
Implement a system for choosing appropriate relay(s) built on 'queries'.
A query is a set of constraints which dictates which relay(s) that *can*
be chosen by the relay selector.

The user's settings can naturally be expressed as a query. The semantics
of merging two queries in a way that always prefer user settings is
defined by the new `Intersection` trait.

Decrust `mullvad-relay-selector` by splitting it up into several modules

- `query.rs`: Definition of a query on different types of relays. This
module is integral to the new API of `mullvad-relay-selector`
- `matcher.rs`: Logic for filtering out candidate relays based on a query.
- `detailer.rs`: Logic for deriving connection details for the selected relay.
- `tests/`: Integration tests for the new relay selector. These tests only use
the public APIs of `RelaySelector` and make sure that the output matches
the expected output in different scenarios.
  • Loading branch information
MarkusPettersson98 committed Mar 18, 2024
1 parent 10aeac9 commit 5103de8
Show file tree
Hide file tree
Showing 48 changed files with 4,690 additions and 3,498 deletions.
17 changes: 14 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 18 additions & 31 deletions docs/relay-selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -49,40 +49,27 @@ Endpoints may be filtered by:
Whilst all user selected constraints are always honored, when the user hasn't selected any specific
constraints, following default ones will take effect:

- If no tunnel protocol is specified, the first three connection attempts will use WireGuard. All
remaining attempts will use OpenVPN. If no specific constraints are set:
- The first two attempts will connect to a Wireguard server, first on a random port, and then port
53.
- The third attempt will connect to a Wireguard server on port 80 with _udp2tcp_.
- Remaining attempts will connect to OpenVPN servers, first over UDP on two random ports, and then
over TCP on port 443. Remaining attempts alternate between TCP and UDP on random ports.
- The first three connection attempts will use Wireguard.
- The first attempt will connect to a Wireguard relay on a random port.
- The second attempt will connect to a Wireguard relay on port 443
- The third attempt will connect to a Wireguard relay over IPv6 (if IPv6 is configured on the host)
- The fourth-to-seventh attempt will alternate between Wireguard and OpenVPN
- The fifth attempt will connect to an OpenVPN relay over TCP on port 443
- The sixth attempt will connect to a Wireguard relay on a random port using [UDP2TCP obfuscation](https://github.com/mullvad/udp-over-tcp)
- The seventh attempt will connect to a Wireguard relay over IPv6 on a random port using UDP2TCP obfuscation (if IPv6 is configured on the host)
- The eighth attempt will connect to an OpenVPN relay over a bridge

- If the tunnel protocol is specified as WireGuard and obfuscation mode is set to _Auto_:
- First two attempts will be used without _udp2tcp_, using a random port on first attempt, and
port 53 on second attempt.
- Next two attempts will use _udp2tcp_ on ports 80 and 5001 respectively.
- The above steps repeat ad infinitum.
If no tunnel has been established after exhausting this list of attempts, the relay selector will
loop back to the first default constraint and continue its search from there.

If obfuscation is turned on, connections will alternate between port 80 and port 5001 using
_udp2tcp_ all of the time.

If obfuscation is turned _off_, WireGuard connections will first alternate between using
a random port and port 53, e.g. first attempt using port 22151, second 53, third
26107, fourth attempt using port 53, and so on.

If the user has specified a specific port for either _udp2tcp_ or WireGuard, it will override the
port selection, but it will not change the connection type described above (WireGuard or WireGuard
over _udp2tcp_).

- If no OpenVPN tunnel constraints are specified, then the first two attempts at selecting a tunnel
will try to select UDP endpoints on any port, and the third and fourth attempts will filter for
TCP endpoints on port 443. Any subsequent filtering attempts will alternate between TCP and UDP on
any port.
Any default constraint that is incompatible with user specified constraints will simply not be
considered. Conversely, all default constraints which do not conflict with user specified constraints
will be used in the search for a working tunnel endpoint on repeated connection failures.

## Selecting tunnel endpoint between filtered relays

To select a single relay from the set of filtered relays, the relay selector uses a roulette wheel
selection algorithm using the weights that are assigned to each relay. The higher the weight is
selection algorithm using the weights that are assigned to each relay. The higher the weight is
relatively to other relays, the higher the likelihood that a given relay will be picked. Once a
relay is picked, then a random endpoint that matches the constraints from the relay is picked.

Expand Down
5 changes: 3 additions & 2 deletions mullvad-cli/src/cmds/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
3 changes: 1 addition & 2 deletions mullvad-cli/src/cmds/custom_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
5 changes: 4 additions & 1 deletion mullvad-cli/src/cmds/debug.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions mullvad-cli/src/cmds/obfuscation.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
9 changes: 5 additions & 4 deletions mullvad-cli/src/cmds/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -318,7 +319,7 @@ impl Relay {

print_option!(
"Multihop state",
if constraints.wireguard_constraints.use_multihop {
if constraints.wireguard_constraints.multihop() {
"enabled"
} else {
"disabled"
Expand Down Expand Up @@ -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)) => {
Expand Down
3 changes: 2 additions & 1 deletion mullvad-cli/src/cmds/relay_constraints.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion mullvad-cli/src/cmds/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down
7 changes: 3 additions & 4 deletions mullvad-daemon/src/custom_list.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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
{
Expand Down
44 changes: 22 additions & 22 deletions mullvad-daemon/src/device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,12 +1348,10 @@ impl TunnelStateChangeHandler {
#[cfg(test)]
mod test {
use super::{Error, TunnelStateChangeHandler, WG_DEVICE_CHECK_THRESHOLD};
use mullvad_relay_selector::RelaySelector;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use talpid_types::net::TunnelType;

const TIMEOUT_ERROR: Error = Error::OtherRestError(mullvad_api::rest::Error::TimeoutError);

Expand Down Expand Up @@ -1437,24 +1435,26 @@ mod test {
);
}

/// Test whether the relay selector selects wireguard often enough, given no special
/// constraints, to verify that the device is valid
#[test]
fn test_validates_by_default() {
for attempt in 0.. {
let should_validate =
TunnelStateChangeHandler::should_check_validity_on_attempt(attempt);
let (_, _, tunnel_type) =
RelaySelector::preferred_tunnel_constraints(attempt.try_into().unwrap());
assert_eq!(
tunnel_type,
TunnelType::Wireguard,
"failed on attempt {attempt}"
);
if should_validate {
// Now that we've triggered a device check, we can give up
break;
}
}
}
// TODO(markus): `preferred_tunnel_constraints` is slated for removal - consider writing a new test which
// does not depend on relay selector internals.
// /// Test whether the relay selector selects wireguard often enough, given no special
// /// constraints, to verify that the device is valid
// #[test]
// fn test_validates_by_default() {
// for attempt in 0.. {
// let should_validate =
// TunnelStateChangeHandler::should_check_validity_on_attempt(attempt);
// let (_, _, tunnel_type) =
// RelaySelector::preferred_tunnel_constraints(attempt.try_into().unwrap());
// assert_eq!(
// tunnel_type,
// TunnelType::Wireguard,
// "failed on attempt {attempt}"
// );
// if should_validate {
// // Now that we've triggered a device check, we can give up
// break;
// }
// }
// }
}
8 changes: 4 additions & 4 deletions mullvad-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mullvad-daemon/src/migrations/v1.rs
Original file line number Diff line number Diff line change
@@ -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};

// ======================================================
Expand Down
2 changes: 1 addition & 1 deletion mullvad-daemon/src/migrations/v4.rs
Original file line number Diff line number Diff line change
@@ -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};

// ======================================================
Expand Down
2 changes: 1 addition & 1 deletion mullvad-daemon/src/migrations/v5.rs
Original file line number Diff line number Diff line change
@@ -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};

// ======================================================
Expand Down
2 changes: 1 addition & 1 deletion mullvad-daemon/src/migrations/v6.rs
Original file line number Diff line number Diff line change
@@ -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};

// ======================================================
Expand Down
Loading

0 comments on commit 5103de8

Please sign in to comment.