From 4575d5e105bf38b4a75abbfd20f431b6bccf5d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 8 Nov 2023 12:57:06 +0100 Subject: [PATCH 01/10] Add relay overrides setting --- mullvad-types/src/relay_constraints.rs | 19 ++++++++++++++++++- mullvad-types/src/settings/mod.rs | 10 +++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index fffc6e03def2..219d0940bae8 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -10,7 +10,12 @@ use crate::{ #[cfg(target_os = "android")] use jnix::{jni::objects::JObject, FromJava, IntoJava, JnixEnv}; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, fmt, str::FromStr}; +use std::{ + collections::HashSet, + fmt, + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; use talpid_types::net::{openvpn::ProxySettings, IpVersion, TransportProtocol, TunnelType}; pub trait Match { @@ -991,3 +996,15 @@ pub struct InternalBridgeConstraints { pub ownership: Constraint, pub transport_protocol: Constraint, } + +/// Options to override for a particular relay to use instead of the ones specified in the relay +/// list +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct RelayOverride { + /// Hostname for which to override the given options + pub hostname: Hostname, + /// IPv4 address to use instead of the default + pub ipv4_addr_in: Option, + /// IPv6 address to use instead of the default + pub ipv6_addr_in: Option, +} diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 5978c6a736e3..2ed2d793946e 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -3,7 +3,7 @@ use crate::{ custom_list::CustomListsSettings, relay_constraints::{ BridgeConstraints, BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint, - LocationConstraint, ObfuscationSettings, RelayConstraints, RelaySettings, + LocationConstraint, ObfuscationSettings, RelayConstraints, RelayOverride, RelaySettings, RelaySettingsFormatter, SelectedObfuscation, WireguardConstraints, }, wireguard, @@ -91,6 +91,9 @@ pub struct Settings { /// Options that should be applied to tunnels of a specific type regardless of where the relays /// might be located. pub tunnel_options: TunnelOptions, + /// Overrides for relays + #[cfg_attr(target_os = "android", jnix(skip))] + pub relay_overrides: Vec, /// Whether to notify users of beta updates. pub show_beta_releases: bool, /// Split tunneling settings @@ -131,16 +134,17 @@ impl Default for Settings { ..Default::default() }, bridge_state: BridgeState::Auto, + custom_lists: CustomListsSettings::default(), + api_access_methods: access_method::Settings::default(), allow_lan: false, block_when_disconnected: false, auto_connect: false, tunnel_options: TunnelOptions::default(), + relay_overrides: vec![], show_beta_releases: false, #[cfg(windows)] split_tunnel: SplitTunnelSettings::default(), settings_version: CURRENT_SETTINGS_VERSION, - custom_lists: CustomListsSettings::default(), - api_access_methods: access_method::Settings::default(), } } } From f49d4c49f73905f1aadd4a67ab47616e943c4e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 8 Nov 2023 13:54:55 +0100 Subject: [PATCH 02/10] Add proto message for relay overrides --- .../proto/management_interface.proto | 7 ++++ .../types/conversions/relay_constraints.rs | 36 +++++++++++++++++++ .../src/types/conversions/settings.rs | 11 ++++++ 3 files changed, 54 insertions(+) diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 0eefd9a1db53..17854b69563b 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -390,6 +390,13 @@ message Settings { ObfuscationSettings obfuscation_settings = 10; CustomListSettings custom_lists = 11; ApiAccessMethodSettings api_access_methods = 12; + repeated RelayOverride relay_overrides = 13; +} + +message RelayOverride { + string hostname = 1; + optional string ipv4_addr_in = 2; + optional string ipv6_addr_in = 3; } message SplitTunnelSettings { diff --git a/mullvad-management-interface/src/types/conversions/relay_constraints.rs b/mullvad-management-interface/src/types/conversions/relay_constraints.rs index cff2bb584726..a1d468c976c9 100644 --- a/mullvad-management-interface/src/types/conversions/relay_constraints.rs +++ b/mullvad-management-interface/src/types/conversions/relay_constraints.rs @@ -545,6 +545,42 @@ impl TryFrom for mullvad_types::relay_constraints::Transpo } } +impl From for proto::RelayOverride { + fn from(r#override: mullvad_types::relay_constraints::RelayOverride) -> proto::RelayOverride { + proto::RelayOverride { + hostname: r#override.hostname, + ipv4_addr_in: r#override.ipv4_addr_in.map(|addr| addr.to_string()), + ipv6_addr_in: r#override.ipv6_addr_in.map(|addr| addr.to_string()), + } + } +} + +impl TryFrom for mullvad_types::relay_constraints::RelayOverride { + type Error = FromProtobufTypeError; + + fn try_from( + r#override: proto::RelayOverride, + ) -> Result { + Ok(mullvad_types::relay_constraints::RelayOverride { + hostname: r#override.hostname, + ipv4_addr_in: r#override + .ipv4_addr_in + .map(|addr| { + addr.parse() + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid IPv4 address")) + }) + .transpose()?, + ipv6_addr_in: r#override + .ipv6_addr_in + .map(|addr| { + addr.parse() + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid IPv6 address")) + }) + .transpose()?, + }) + } +} + pub fn try_providers_constraint_from_proto( providers: &[String], ) -> Result, FromProtobufTypeError> { diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index c18e7aab57c0..a7c4bcd78c31 100644 --- a/mullvad-management-interface/src/types/conversions/settings.rs +++ b/mullvad-management-interface/src/types/conversions/settings.rs @@ -45,6 +45,12 @@ impl From<&mullvad_types::settings::Settings> for proto::Settings { api_access_methods: Some(proto::ApiAccessMethodSettings::from( &settings.api_access_methods, )), + relay_overrides: settings + .relay_overrides + .iter() + .cloned() + .map(proto::RelayOverride::from) + .collect(), } } } @@ -168,6 +174,11 @@ impl TryFrom for mullvad_types::settings::Settings { block_when_disconnected: settings.block_when_disconnected, auto_connect: settings.auto_connect, tunnel_options: mullvad_types::settings::TunnelOptions::try_from(tunnel_options)?, + relay_overrides: settings + .relay_overrides + .into_iter() + .map(mullvad_types::relay_constraints::RelayOverride::try_from) + .collect::, _>>()?, show_beta_releases: settings.show_beta_releases, #[cfg(windows)] split_tunnel: mullvad_types::settings::SplitTunnelSettings::from(split_tunnel), From 1acc09c62e42762bfe070f602a63d68d8067adb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 8 Nov 2023 15:03:33 +0100 Subject: [PATCH 03/10] Patch in-ips in relay selector --- mullvad-daemon/src/lib.rs | 1 + mullvad-relay-selector/src/lib.rs | 92 ++++++++++++++++++++------- mullvad-relay-selector/src/updater.rs | 2 +- 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 77304ae75f8b..7f86e7b364b7 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -2522,5 +2522,6 @@ fn new_selector_config(settings: &Settings) -> SelectorConfig { obfuscation_settings: settings.obfuscation_settings.clone(), default_tunnel_type, custom_lists: settings.custom_lists.clone(), + relay_overrides: settings.relay_overrides.clone(), } } diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index 1046624af7d7..c3290363dd13 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -6,12 +6,12 @@ use ipnetwork::IpNetwork; use mullvad_types::{ custom_list::CustomListsSettings, endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, - location::{Coordinates, Location}, + location::{Coordinates, Hostname, Location}, relay_constraints::{ BridgeSettings, BridgeState, Constraint, InternalBridgeConstraints, LocationConstraint, Match, ObfuscationSettings, OpenVpnConstraints, Ownership, Providers, RelayConstraints, - RelayConstraintsFormatter, RelaySettings, ResolvedLocationConstraint, SelectedObfuscation, - Set, TransportPort, Udp2TcpObfuscationSettings, + RelayConstraintsFormatter, RelayOverride, RelaySettings, ResolvedLocationConstraint, + SelectedObfuscation, Set, TransportPort, Udp2TcpObfuscationSettings, }, relay_list::{BridgeEndpointData, Relay, RelayEndpointData, RelayList}, CustomTunnelEndpoint, @@ -19,6 +19,7 @@ use mullvad_types::{ use parking_lot::{Mutex, MutexGuard}; use rand::{seq::SliceRandom, Rng}; use std::{ + collections::HashMap, io, net::{IpAddr, SocketAddr}, path::Path, @@ -81,6 +82,7 @@ struct ParsedRelays { last_updated: SystemTime, locations: RelayList, relays: Vec, + overrides: HashMap, } impl ParsedRelays { @@ -89,6 +91,7 @@ impl ParsedRelays { last_updated: time::UNIX_EPOCH, locations: RelayList::empty(), relays: Vec::new(), + overrides: HashMap::new(), } } @@ -97,36 +100,33 @@ impl ParsedRelays { if relay_list.wireguard.udp2tcp_ports.is_empty() { relay_list.wireguard.udp2tcp_ports.extend(UDP2TCP_PORTS); } + ParsedRelays { + last_updated, + relays: Self::relays_with_location(&relay_list), + locations: relay_list, + overrides: HashMap::new(), + } + } - let mut relays = Vec::new(); + fn relays_with_location(relay_list: &RelayList) -> Vec { + let mut relays = vec![]; for country in &relay_list.countries { - let country_name = country.name.clone(); - let country_code = country.code.clone(); for city in &country.cities { - let city_name = city.name.clone(); - let city_code = city.code.clone(); - let latitude = city.latitude; - let longitude = city.longitude; for relay in &city.relays { let mut relay_with_location = relay.clone(); relay_with_location.location = Some(Location { - country: country_name.clone(), - country_code: country_code.clone(), - city: city_name.clone(), - city_code: city_code.clone(), - latitude, - longitude, + country: country.name.clone(), + country_code: country.code.clone(), + city: city.name.clone(), + city_code: city.code.clone(), + latitude: city.latitude, + longitude: city.longitude, }); relays.push(relay_with_location); } } } - - ParsedRelays { - last_updated, - locations: relay_list, - relays, - } + relays } pub fn from_file(path: impl AsRef) -> Result { @@ -160,6 +160,43 @@ impl ParsedRelays { pub fn tag(&self) -> Option<&str> { self.locations.etag.as_deref() } + + fn reset_overrides(&mut self) { + self.relays = Self::relays_with_location(&self.locations); + } + + fn append_overrides(&mut self, overrides: Vec) { + self.overrides.clear(); + for r#override in overrides { + self.overrides + .insert(r#override.hostname.to_owned(), r#override); + } + + for relay in &mut self.relays { + if let Some(overrides) = self.overrides.get(&relay.hostname) { + if let Some(ipv4_addr_in) = overrides.ipv4_addr_in { + log::debug!( + "Overriding ipv4_addr_in for {}: {ipv4_addr_in}", + relay.hostname + ); + relay.ipv4_addr_in = ipv4_addr_in; + } + if let Some(ipv6_addr_in) = overrides.ipv6_addr_in { + log::debug!( + "Overriding ipv6_addr_in for {}: {ipv6_addr_in}", + relay.hostname + ); + relay.ipv6_addr_in = Some(ipv6_addr_in); + } + } + } + } + + /// Update list while keeping overrides + pub fn update(&mut self, update: ParsedRelays) { + let old = std::mem::replace(self, update); + self.append_overrides(old.overrides.into_values().collect()); + } } #[derive(Clone)] @@ -170,6 +207,7 @@ pub struct SelectorConfig { pub obfuscation_settings: ObfuscationSettings, pub default_tunnel_type: TunnelType, pub custom_lists: CustomListsSettings, + pub relay_overrides: Vec, } #[derive(Clone)] @@ -183,8 +221,8 @@ impl RelaySelector { pub fn new(config: SelectorConfig, resource_dir: &Path, cache_dir: &Path) -> Self { let cache_path = cache_dir.join(RELAYS_FILENAME); let resource_path = resource_dir.join(RELAYS_FILENAME); - let unsynchronized_parsed_relays = Self::read_relays_from_disk(&cache_path, &resource_path) - .unwrap_or_else(|error| { + let mut unsynchronized_parsed_relays = + Self::read_relays_from_disk(&cache_path, &resource_path).unwrap_or_else(|error| { log::error!( "{}", error.display_chain_with_msg("Unable to load cached relays") @@ -198,6 +236,8 @@ impl RelaySelector { .format(DATE_TIME_FORMAT_STR) ); + unsynchronized_parsed_relays.append_overrides(config.relay_overrides.clone()); + RelaySelector { config: Arc::new(Mutex::new(config)), parsed_relays: Arc::new(Mutex::new(unsynchronized_parsed_relays)), @@ -205,6 +245,9 @@ impl RelaySelector { } pub fn set_config(&mut self, config: SelectorConfig) { + let mut parsed_relays = self.parsed_relays.lock(); + parsed_relays.reset_overrides(); + parsed_relays.append_overrides(config.relay_overrides.clone()); *self.config.lock() = config; } @@ -1474,6 +1517,7 @@ mod test { bridge_state: BridgeState::Auto, default_tunnel_type: default_tunnel_type(), custom_lists: CustomListsSettings::default(), + relay_overrides: vec![], })), } } diff --git a/mullvad-relay-selector/src/updater.rs b/mullvad-relay-selector/src/updater.rs index 6600efb02f42..ff15e7c793b5 100644 --- a/mullvad-relay-selector/src/updater.rs +++ b/mullvad-relay-selector/src/updater.rs @@ -185,7 +185,7 @@ impl RelayListUpdater { ); let mut parsed_relays = self.parsed_relays.lock(); - *parsed_relays = new_parsed_relays; + parsed_relays.update(new_parsed_relays); (self.on_update)(parsed_relays.locations()); Ok(()) } From 2ddc9d4d93690e8fc12ab0926ac4c6e770ee439e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 8 Nov 2023 16:25:53 +0100 Subject: [PATCH 04/10] Add management interface for relay override --- mullvad-daemon/src/lib.rs | 62 ++++++++++++++++++- mullvad-daemon/src/management_interface.rs | 29 ++++++++- .../proto/management_interface.proto | 2 + mullvad-types/src/relay_constraints.rs | 14 +++++ mullvad-types/src/settings/mod.rs | 18 ++++++ 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 7f86e7b364b7..98a8febe2a28 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -47,7 +47,9 @@ use mullvad_types::{ custom_list::CustomList, device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent}, location::GeoIpLocation, - relay_constraints::{BridgeSettings, BridgeState, ObfuscationSettings, RelaySettings}, + relay_constraints::{ + BridgeSettings, BridgeState, ObfuscationSettings, RelayOverride, RelaySettings, + }, relay_list::RelayList, settings::{DnsOptions, Settings}, states::{TargetState, TunnelState}, @@ -253,6 +255,10 @@ pub enum DaemonCommand { SetQuantumResistantTunnel(ResponseTx<(), settings::Error>, QuantumResistantState), /// Set DNS options or servers to use SetDnsOptions(ResponseTx<(), settings::Error>, DnsOptions), + /// Set override options to use for a given relay + SetRelayOverride(ResponseTx<(), settings::Error>, RelayOverride), + /// Remove all relay override options + ClearAllRelayOverrides(ResponseTx<(), settings::Error>), /// Toggle macOS network check leak /// Set MTU for wireguard tunnels SetWireguardMtu(ResponseTx<(), settings::Error>, Option), @@ -1077,6 +1083,10 @@ where .await } SetDnsOptions(tx, dns_servers) => self.on_set_dns_options(tx, dns_servers).await, + SetRelayOverride(tx, relay_override) => { + self.on_set_relay_override(tx, relay_override).await + } + ClearAllRelayOverrides(tx) => self.on_clear_all_relay_overrides(tx).await, SetWireguardMtu(tx, mtu) => self.on_set_wireguard_mtu(tx, mtu).await, SetWireguardRotationInterval(tx, interval) => { self.on_set_wireguard_rotation_interval(tx, interval).await @@ -2163,6 +2173,56 @@ where } } + async fn on_set_relay_override( + &mut self, + tx: ResponseTx<(), settings::Error>, + relay_override: RelayOverride, + ) { + match self + .settings + .update(move |settings| settings.set_relay_override(relay_override)) + .await + { + Ok(settings_changed) => { + Self::oneshot_send(tx, Ok(()), "set_relay_override response"); + if settings_changed { + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector + .set_config(new_selector_config(&self.settings)); + self.reconnect_tunnel(); + } + } + Err(e) => { + log::error!("{}", e.display_chain_with_msg("Unable to save settings")); + Self::oneshot_send(tx, Err(e), "set_relay_override response"); + } + } + } + + async fn on_clear_all_relay_overrides(&mut self, tx: ResponseTx<(), settings::Error>) { + match self + .settings + .update(move |settings| settings.relay_overrides.clear()) + .await + { + Ok(settings_changed) => { + Self::oneshot_send(tx, Ok(()), "clear_all_relay_overrides response"); + if settings_changed { + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector + .set_config(new_selector_config(&self.settings)); + self.reconnect_tunnel(); + } + } + Err(e) => { + log::error!("{}", e.display_chain_with_msg("Unable to save settings")); + Self::oneshot_send(tx, Err(e), "clear_all_relay_overrides response"); + } + } + } + async fn on_set_wireguard_mtu( &mut self, tx: ResponseTx<(), settings::Error>, diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 79466d887869..e67a02117ce3 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -13,7 +13,9 @@ use mullvad_paths; use mullvad_types::settings::DnsOptions; use mullvad_types::{ account::AccountToken, - relay_constraints::{BridgeSettings, BridgeState, ObfuscationSettings, RelaySettings}, + relay_constraints::{ + BridgeSettings, BridgeState, ObfuscationSettings, RelayOverride, RelaySettings, + }, relay_list::RelayList, settings::Settings, states::{TargetState, TunnelState}, @@ -379,6 +381,31 @@ impl ManagementService for ManagementServiceImpl { Ok(Response::new(())) } + async fn set_relay_override( + &self, + request: Request, + ) -> ServiceResult<()> { + let relay_override = + RelayOverride::try_from(request.into_inner()).map_err(map_protobuf_type_err)?; + log::debug!("set_relay_override"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetRelayOverride(tx, relay_override))?; + self.wait_for_result(rx) + .await? + .map(Response::new) + .map_err(map_settings_error) + } + + async fn clear_all_relay_overrides(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("clear_all_relay_overrides"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::ClearAllRelayOverrides(tx))?; + self.wait_for_result(rx) + .await? + .map(Response::new) + .map_err(map_settings_error) + } + // Account management // diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 17854b69563b..64d87d37effd 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -46,6 +46,8 @@ service ManagementService { rpc SetEnableIpv6(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetQuantumResistantTunnel(QuantumResistantState) returns (google.protobuf.Empty) {} rpc SetDnsOptions(DnsOptions) returns (google.protobuf.Empty) {} + rpc SetRelayOverride(RelayOverride) returns (google.protobuf.Empty) {} + rpc ClearAllRelayOverrides(google.protobuf.Empty) returns (google.protobuf.Empty) {} // Account management rpc CreateNewAccount(google.protobuf.Empty) returns (google.protobuf.StringValue) {} diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index 219d0940bae8..e03673489682 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -1008,3 +1008,17 @@ pub struct RelayOverride { /// IPv6 address to use instead of the default pub ipv6_addr_in: Option, } + +impl RelayOverride { + pub fn empty(hostname: Hostname) -> RelayOverride { + RelayOverride { + hostname, + ipv4_addr_in: None, + ipv6_addr_in: None, + } + } + + pub fn is_empty(&self) -> bool { + self == &Self::empty(self.hostname.clone()) + } +} diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 2ed2d793946e..612e35348731 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -175,6 +175,24 @@ impl Settings { self.relay_settings = new_settings; } } + + pub fn set_relay_override(&mut self, relay_override: RelayOverride) { + let existing_override = self + .relay_overrides + .iter_mut() + .enumerate() + .find(|(_, elem)| elem.hostname == relay_override.hostname); + match existing_override { + None => self.relay_overrides.push(relay_override), + Some((index, elem)) => { + if relay_override.is_empty() { + self.relay_overrides.swap_remove(index); + } else { + *elem = relay_override; + } + } + } + } } /// TunnelOptions holds configuration data that applies to all kinds of tunnels. From bb7127eec1ba956086e99805a96506f1d6c48d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 8 Nov 2023 17:23:57 +0100 Subject: [PATCH 05/10] Add relay override commands to wrapper client --- mullvad-management-interface/src/client.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index 1c15a169cdd6..140eddc08a75 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -8,7 +8,9 @@ use mullvad_types::{ custom_list::{CustomList, Id}, device::{Device, DeviceEvent, DeviceId, DeviceState, RemoveDeviceEvent}, location::GeoIpLocation, - relay_constraints::{BridgeSettings, BridgeState, ObfuscationSettings, RelaySettings}, + relay_constraints::{ + BridgeSettings, BridgeState, ObfuscationSettings, RelayOverride, RelaySettings, + }, relay_list::RelayList, settings::{DnsOptions, Settings}, states::TunnelState, @@ -339,6 +341,23 @@ impl MullvadProxyClient { Ok(()) } + pub async fn set_relay_override(&mut self, relay_override: RelayOverride) -> Result<()> { + let r#override = types::RelayOverride::from(relay_override); + self.0 + .set_relay_override(r#override) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn clear_all_relay_overrides(&mut self) -> Result<()> { + self.0 + .clear_all_relay_overrides(()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + pub async fn create_new_account(&mut self) -> Result { Ok(self .0 From 38d7f614aa7baaa50fa0a4e5712aec54f730f142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Thu, 9 Nov 2023 09:21:24 +0100 Subject: [PATCH 06/10] Log warnings when overrides are set for unknown hostnames --- mullvad-relay-selector/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index c3290363dd13..1289ab332f70 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -172,8 +172,10 @@ impl ParsedRelays { .insert(r#override.hostname.to_owned(), r#override); } + let mut remaining_overrides = self.overrides.clone(); + for relay in &mut self.relays { - if let Some(overrides) = self.overrides.get(&relay.hostname) { + if let Some(overrides) = remaining_overrides.remove(&relay.hostname) { if let Some(ipv4_addr_in) = overrides.ipv4_addr_in { log::debug!( "Overriding ipv4_addr_in for {}: {ipv4_addr_in}", @@ -190,6 +192,13 @@ impl ParsedRelays { } } } + + for relay_override in remaining_overrides.into_values() { + log::warn!( + "No overrides were applied for hostname \"{}\": not found", + relay_override.hostname + ); + } } /// Update list while keeping overrides From 1d4b66058343a9ed93d253e742e5e517fea60ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Thu, 9 Nov 2023 09:46:32 +0100 Subject: [PATCH 07/10] Add CLI for relay overrides --- mullvad-cli/src/cmds/mod.rs | 21 ++++ mullvad-cli/src/cmds/relay.rs | 203 +++++++++++++++++++++++++++++++++- mullvad-cli/src/cmds/reset.rs | 32 ++---- 3 files changed, 227 insertions(+), 29 deletions(-) diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 88e4184f0729..9001bcf70d88 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -1,4 +1,5 @@ use clap::builder::{PossibleValuesParser, TypedValueParser, ValueParser}; +use std::io::stdin; use std::ops::Deref; pub mod account; @@ -80,3 +81,23 @@ impl std::fmt::Display for BooleanOption { } } } + +async fn receive_confirmation(msg: &'static str, default: bool) -> bool { + println!("{}", msg); + + tokio::task::spawn_blocking(move || loop { + let mut buf = String::new(); + if let Err(e) = stdin().read_line(&mut buf) { + eprintln!("Couldn't read from STDIN: {e}"); + return false; + } + match buf.trim().to_ascii_lowercase().as_str() { + "" => return default, + "y" | "ye" | "yes" => return true, + "n" | "no" => return false, + _ => eprintln!("Unexpected response. Please enter \"y\" or \"n\""), + } + }) + .await + .unwrap() +} diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 6d1b4871267b..69d2a93080d6 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -3,16 +3,17 @@ use clap::Subcommand; use itertools::Itertools; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ - location::Location, + location::{CountryCode, Location}, relay_constraints::{ Constraint, GeographicLocationConstraint, LocationConstraint, LocationConstraintFormatter, - Match, OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelaySettings, - TransportPort, WireguardConstraints, + Match, OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelayOverride, + RelaySettings, TransportPort, WireguardConstraints, }, relay_list::{RelayEndpointData, RelayListCountry}, ConnectionConfig, CustomTunnelEndpoint, }; use std::{ + collections::HashMap, io::BufRead, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, }; @@ -21,7 +22,7 @@ use talpid_types::net::{ }; use super::{relay_constraints::LocationArgs, BooleanOption}; -use crate::print_option; +use crate::{cmds::receive_confirmation, print_option}; #[derive(Subcommand, Debug)] pub enum Relay { @@ -37,6 +38,10 @@ pub enum Relay { /// Update the relay list Update, + + /// Override options for individual relays/servers + #[clap(subcommand)] + Override(OverrideCommands), } #[derive(Subcommand, Debug, Clone)] @@ -201,6 +206,50 @@ pub enum SetCustomCommands { }, } +#[derive(Subcommand, Debug, Clone)] +pub enum OverrideCommands { + /// Show current custom fields for servers + Get, + /// Set a custom field for a server + #[clap(subcommand)] + Set(OverrideSetCommands), + /// Unset a custom field for a server + #[clap(subcommand)] + Unset(OverrideUnsetCommands), + /// Unset custom IPs for all servers + ClearAll { + /// Clear overrides without asking for confirmation + #[arg(long, short = 'y', default_value_t = false)] + confirm: bool, + }, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum OverrideSetCommands { + /// Override entry IPv4 address for a given relay + Ipv4 { + /// The unique hostname for the server to set the override on + hostname: String, + /// The IPv4 address to use to connect to this server + address: Ipv4Addr, + }, + /// Override entry IPv6 address for a given relay + Ipv6 { + /// The unique hostname for the server to set the override on + hostname: String, + /// The IPv6 address to use to connect to this server + address: Ipv6Addr, + }, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum OverrideUnsetCommands { + /// Remove overridden entry IPv4 address for the given server + Ipv4 { hostname: String }, + /// Remove overridden entry IPv6 address for the given server + Ipv6 { hostname: String }, +} + impl Relay { pub async fn handle(self) -> Result<()> { match self { @@ -208,6 +257,7 @@ impl Relay { Relay::List => Self::list().await, Relay::Update => Self::update().await, Relay::Set(subcmd) => Self::set(subcmd).await, + Relay::Override(subcmd) => Self::r#override(subcmd).await, } } @@ -662,6 +712,151 @@ impl Relay { }) .await } + + async fn update_override( + hostname: &str, + update_fn: impl FnOnce(&mut RelayOverride), + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + + let mut relay_overrides = settings.relay_overrides; + let mut element = relay_overrides + .iter() + .position(|elem| elem.hostname == hostname) + .map(|index| relay_overrides.swap_remove(index)) + .unwrap_or_else(|| RelayOverride::empty(hostname.to_owned())); + update_fn(&mut element); + + rpc.set_relay_override(element).await?; + println!("Updated override options for {hostname}"); + Ok(()) + } + + async fn r#override(subcmd: OverrideCommands) -> Result<()> { + match subcmd { + OverrideCommands::Get => { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + + let mut overrides = HashMap::new(); + for relay_override in settings.relay_overrides { + overrides.insert(relay_override.hostname.clone(), relay_override); + } + + struct Country { + name: String, + code: CountryCode, + cities: Vec, + } + struct City { + name: String, + code: CountryCode, + overrides: Vec, + } + + let mut countries_with_overrides = vec![]; + for country in get_filtered_relays().await? { + let mut country_with_overrides = Country { + name: country.name, + code: country.code, + cities: vec![], + }; + + for city in country.cities { + let mut city_with_overrides = City { + name: city.name, + code: city.code, + overrides: vec![], + }; + + for relay in city.relays { + if let Some(relay_override) = overrides.remove(&relay.hostname) { + city_with_overrides.overrides.push(relay_override); + } + } + + if !city_with_overrides.overrides.is_empty() { + country_with_overrides.cities.push(city_with_overrides); + } + } + + if !country_with_overrides.cities.is_empty() { + countries_with_overrides.push(country_with_overrides); + } + } + + let print_relay_override = |relay_override: RelayOverride| { + println!("{:<8}{}:", " ", relay_override.hostname); + if let Some(ipv4) = relay_override.ipv4_addr_in { + println!("{:<12}ipv4: {ipv4}", " "); + } + if let Some(ipv6) = relay_override.ipv6_addr_in { + println!("{:<12}ipv6: {ipv6}", " "); + } + }; + + for country in countries_with_overrides { + println!("{} ({})", country.name, country.code); + for city in country.cities { + println!("{:<4}{} ({})", " ", city.name, city.code); + for relay_override in city.overrides { + print_relay_override(relay_override); + } + } + } + + if !overrides.is_empty() { + println!("Overrides for unrecognized servers. Consider removing these!"); + for relay_override in overrides.into_values() { + print_relay_override(relay_override); + } + } + } + OverrideCommands::Set(set_cmds) => match set_cmds { + OverrideSetCommands::Ipv4 { hostname, address } => { + Self::update_override(&hostname, |relay_override| { + relay_override.ipv4_addr_in = Some(address) + }) + .await?; + } + OverrideSetCommands::Ipv6 { hostname, address } => { + Self::update_override(&hostname, |relay_override| { + relay_override.ipv6_addr_in = Some(address) + }) + .await?; + } + }, + OverrideCommands::Unset(cmds) => match cmds { + OverrideUnsetCommands::Ipv4 { hostname } => { + Self::update_override(&hostname, |relay_override| { + let _ = relay_override.ipv4_addr_in.take(); + }) + .await?; + } + OverrideUnsetCommands::Ipv6 { hostname } => { + Self::update_override(&hostname, |relay_override| { + let _ = relay_override.ipv6_addr_in.take(); + }) + .await?; + } + }, + OverrideCommands::ClearAll { confirm } => { + if confirm + || receive_confirmation( + "Are you sure you want to clear all overrides? [Y/n]", + true, + ) + .await + { + let mut rpc = MullvadProxyClient::new().await?; + rpc.clear_all_relay_overrides().await?; + println!("All overrides unset"); + } + } + } + Ok(()) + } } fn parse_transport_port( diff --git a/mullvad-cli/src/cmds/reset.rs b/mullvad-cli/src/cmds/reset.rs index a8c275a0427d..74c5df34f275 100644 --- a/mullvad-cli/src/cmds/reset.rs +++ b/mullvad-cli/src/cmds/reset.rs @@ -1,32 +1,14 @@ +use super::receive_confirmation; use anyhow::Result; use mullvad_management_interface::MullvadProxyClient; -use std::io::stdin; pub async fn handle() -> Result<()> { - if receive_confirmation().await { - let mut rpc = MullvadProxyClient::new().await?; - rpc.factory_reset().await?; - #[cfg(target_os = "linux")] - println!("If you're running systemd, to remove all logs, you must use journalctl"); + if !receive_confirmation("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [y/N]", false).await { + return Ok(()); } + let mut rpc = MullvadProxyClient::new().await?; + rpc.factory_reset().await?; + #[cfg(target_os = "linux")] + println!("If you're running systemd, to remove all logs, you must use journalctl"); Ok(()) } - -async fn receive_confirmation() -> bool { - println!("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [Yes/No (default)]"); - - tokio::task::spawn_blocking(|| loop { - let mut buf = String::new(); - if let Err(e) = stdin().read_line(&mut buf) { - eprintln!("Couldn't read from STDIN: {e}"); - return false; - } - match buf.trim() { - "Yes" => return true, - "No" | "no" | "" => return false, - _ => eprintln!("Unexpected response. Please enter \"Yes\" or \"No\""), - } - }) - .await - .unwrap() -} From 14968821c3a0dda206833954db49d3cfed27946f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Thu, 9 Nov 2023 15:18:43 +0100 Subject: [PATCH 08/10] Warn if setting relay overrides for non-existent hostname in the CLI --- mullvad-cli/src/cmds/mod.rs | 7 ++++- mullvad-cli/src/cmds/relay.rs | 55 ++++++++++++++++++++++++----------- mullvad-cli/src/cmds/reset.rs | 2 +- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 9001bcf70d88..43d224233ee7 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -83,7 +83,12 @@ impl std::fmt::Display for BooleanOption { } async fn receive_confirmation(msg: &'static str, default: bool) -> bool { - println!("{}", msg); + let helper_str = match default { + true => "[Y/n]", + false => "[y/N]", + }; + + println!("{msg} {helper_str}"); tokio::task::spawn_blocking(move || loop { let mut buf = String::new(); diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 69d2a93080d6..273b6f3395da 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -716,10 +716,18 @@ impl Relay { async fn update_override( hostname: &str, update_fn: impl FnOnce(&mut RelayOverride), + warn_non_existent_hostname: bool, ) -> Result<()> { let mut rpc = MullvadProxyClient::new().await?; let settings = rpc.get_settings().await?; + if warn_non_existent_hostname { + let countries = get_filtered_relays_with_client(&mut rpc).await?; + if find_relay_by_hostname(&countries, hostname).is_none() { + eprintln!("Warning: Setting overrides for an unrecognized server"); + } + } + let mut relay_overrides = settings.relay_overrides; let mut element = relay_overrides .iter() @@ -815,39 +823,44 @@ impl Relay { } OverrideCommands::Set(set_cmds) => match set_cmds { OverrideSetCommands::Ipv4 { hostname, address } => { - Self::update_override(&hostname, |relay_override| { - relay_override.ipv4_addr_in = Some(address) - }) + Self::update_override( + &hostname, + |relay_override| relay_override.ipv4_addr_in = Some(address), + true, + ) .await?; } OverrideSetCommands::Ipv6 { hostname, address } => { - Self::update_override(&hostname, |relay_override| { - relay_override.ipv6_addr_in = Some(address) - }) + Self::update_override( + &hostname, + |relay_override| relay_override.ipv6_addr_in = Some(address), + true, + ) .await?; } }, OverrideCommands::Unset(cmds) => match cmds { OverrideUnsetCommands::Ipv4 { hostname } => { - Self::update_override(&hostname, |relay_override| { - let _ = relay_override.ipv4_addr_in.take(); - }) + Self::update_override( + &hostname, + |relay_override| relay_override.ipv4_addr_in = None, + false, + ) .await?; } OverrideUnsetCommands::Ipv6 { hostname } => { - Self::update_override(&hostname, |relay_override| { - let _ = relay_override.ipv6_addr_in.take(); - }) + Self::update_override( + &hostname, + |relay_override| relay_override.ipv6_addr_in = None, + false, + ) .await?; } }, OverrideCommands::ClearAll { confirm } => { if confirm - || receive_confirmation( - "Are you sure you want to clear all overrides? [Y/n]", - true, - ) - .await + || receive_confirmation("Are you sure you want to clear all overrides?", true) + .await { let mut rpc = MullvadProxyClient::new().await?; rpc.clear_all_relay_overrides().await?; @@ -909,8 +922,16 @@ pub fn find_relay_by_hostname( }) } +/// Return a list of all active non-bridge relays pub async fn get_filtered_relays() -> Result> { let mut rpc = MullvadProxyClient::new().await?; + get_filtered_relays_with_client(&mut rpc).await +} + +/// Return a list of all active non-bridge relays +async fn get_filtered_relays_with_client( + rpc: &mut MullvadProxyClient, +) -> Result> { let relay_list = rpc.get_relay_locations().await?; let mut countries = vec![]; diff --git a/mullvad-cli/src/cmds/reset.rs b/mullvad-cli/src/cmds/reset.rs index 74c5df34f275..88005c463113 100644 --- a/mullvad-cli/src/cmds/reset.rs +++ b/mullvad-cli/src/cmds/reset.rs @@ -3,7 +3,7 @@ use anyhow::Result; use mullvad_management_interface::MullvadProxyClient; pub async fn handle() -> Result<()> { - if !receive_confirmation("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [y/N]", false).await { + if !receive_confirmation("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service?", false).await { return Ok(()); } let mut rpc = MullvadProxyClient::new().await?; From 8f3f499854d764ee9b575e00f622d552c56c6691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Fri, 10 Nov 2023 13:06:15 +0100 Subject: [PATCH 09/10] Simplify relay list parsing and flattening --- mullvad-relay-selector/src/lib.rs | 295 ++++++++++++------------- mullvad-relay-selector/src/matcher.rs | 11 +- mullvad-relay-selector/src/updater.rs | 14 +- mullvad-types/src/relay_constraints.rs | 17 ++ mullvad-types/src/relay_list.rs | 8 + 5 files changed, 174 insertions(+), 171 deletions(-) diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index 1289ab332f70..7c8583493c3c 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -6,7 +6,7 @@ use ipnetwork::IpNetwork; use mullvad_types::{ custom_list::CustomListsSettings, endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, - location::{Coordinates, Hostname, Location}, + location::{Coordinates, Location}, relay_constraints::{ BridgeSettings, BridgeState, Constraint, InternalBridgeConstraints, LocationConstraint, Match, ObfuscationSettings, OpenVpnConstraints, Ownership, Providers, RelayConstraints, @@ -80,63 +80,80 @@ pub enum Error { struct ParsedRelays { last_updated: SystemTime, - locations: RelayList, - relays: Vec, - overrides: HashMap, + parsed_list: RelayList, + original_list: RelayList, + overrides: Vec, } impl ParsedRelays { - pub fn empty() -> Self { - ParsedRelays { - last_updated: time::UNIX_EPOCH, - locations: RelayList::empty(), - relays: Vec::new(), - overrides: HashMap::new(), - } + /// Return a flat iterator with all relays + pub fn relays(&self) -> impl Iterator + Clone + '_ { + self.parsed_list.relays() } - pub fn from_relay_list(mut relay_list: RelayList, last_updated: SystemTime) -> Self { - // Append data for obfuscation protocols ourselves, since the API does not provide it. - if relay_list.wireguard.udp2tcp_ports.is_empty() { - relay_list.wireguard.udp2tcp_ports.extend(UDP2TCP_PORTS); - } + pub fn update(&mut self, new_relays: RelayList) { + *self = Self::from_relay_list(new_relays, SystemTime::now(), &self.overrides); + + log::info!( + "Updated relay inventory has {} relays", + self.relays().count() + ); + } + + pub fn last_updated(&self) -> SystemTime { + self.last_updated + } + + fn set_overrides(&mut self, new_overrides: &[RelayOverride]) { + self.parsed_list = Self::parse_relay_list(&self.original_list, new_overrides); + self.overrides = new_overrides.to_vec(); + } + + fn empty() -> Self { ParsedRelays { - last_updated, - relays: Self::relays_with_location(&relay_list), - locations: relay_list, - overrides: HashMap::new(), + last_updated: time::UNIX_EPOCH, + parsed_list: RelayList::empty(), + original_list: RelayList::empty(), + overrides: vec![], } } - fn relays_with_location(relay_list: &RelayList) -> Vec { - let mut relays = vec![]; - for country in &relay_list.countries { - for city in &country.cities { - for relay in &city.relays { - let mut relay_with_location = relay.clone(); - relay_with_location.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, - }); - relays.push(relay_with_location); - } + /// Try to read the relays from disk, preferring the newer ones. + fn from_dir( + cache_path: &Path, + resource_path: &Path, + overrides: &[RelayOverride], + ) -> Result { + // 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(cache_path, overrides); + let bundled_relays = match Self::from_file(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) } - relays } - pub fn from_file(path: impl AsRef) -> Result { + fn from_file(path: impl AsRef, overrides: &[RelayOverride]) -> Result { log::debug!("Reading relays from {}", path.as_ref().display()); let (last_modified, file) = Self::open_file(path.as_ref()).map_err(Error::OpenRelayCache)?; let relay_list = serde_json::from_reader(io::BufReader::new(file)).map_err(Error::Serialize)?; - Ok(Self::from_relay_list(relay_list, last_modified)) + Ok(Self::from_relay_list(relay_list, last_modified, overrides)) } fn open_file(path: &Path) -> io::Result<(SystemTime, std::fs::File)> { @@ -145,66 +162,58 @@ impl ParsedRelays { Ok((last_modified, file)) } - pub fn last_updated(&self) -> SystemTime { - self.last_updated - } - - pub fn locations(&self) -> &RelayList { - &self.locations - } - - pub fn relays(&self) -> &Vec { - &self.relays + 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(), + } } - pub fn tag(&self) -> Option<&str> { - self.locations.etag.as_deref() - } + 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(), + ); + } - fn reset_overrides(&mut self) { - self.relays = Self::relays_with_location(&self.locations); - } + let mut parsed_list = relay_list.clone(); - fn append_overrides(&mut self, overrides: Vec) { - self.overrides.clear(); - for r#override in overrides { - self.overrides - .insert(r#override.hostname.to_owned(), r#override); + // 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); } - let mut remaining_overrides = self.overrides.clone(); + // 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, + }); - for relay in &mut self.relays { - if let Some(overrides) = remaining_overrides.remove(&relay.hostname) { - if let Some(ipv4_addr_in) = overrides.ipv4_addr_in { - log::debug!( - "Overriding ipv4_addr_in for {}: {ipv4_addr_in}", - relay.hostname - ); - relay.ipv4_addr_in = ipv4_addr_in; - } - if let Some(ipv6_addr_in) = overrides.ipv6_addr_in { - log::debug!( - "Overriding ipv6_addr_in for {}: {ipv6_addr_in}", - relay.hostname - ); - relay.ipv6_addr_in = Some(ipv6_addr_in); + // Append overrides + if let Some(overrides) = remaining_overrides.remove(&relay.hostname) { + overrides.apply_to_relay(relay); + } } } } - for relay_override in remaining_overrides.into_values() { - log::warn!( - "No overrides were applied for hostname \"{}\": not found", - relay_override.hostname - ); - } - } - - /// Update list while keeping overrides - pub fn update(&mut self, update: ParsedRelays) { - let old = std::mem::replace(self, update); - self.append_overrides(old.overrides.into_values().collect()); + parsed_list } } @@ -230,23 +239,22 @@ impl RelaySelector { pub fn new(config: SelectorConfig, resource_dir: &Path, cache_dir: &Path) -> Self { let cache_path = cache_dir.join(RELAYS_FILENAME); let resource_path = resource_dir.join(RELAYS_FILENAME); - let mut unsynchronized_parsed_relays = - Self::read_relays_from_disk(&cache_path, &resource_path).unwrap_or_else(|error| { - log::error!( - "{}", - error.display_chain_with_msg("Unable to load cached relays") - ); - ParsedRelays::empty() - }); + let unsynchronized_parsed_relays = + ParsedRelays::from_dir(&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().len(), + unsynchronized_parsed_relays.relays().count(), DateTime::::from(unsynchronized_parsed_relays.last_updated()) .format(DATE_TIME_FORMAT_STR) ); - unsynchronized_parsed_relays.append_overrides(config.relay_overrides.clone()); - RelaySelector { config: Arc::new(Mutex::new(config)), parsed_relays: Arc::new(Mutex::new(unsynchronized_parsed_relays)), @@ -255,15 +263,14 @@ impl RelaySelector { pub fn set_config(&mut self, config: SelectorConfig) { let mut parsed_relays = self.parsed_relays.lock(); - parsed_relays.reset_overrides(); - parsed_relays.append_overrides(config.relay_overrides.clone()); + parsed_relays.set_overrides(&config.relay_overrides); *self.config.lock() = config; } /// Returns all countries and cities. The cities in the object returned does not have any /// relays in them. pub fn get_locations(&mut self) -> RelayList { - self.parsed_relays.lock().locations().clone() + self.parsed_relays.lock().original_list.clone() } /// Returns a random relay and relay endpoint matching the current constraints. @@ -367,8 +374,8 @@ impl RelaySelector { let (openvpn_data, wireguard_data) = { let relays = self.parsed_relays.lock(); ( - relays.locations.openvpn.clone(), - relays.locations.wireguard.clone(), + relays.parsed_list.openvpn.clone(), + relays.parsed_list.wireguard.clone(), ) }; @@ -411,7 +418,7 @@ impl RelaySelector { ownership: relay_constraints.ownership, endpoint_matcher: OpenVpnMatcher::new( relay_constraints.openvpn_constraints, - self.parsed_relays.lock().locations.openvpn.clone(), + self.parsed_relays.lock().parsed_list.openvpn.clone(), ), }; @@ -519,7 +526,7 @@ impl RelaySelector { retry_attempt: u32, custom_lists: &CustomListsSettings, ) -> Result { - let wg_endpoint_data = self.parsed_relays.lock().locations.wireguard.clone(); + let wg_endpoint_data = self.parsed_relays.lock().parsed_list.wireguard.clone(); // NOTE: If not using multihop then `location` is set as the only location constraint. // If using multihop then location is the exit constraint and @@ -586,8 +593,8 @@ impl RelaySelector { let (openvpn_data, wireguard_data) = { let relays = self.parsed_relays.lock(); ( - relays.locations.openvpn.clone(), - relays.locations.wireguard.clone(), + relays.parsed_list.openvpn.clone(), + relays.parsed_list.wireguard.clone(), ) }; let mut matcher = RelayMatcher::new( @@ -976,7 +983,7 @@ impl RelaySelector { self.pick_random_relay(&matching_relays).cloned() }; relay.and_then(|relay| { - self.pick_random_bridge(&self.parsed_relays.lock().locations.bridge, &relay) + self.pick_random_bridge(&self.parsed_relays.lock().parsed_list.bridge, &relay) .map(|bridge| (bridge, relay.clone())) }) } @@ -1049,7 +1056,12 @@ impl RelaySelector { endpoint: &MullvadWireguardEndpoint, retry_attempt: u32, ) -> Option { - let udp2tcp_ports = &self.parsed_relays.lock().locations.wireguard.udp2tcp_ports; + let udp2tcp_ports = &self + .parsed_relays + .lock() + .parsed_list + .wireguard + .udp2tcp_ports; let udp2tcp_endpoint = if obfuscation_settings.port.is_only() { udp2tcp_ports .iter() @@ -1079,14 +1091,13 @@ impl RelaySelector { ) -> (Constraint, TransportProtocol, TunnelType) { match default_tunnel_type { TunnelType::OpenVpn => { - let location_supports_openvpn = - self.parsed_relays.lock().relays().iter().any(|relay| { - relay.active - && relay.endpoint_data == RelayEndpointData::Openvpn - && location_constraint.matches_with_opts(relay, true) - && providers_constraint.matches(relay) - && ownership_constraint.matches(relay) - }); + let location_supports_openvpn = self.parsed_relays.lock().relays().any(|relay| { + relay.active + && relay.endpoint_data == RelayEndpointData::Openvpn + && location_constraint.matches_with_opts(relay, true) + && providers_constraint.matches(relay) + && ownership_constraint.matches(relay) + }); if location_supports_openvpn { let (preferred_port, preferred_protocol) = @@ -1095,14 +1106,13 @@ impl RelaySelector { } } TunnelType::Wireguard => { - let location_supports_wireguard = - self.parsed_relays.lock().relays().iter().any(|relay| { - relay.active - && matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_)) - && location_constraint.matches_with_opts(relay, true) - && providers_constraint.matches(relay) - && ownership_constraint.matches(relay) - }); + let location_supports_wireguard = self.parsed_relays.lock().relays().any(|relay| { + relay.active + && matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_)) + && location_constraint.matches_with_opts(relay, true) + && providers_constraint.matches(relay) + && ownership_constraint.matches(relay) + }); // If location does not support WireGuard, defer to preferred OpenVPN tunnel // constraints @@ -1249,36 +1259,10 @@ impl RelaySelector { }) } - /// Try to read the relays from disk, preferring the newer ones. - fn read_relays_from_disk( - cache_path: &Path, - resource_path: &Path, - ) -> Result { - // 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 = ParsedRelays::from_file(cache_path); - let bundled_relays = match ParsedRelays::from_file(resource_path) { - 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 wireguard_exit_matcher(&self) -> WireguardMatcher { - let mut tunnel = - WireguardMatcher::from_endpoint(self.parsed_relays.lock().locations.wireguard.clone()); + let mut tunnel = WireguardMatcher::from_endpoint( + self.parsed_relays.lock().parsed_list.wireguard.clone(), + ); tunnel.ip_version = WIREGUARD_EXIT_IP_VERSION; tunnel.port = WIREGUARD_EXIT_PORT; tunnel @@ -1510,6 +1494,7 @@ mod test { parsed_relays: Arc::new(Mutex::new(ParsedRelays::from_relay_list( relay_list, SystemTime::now(), + &[], ))), config: Arc::new(Mutex::new(SelectorConfig { relay_settings: RelaySettings::Normal(RelayConstraints { diff --git a/mullvad-relay-selector/src/matcher.rs b/mullvad-relay-selector/src/matcher.rs index 05b3799dd84e..e02d8abc450b 100644 --- a/mullvad-relay-selector/src/matcher.rs +++ b/mullvad-relay-selector/src/matcher.rs @@ -67,13 +67,12 @@ impl RelayMatcher { impl RelayMatcher { /// 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(&self, relays: &[Relay]) -> Vec { - let matches = relays - .iter() - .filter(|relay| self.pre_filter_matching_relay(relay)); - + pub fn filter_matching_relay_list<'a, R: Iterator + Clone>( + &self, + relays: R, + ) -> Vec { + 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() diff --git a/mullvad-relay-selector/src/updater.rs b/mullvad-relay-selector/src/updater.rs index ff15e7c793b5..e9d65d1dd8cb 100644 --- a/mullvad-relay-selector/src/updater.rs +++ b/mullvad-relay-selector/src/updater.rs @@ -91,7 +91,7 @@ impl RelayListUpdater { futures::select! { _check_update = next_check => { if download_future.is_terminated() && self.should_update() { - let tag = self.parsed_relays.lock().tag().map(|tag| tag.to_string()); + let tag = self.parsed_relays.lock().parsed_list.etag.clone(); download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse()); self.last_check = SystemTime::now(); } @@ -104,7 +104,7 @@ impl RelayListUpdater { cmd = cmd_rx.next() => { match cmd { Some(()) => { - let tag = self.parsed_relays.lock().tag().map(|tag| tag.to_string()); + let tag = self.parsed_relays.lock().parsed_list.etag.clone(); download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.api_client.clone(), tag).fuse()); self.last_check = SystemTime::now(); }, @@ -178,15 +178,9 @@ impl RelayListUpdater { ); } - let new_parsed_relays = ParsedRelays::from_relay_list(new_relay_list, SystemTime::now()); - log::info!( - "Downloaded relay inventory has {} relays", - new_parsed_relays.relays().len() - ); - let mut parsed_relays = self.parsed_relays.lock(); - parsed_relays.update(new_parsed_relays); - (self.on_update)(parsed_relays.locations()); + parsed_relays.update(new_relay_list); + (self.on_update)(&parsed_relays.original_list); Ok(()) } diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index e03673489682..6645fb493d21 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -1021,4 +1021,21 @@ impl RelayOverride { pub fn is_empty(&self) -> bool { self == &Self::empty(self.hostname.clone()) } + + pub fn apply_to_relay(&self, relay: &mut Relay) { + if let Some(ipv4_addr_in) = self.ipv4_addr_in { + log::debug!( + "Overriding ipv4_addr_in for {}: {ipv4_addr_in}", + relay.hostname + ); + relay.ipv4_addr_in = ipv4_addr_in; + } + if let Some(ipv6_addr_in) = self.ipv6_addr_in { + log::debug!( + "Overriding ipv6_addr_in for {}: {ipv6_addr_in}", + relay.hostname + ); + relay.ipv6_addr_in = Some(ipv6_addr_in); + } + } } diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 4b1c8df34682..94f4a9d59d8d 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -35,6 +35,14 @@ impl RelayList { .iter() .find(|country| country.code == country_code) } + + /// Return a flat iterator with all relays + pub fn relays(&self) -> impl Iterator + Clone + '_ { + self.countries + .iter() + .flat_map(|country| country.cities.iter()) + .flat_map(|city| city.relays.iter()) + } } /// A list of [`RelayListCity`]s within a country. Used by [`RelayList`]. From 086cf83be3c69ebe2bb0eec26445733888f76d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Thu, 9 Nov 2023 15:29:20 +0100 Subject: [PATCH 10/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b208bd3e117..0a4af7319cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Line wrap the file at 100 chars. Th `mullvad api-access`, and the initially supported network protocols are `Shadowsocks` and `SOCKS5`. - Add social media content blocker. +- Add ability to override server IPs to the CLI. ### Changed - Update Electron from 25.2.0 to 26.3.0.