diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-10-18 16:09:56 +0200 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-10-18 16:09:56 +0200 |
| commit | f7ee8f3b4a2a746cd0001f1bf3a550d0d9515399 (patch) | |
| tree | 54445379a7b87a448f1e9e42d310c5074323fc38 | |
| parent | aab9cfa3b926f896643ef3687c9ef58b3546b801 (diff) | |
| parent | a416d9d12f5df6732eeb212e40cbf0800ff89bdf (diff) | |
| download | mullvadvpn-f7ee8f3b4a2a746cd0001f1bf3a550d0d9515399.tar.xz mullvadvpn-f7ee8f3b4a2a746cd0001f1bf3a550d0d9515399.zip | |
Merge branch 'make-sure-to-use-a-daita-compatible-entry-even-if-the-exit-des-1303'
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 3 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/error.rs | 2 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/lib.rs | 6 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/helpers.rs | 4 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/mod.rs | 186 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/query.rs | 28 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/relays.rs | 81 | ||||
| -rw-r--r-- | mullvad-relay-selector/tests/relay_selector.rs | 74 |
9 files changed, 273 insertions, 113 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a08634efd..9c9c19736d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ Line wrap the file at 100 chars. Th ### Changed - Replace the draft key encapsulation mechanism Kyber (round 3) with the standardized ML-KEM (FIPS 203) dito in the handshake for Quantum-resistant tunnels. +- Make Smart Routing override multihop if both are enabled. To manually set the entry relay, + explicitly enable the "Direct only" option in the DAITA settings. #### Windows - Enable quantum-resistant tunnels by default (when set to `auto`). diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index dd6c828802..08c523ffd7 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -2403,10 +2403,9 @@ impl Daemon { .intersection(Constraint::Only(TunnelType::Wireguard)) .is_some(); - let multihop_enabled = constraints.wireguard_constraints.use_multihop; let daita_enabled = self.settings.tunnel_options.wireguard.daita.enabled; - if settings_changed && wireguard_enabled && daita_enabled && !multihop_enabled { + if settings_changed && wireguard_enabled && daita_enabled { log::info!("Reconnecting because DAITA settings changed"); self.reconnect_tunnel(); } diff --git a/mullvad-relay-selector/src/error.rs b/mullvad-relay-selector/src/error.rs index dea6f81c3e..9f7a50bda3 100644 --- a/mullvad-relay-selector/src/error.rs +++ b/mullvad-relay-selector/src/error.rs @@ -3,7 +3,7 @@ use mullvad_types::{relay_constraints::MissingCustomBridgeSettings, relay_list::Relay}; -use crate::{detailer, WireguardConfig}; +use crate::{detailer, relay_selector::relays::WireguardConfig}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index 09708a059e..be12c29443 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -9,7 +9,7 @@ mod relay_selector; // Re-exports pub use error::Error; pub use relay_selector::{ - detailer, query, AdditionalRelayConstraints, AdditionalWireguardConstraints, GetRelay, - RelaySelector, RuntimeParameters, SelectedBridge, SelectedObfuscator, SelectorConfig, - WireguardConfig, RETRY_ORDER, + detailer, query, relays::WireguardConfig, AdditionalRelayConstraints, + AdditionalWireguardConstraints, GetRelay, RelaySelector, RuntimeParameters, SelectedBridge, + SelectedObfuscator, SelectorConfig, RETRY_ORDER, }; diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs index c8b529e0cd..a8a2ba7fcb 100644 --- a/mullvad-relay-selector/src/relay_selector/helpers.rs +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -207,8 +207,8 @@ fn port_if_in_range<R: RangeBounds<u16>>(port_ranges: &[R], port: u16) -> Result /// - `port_ranges`: A slice of port numbers. /// /// # Returns -/// - On success, a randomly selected port number within the given ranges. Otherwise, -/// an error is returned. +/// - On success, a randomly selected port number within the given ranges. Otherwise, an error is +/// returned. pub fn select_random_port<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>( port_ranges: &[R], ) -> Result<u16, Error> { diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index b94a5812a0..e7a6922727 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -5,16 +5,30 @@ mod helpers; mod matcher; mod parsed_relays; pub mod query; +pub mod relays; + +use matcher::{filter_matching_bridges, filter_matching_relay_list}; +use parsed_relays::ParsedRelays; +use relays::{Multihop, Singlehop, WireguardConfig}; + +use crate::{ + detailer::{openvpn_endpoint, wireguard_endpoint}, + error::{EndpointErrorDetails, Error}, + query::{ + BridgeQuery, ObfuscationQuery, OpenVpnRelayQuery, RelayQuery, RelayQueryExt, + WireguardRelayQuery, + }, +}; -use chrono::{DateTime, Local}; -use itertools::Itertools; -use query::ObfuscationQuery; use std::{ path::Path, sync::{Arc, LazyLock, Mutex}, time::SystemTime, }; +use chrono::{DateTime, Local}; +use itertools::Itertools; + use mullvad_types::{ constraints::Constraint, custom_list::CustomListsSettings, @@ -37,15 +51,6 @@ use talpid_types::{ 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, 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. @@ -221,49 +226,6 @@ pub enum GetRelay { 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 { - /// Strongly prefer to instantiate this variant using [`WireguardConfig::singlehop`] as that - /// will assert that the relay is of the expected type. - Singlehop { exit: Relay }, - /// Strongly prefer to instantiate this variant using [`WireguardConfig::multihop`] as that - /// will assert that the entry & exit relays are of the expected type. - Multihop { exit: Relay, entry: Relay }, -} - -impl WireguardConfig { - const fn singlehop(exit: Relay) -> Self { - // FIXME: This assert would be better to encode at the type level. - assert!(matches!( - exit.endpoint_data, - RelayEndpointData::Wireguard(_) - )); - Self::Singlehop { exit } - } - - const fn multihop(exit: Relay, entry: Relay) -> Self { - // FIXME: This assert would be better to encode at the type level. - assert!(matches!( - exit.endpoint_data, - RelayEndpointData::Wireguard(_) - )); - // FIXME: This assert would be better to encode at the type level. - assert!(matches!( - entry.endpoint_data, - RelayEndpointData::Wireguard(_) - )); - Self::Multihop { exit, entry } - } -} - #[derive(Clone, Debug)] pub enum SelectedBridge { Normal { settings: CustomProxy, relay: Relay }, @@ -675,7 +637,7 @@ impl RelaySelector { Self::get_wireguard_relay(&query, custom_lists, parsed_relays) } - /// Derive a valid Wireguard relay configuration from `query`. + /// Derive a valid relay configuration from `query`. /// /// # Note /// This function should *only* be called with a Wireguard query. @@ -697,11 +659,7 @@ impl RelaySelector { query.tunnel_protocol(), Constraint::Only(TunnelType::Wireguard) ); - let inner = if !query.wireguard_constraints().multihop() { - Self::get_wireguard_singlehop_config(query, custom_lists, parsed_relays)? - } else { - Self::get_wireguard_multihop_config(query, custom_lists, parsed_relays)? - }; + let inner = Self::get_wireguard_relay_config(query, custom_lists, parsed_relays)?; let endpoint = Self::get_wireguard_endpoint(query, parsed_relays, &inner)?; let obfuscator = Self::get_wireguard_obfuscator(query, inner.clone(), &endpoint, parsed_relays)?; @@ -713,67 +671,85 @@ impl RelaySelector { }) } - /// Select a valid Wireguard exit relay. + /// Derive a valid Wireguard relay configuration from `query`. /// /// # Returns /// * An `Err` if no exit relay can be chosen - /// * `Ok(WireguardInner::Singlehop)` otherwise - fn get_wireguard_singlehop_config( + /// * An `Err` if no entry relay can be chosen (if multihop is enabled on `query`) + /// * `Ok(WireguardConfig)` otherwise + fn get_wireguard_relay_config( query: &RelayQuery, custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, ) -> Result<WireguardConfig, Error> { - let candidates = filter_matching_relay_list(query, parsed_relays, custom_lists); - - // are we using daita? - let using_daita = || query.wireguard_constraints().daita == Constraint::Only(true); - - // is the `candidates` list empty because DAITA is enabled? - let no_relay_because_daita = || { - let mut query = query.clone(); - let mut wireguard_constraints = query.wireguard_constraints().clone(); - wireguard_constraints.daita = Constraint::Any; - query.set_wireguard_constraints(wireguard_constraints)?; - let candidates = filter_matching_relay_list(&query, parsed_relays, custom_lists); - Result::<_, Error>::Ok(!candidates.is_empty()) - }; - - // is `use_multihop_if_necessary` enabled? - let use_multihop_if_necessary = || { - query - .wireguard_constraints() - .daita_use_multihop_if_necessary - .intersection(Constraint::Only(true)) - .is_some() + // TODO: Remove when Android gets support for multihop. + if cfg!(target_os = "android") { + let relay = Self::get_wireguard_singlehop_config(query, custom_lists, parsed_relays) + .ok_or(Error::NoRelay)?; + return Ok(WireguardConfig::from(relay)); + } + let inner = if query.singlehop() { + match Self::get_wireguard_singlehop_config(query, custom_lists, parsed_relays) { + Some(exit) => WireguardConfig::from(exit), + None => { + // If we found no matching relays because DAITA was enabled, and + // `use_multihop_if_necessary` is enabled, try enabling + // multihop and connecting using an automatically selected + // entry relay. + if query.using_daita() && query.use_multihop_if_necessary() { + let multihop = Self::get_wireguard_auto_multihop_config( + query, + custom_lists, + parsed_relays, + )?; + WireguardConfig::from(multihop) + } else { + return Err(Error::NoRelay); + } + } + } + } else { + // A DAITA compatible entry should be used even when the exit is DAITA compatible. + // This only makes sense in context: The user is no longer able to explicitly choose an + // entry relay with smarting routing enabled, even if multihop is turned on + // Also implied: Multihop is enabled. + let multihop = if query.using_daita() && query.use_multihop_if_necessary() { + Self::get_wireguard_auto_multihop_config(query, custom_lists, parsed_relays)? + } else { + Self::get_wireguard_multihop_config(query, custom_lists, parsed_relays)? + }; + WireguardConfig::from(multihop) }; - // if we found no matching relays because DAITA was enabled, and `use_multihop_if_necessary` - // is enabled, try enabling multihop and connecting using an automatically selected - // entry relay. - if candidates.is_empty() - && using_daita() - && no_relay_because_daita()? - && use_multihop_if_necessary() - { - return Self::get_wireguard_auto_multihop_config(query, custom_lists, parsed_relays); - } + Ok(inner) + } + /// Select a valid Wireguard exit relay. + /// + /// # Returns + /// * `Ok(Singlehop)` if an exit relay was selected + /// * `None` otherwise + fn get_wireguard_singlehop_config( + query: &RelayQuery, + custom_lists: &CustomListsSettings, + parsed_relays: &ParsedRelays, + ) -> Option<Singlehop> { + let candidates = filter_matching_relay_list(query, parsed_relays, custom_lists); helpers::pick_random_relay(&candidates) .cloned() - .map(WireguardConfig::singlehop) - .ok_or(Error::NoRelay) + .map(Singlehop::new) } /// Select a valid Wireguard exit relay, together with with an automatically chosen entry relay. /// /// # Returns /// * An `Err` if no entry/exit relay can be chosen - /// * `Ok(WireguardInner::Multihop)` otherwise + /// * `Ok(Multihop)` otherwise fn get_wireguard_auto_multihop_config( query: &RelayQuery, custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, - ) -> Result<WireguardConfig, Error> { + ) -> Result<Multihop, Error> { let mut exit_relay_query = query.clone(); // DAITA should only be enabled for the entry relay @@ -810,7 +786,7 @@ impl RelaySelector { let entry = helpers::pick_random_relay_excluding(&entry_candidates, exit).ok_or(Error::NoRelay)?; - Ok(WireguardConfig::multihop(exit.clone(), entry.clone())) + Ok(Multihop::new(entry.clone(), exit.clone())) } /// This function selects a valid entry and exit relay to be used in a multihop configuration. @@ -819,14 +795,14 @@ impl RelaySelector { /// * 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 + /// * `Ok(WireguardConfig::Multihop)` otherwise fn get_wireguard_multihop_config( query: &RelayQuery, custom_lists: &CustomListsSettings, parsed_relays: &ParsedRelays, - ) -> Result<WireguardConfig, Error> { + ) -> Result<Multihop, 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 + // The actual query for an entry 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(); @@ -864,7 +840,7 @@ impl RelaySelector { } .ok_or(Error::NoRelay)?; - Ok(WireguardConfig::multihop(exit.clone(), entry.clone())) + Ok(Multihop::new(entry.clone(), exit.clone())) } /// Constructs a [`MullvadEndpoint`] with details for how to connect to `relay`. diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index 570a2212cb..162865368a 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -913,6 +913,34 @@ pub mod builder { } } +/// This trait defines a bunch of helper methods on [`RelayQuery`]. +pub trait RelayQueryExt { + /// Are we using daita? + fn using_daita(&self) -> bool; + /// is `use_multihop_if_necessary` enabled? In other words, is `Direct only` disabled? + fn use_multihop_if_necessary(&self) -> bool; + /// Are we using singlehop? I.e. is multihop *not* explicitly enabled? + fn singlehop(&self) -> bool; +} + +impl RelayQueryExt for RelayQuery { + fn using_daita(&self) -> bool { + self.wireguard_constraints() + .daita + .is_only_and(|enabled| enabled) + } + fn use_multihop_if_necessary(&self) -> bool { + self.wireguard_constraints() + .daita_use_multihop_if_necessary + // The default value is `Any`, which means that we need to check the intersection. + .intersection(Constraint::Only(true)) + .is_some() + } + fn singlehop(&self) -> bool { + !self.wireguard_constraints().multihop() + } +} + #[cfg(test)] mod test { use mullvad_types::{ diff --git a/mullvad-relay-selector/src/relay_selector/relays.rs b/mullvad-relay-selector/src/relay_selector/relays.rs new file mode 100644 index 0000000000..3cb8ff3c5d --- /dev/null +++ b/mullvad-relay-selector/src/relay_selector/relays.rs @@ -0,0 +1,81 @@ +//! This module defines wrapper types around [`Relay`], often to provide certain runtime guarantees +//! or disambiguate the type of relay which is used in the relay selector's internal APIs. + +use mullvad_types::relay_list::{Relay, RelayEndpointData}; + +/// - [`WireguardConfig::Singlehop`]: A wireguard relay where VPN traffic enters and exits. +/// - [`WireguardConfig::Multihop`]: Two wireguard relays to be used in a multihop circuit. VPN +/// traffic will enter through `entry` and eventually exit through `exit` before the traffic will +/// actually be routed to the internet. +#[derive(Clone, Debug)] +pub enum WireguardConfig { + /// An exit relay. + Singlehop { exit: Relay }, + /// An entry and an exit relay. + Multihop { exit: Relay, entry: Relay }, +} + +/// A type representing single Wireguard relay. +/// +/// Before you can read any data out of a [`Singlehop`] value uou need to convert it to +/// [`WireguardConfig`]. This is easy since [`Singlehop`] implements [`Into<WireguardConfig>`]. +/// +/// # Why not simply use [`Relay`]? +/// The only way to construct a [`Singlehop`] value is with [`Singlehop::new`] which performs +/// additional validation which guarantees that the relay actually is a Wireguard relay, while +/// [`Relay`] is not guaranteed to be a Wireguard relay. +pub struct Singlehop(Relay); +/// A type representing two Wireguard relay - an entry and an exit. +/// +/// Before you can read any data out of a [`Multihop`] value uou need to convert it to +/// [`WireguardConfig`]. This is easy since [`Multihop`] implements [`Into<WireguardConfig>`]. +/// +/// # Why not simply use [`Relay`]? +/// The same rationale as for [`Singlehop`] applies - [`Multihop::new`] performs additional +/// validation on the entry and exit relays. +pub struct Multihop { + entry: Relay, + exit: Relay, +} + +impl From<Singlehop> for WireguardConfig { + fn from(relay: Singlehop) -> Self { + Self::Singlehop { exit: relay.0 } + } +} + +impl From<Multihop> for WireguardConfig { + fn from(relay: Multihop) -> Self { + WireguardConfig::Multihop { + exit: relay.exit, + entry: relay.entry, + } + } +} + +impl Singlehop { + pub const fn new(exit: Relay) -> Self { + // FIXME: This assert would be better to encode at the type level. + assert!(matches!( + exit.endpoint_data, + RelayEndpointData::Wireguard(_) + )); + Self(exit) + } +} + +impl Multihop { + pub const fn new(entry: Relay, exit: Relay) -> Self { + // FIXME: This assert would be better to encode at the type level. + assert!(matches!( + exit.endpoint_data, + RelayEndpointData::Wireguard(_) + )); + // FIXME: This assert would be better to encode at the type level. + assert!(matches!( + entry.endpoint_data, + RelayEndpointData::Wireguard(_) + )); + Multihop { exit, entry } + } +} diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 1ce69b0591..6908d468de 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -96,6 +96,27 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { location: DUMMY_LOCATION.clone(), }, Relay { + hostname: "se11-wireguard".to_string(), + ipv4_addr_in: "185.213.154.69".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a11f".parse().unwrap()), + overridden_ipv4: false, + overridden_ipv6: false, + include_in_country: true, + active: true, + owned: false, + provider: "provider2".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + daita: true, + shadowsocks_extra_addr_in: vec![], + }), + location: DUMMY_LOCATION.clone(), + }, + Relay { hostname: "se-got-001".to_string(), ipv4_addr_in: "185.213.154.131".parse().unwrap(), ipv6_addr_in: None, @@ -1401,6 +1422,59 @@ fn test_daita_any_tunnel_protocol() { ); } +/// Always use smart routing to select a DAITA-enabled entry relay if both smart routing and +/// multihop is enabled. This applies even if the entry is set explicitly. +/// DAITA is a core privacy feature +#[test] +fn test_daita_smart_routing_overrides_multihop() { + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .daita_use_multihop_if_necessary(true) + .multihop() + // Set the entry to a relay that explicitly does *not* support DAITA. + // Later, we check that the smart routing disregards this choice and selects a DAITA-enabled + // relay instead. + .entry(NON_DAITA_RELAY_LOCATION.clone()) + .build(); + + for _ in 0..100 { + // Make sure a DAITA-enabled relay is always selected due to smart routing. + let relay = relay_selector + .get_relay_by_query(query.clone()) + .expect("Expected to find a relay with daita_use_multihop_if_necessary"); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { entry, exit: _ }, + .. + } => { + assert!(supports_daita(&entry), "entry relay must support DAITA"); + } + wrong_relay => panic!( + "Relay selector should have picked two Wireguard relays, instead chose {wrong_relay:?}" + ), + } + } + + // Assert that disabling smart routing for this query will fail to generate a valid multihop + // config, thus blocking the user. + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .daita_use_multihop_if_necessary(false) + .multihop() + .entry(NON_DAITA_RELAY_LOCATION.clone()) + .build(); + + let relay = relay_selector.get_relay_by_query(query); + + assert!( + relay.is_err(), + "expected there to be no valid multihop configuration! Instead got {relay:#?}" + ); +} + /// Always select a WireGuard relay when multihop is enabled /// Multihop is a core privacy feature #[test] |
