diff options
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 26 | ||||
| -rw-r--r-- | mullvad-daemon/src/relays/matcher.rs | 313 | ||||
| -rw-r--r-- | mullvad-daemon/src/relays/mod.rs (renamed from mullvad-daemon/src/relays.rs) | 1046 | ||||
| -rw-r--r-- | mullvad-daemon/src/relays/updater.rs | 211 | ||||
| -rw-r--r-- | mullvad-types/src/endpoint.rs | 31 | ||||
| -rw-r--r-- | mullvad-types/src/relay_constraints.rs | 62 |
6 files changed, 1001 insertions, 688 deletions
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index b2b3468a54..b54eae5908 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -1008,16 +1008,21 @@ where self.settings.get_wireguard().is_some(), ) .ok(); - if let Some((relay, entry_relay, endpoint)) = endpoint { + if let Some(relays::RelaySelectorResult { + exit_relay, + entry_relay, + endpoint, + }) = endpoint + { let result = self .create_tunnel_parameters( - &relay, + &exit_relay, endpoint, account_token, retry_attempt, ) .await; - self.last_generated_relay = Some(relay); + self.last_generated_relay = Some(exit_relay); self.last_generated_entry_relay = entry_relay; match result { Ok(result) => Ok(result), @@ -1123,12 +1128,7 @@ where } .into()) } - MullvadEndpoint::Wireguard { - peer, - exit_peer, - ipv4_gateway, - ipv6_gateway, - } => { + MullvadEndpoint::Wireguard(endpoint) => { let wg_data = self.settings.get_wireguard().ok_or(Error::NoKeyAvailable)?; let tunnel = wireguard::TunnelConfig { private_key: wg_data.private_key, @@ -1140,10 +1140,10 @@ where Ok(wireguard::TunnelParameters { connection: wireguard::ConnectionConfig { tunnel, - peer, - exit_peer, - ipv4_gateway, - ipv6_gateway: Some(ipv6_gateway), + peer: endpoint.peer, + exit_peer: endpoint.exit_peer, + ipv4_gateway: endpoint.ipv4_gateway, + ipv6_gateway: Some(endpoint.ipv6_gateway), }, options: tunnel_options.wireguard.options, generic_options: tunnel_options.generic, diff --git a/mullvad-daemon/src/relays/matcher.rs b/mullvad-daemon/src/relays/matcher.rs new file mode 100644 index 0000000000..08924a78e9 --- /dev/null +++ b/mullvad-daemon/src/relays/matcher.rs @@ -0,0 +1,313 @@ +use mullvad_types::{ + endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, + relay_constraints::{ + Constraint, LocationConstraint, Match, OpenVpnConstraints, Providers, RelayConstraints, + TransportPort, WireguardConstraints, + }, + relay_list::{Relay, RelayTunnels, WireguardEndpointData}, +}; +use rand::{seq::SliceRandom, Rng}; +use std::net::{IpAddr, SocketAddr}; +use talpid_types::net::{all_of_the_internet, wireguard, IpVersion, TransportProtocol, TunnelType}; + +#[derive(Clone)] +pub struct RelayMatcher<T: TunnelMatcher> { + pub location: Constraint<LocationConstraint>, + pub providers: Constraint<Providers>, + pub tunnel: T, +} + +impl From<RelayConstraints> for RelayMatcher<AnyTunnelMatcher> { + fn from(constraints: RelayConstraints) -> Self { + Self { + location: constraints.location, + providers: constraints.providers, + tunnel: AnyTunnelMatcher { + wireguard: constraints.wireguard_constraints.into(), + openvpn: constraints.openvpn_constraints, + tunnel_type: constraints.tunnel_protocol, + }, + } + } +} + +impl RelayMatcher<AnyTunnelMatcher> { + pub fn to_wireguard_matcher(self) -> RelayMatcher<WireguardMatcher> { + RelayMatcher { + tunnel: self.tunnel.wireguard, + location: self.location, + providers: self.providers, + } + } +} + +impl RelayMatcher<WireguardMatcher> { + pub fn set_peer(&mut self, peer: Relay) { + self.tunnel.peer = Some(peer); + } +} + +impl<T: TunnelMatcher> RelayMatcher<T> { + /// Filter a relay and its endpoints based on constraints. + /// Only matching endpoints are included in the returned Relay. + pub fn filter_matching_relay(&self, relay: &Relay) -> Option<Relay> { + if !self.location.matches(relay) || !self.providers.matches(relay) { + return None; + } + + self.tunnel.filter_matching_endpoints(relay) + } + + pub fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { + self.tunnel.mullvad_endpoint(relay) + } +} + +/// TunnelMatcher allows to abstract over different tunnel-specific constraints, +/// as to not have false dependencies on OpenVpn specific constraints when +/// selecting only WireGuard tunnels. +pub trait TunnelMatcher: Clone { + /// Filter a relay and its endpoints based on constraints. + /// Only matching endpoints are included in the returned Relay. + fn filter_matching_endpoints(&self, relay: &Relay) -> Option<Relay>; + /// Constructs a MullvadEndpoint for a given Relay using extra data from the relay matcher + /// itself. + fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint>; +} + +impl TunnelMatcher for OpenVpnMatcher { + fn filter_matching_endpoints(&self, relay: &Relay) -> Option<Relay> { + let tunnels = relay + .tunnels + .openvpn + .iter() + .filter(|endpoint| self.matches(endpoint)) + .cloned() + .collect::<Vec<_>>(); + if tunnels.is_empty() { + return None; + } + let mut relay = relay.clone(); + relay.tunnels = RelayTunnels { + openvpn: tunnels, + wireguard: vec![], + }; + Some(relay) + } + + fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { + relay + .tunnels + .openvpn + .choose(&mut rand::thread_rng()) + .cloned() + .map(|endpoint| endpoint.into_mullvad_endpoint(relay.ipv4_addr_in.into())) + } +} + +pub type OpenVpnMatcher = OpenVpnConstraints; + +#[derive(Clone)] +pub struct AnyTunnelMatcher { + wireguard: WireguardMatcher, + openvpn: OpenVpnMatcher, + /// in the case that a user hasn't specified a tunnel protocol, the relay + /// selector might still construct preferred constraints that do select a + /// specific tunnel protocol, which is why the tunnel type may be specified + /// in the `AnyTunnelMatcher`. + tunnel_type: Constraint<TunnelType>, +} + +impl TunnelMatcher for AnyTunnelMatcher { + fn filter_matching_endpoints(&self, relay: &Relay) -> Option<Relay> { + match self.tunnel_type { + Constraint::Any => { + let wireguard_relay = self.wireguard.filter_matching_endpoints(relay); + let openvpn_relay = self.openvpn.filter_matching_endpoints(relay); + + match (wireguard_relay, openvpn_relay) { + (Some(mut matched_relay), Some(openvpn_relay)) => { + matched_relay.tunnels.openvpn = openvpn_relay.tunnels.openvpn; + Some(matched_relay) + } + (Some(relay), None) | (None, Some(relay)) => Some(relay), + _ => None, + } + } + Constraint::Only(TunnelType::OpenVpn) => self.openvpn.filter_matching_endpoints(relay), + Constraint::Only(TunnelType::Wireguard) => { + self.wireguard.filter_matching_endpoints(relay) + } + } + } + + fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { + #[cfg(not(target_os = "android"))] + match self.tunnel_type { + Constraint::Any => vec![ + self.openvpn.mullvad_endpoint(relay), + self.wireguard.mullvad_endpoint(relay), + ] + .into_iter() + .filter_map(|relay| relay) + .collect::<Vec<_>>() + .choose(&mut rand::thread_rng()) + .cloned(), + Constraint::Only(TunnelType::OpenVpn) => self.openvpn.mullvad_endpoint(relay), + Constraint::Only(TunnelType::Wireguard) => self.wireguard.mullvad_endpoint(relay), + } + + #[cfg(target_os = "android")] + self.wireguard.mullvad_endpoint(relay) + } +} + +#[derive(Clone)] +pub struct WireguardMatcher { + /// The peer is an already selected peer relay to be used with multihop. + /// It's stored here so we can exclude it from further selections being made. + pub peer: Option<Relay>, + pub port: Constraint<TransportPort>, + pub ip_version: Constraint<IpVersion>, +} + +impl WireguardMatcher { + fn wg_data_to_endpoint( + &self, + relay: &Relay, + data: WireguardEndpointData, + ) -> Option<MullvadEndpoint> { + let host = self.get_address_for_wireguard_relay(relay)?; + let port = self.get_port_for_wireguard_relay(&data)?; + let peer_config = wireguard::PeerConfig { + public_key: data.public_key, + endpoint: SocketAddr::new(host, port), + allowed_ips: all_of_the_internet(), + protocol: self + .port + .map(|port| port.protocol) + .unwrap_or(TransportProtocol::Udp), + }; + Some(MullvadEndpoint::Wireguard(MullvadWireguardEndpoint { + peer: peer_config, + exit_peer: None, + ipv4_gateway: data.ipv4_gateway, + ipv6_gateway: data.ipv6_gateway, + })) + } + + fn get_address_for_wireguard_relay(&self, relay: &Relay) -> Option<IpAddr> { + match self.ip_version { + Constraint::Any | Constraint::Only(IpVersion::V4) => Some(relay.ipv4_addr_in.into()), + Constraint::Only(IpVersion::V6) => relay.ipv6_addr_in.map(|addr| addr.into()), + } + } + + fn get_port_for_wireguard_relay(&self, data: &WireguardEndpointData) -> Option<u16> { + match self + .port + .as_ref() + .map(|port| port.port) + .unwrap_or(Constraint::Any) + { + Constraint::Any => { + let get_port_amount = + |range: &(u16, u16)| -> u64 { (1 + range.1 - range.0) as u64 }; + let port_amount: u64 = data.port_ranges.iter().map(get_port_amount).sum(); + + if port_amount < 1 { + return None; + } + + let mut port_index = rand::thread_rng().gen_range(0, port_amount); + + for range in data.port_ranges.iter() { + let ports_in_range = get_port_amount(range); + if port_index < ports_in_range { + return Some(port_index as u16 + range.0); + } + port_index -= ports_in_range; + } + log::error!("Port selection algorithm is broken!"); + None + } + Constraint::Only(port) => { + if data + .port_ranges + .iter() + .any(|range| (range.0 <= port && port <= range.1)) + { + Some(port) + } else { + None + } + } + } + } +} + +impl From<WireguardConstraints> for WireguardMatcher { + fn from(constraints: WireguardConstraints) -> Self { + Self { + peer: None, + port: constraints.port, + ip_version: constraints.ip_version, + } + } +} + +impl Match<WireguardEndpointData> for WireguardMatcher { + fn matches(&self, endpoint: &WireguardEndpointData) -> bool { + match self + .port + .as_ref() + .map(|port| port.port) + .unwrap_or(Constraint::Any) + { + Constraint::Any => true, + Constraint::Only(port) => endpoint + .port_ranges + .iter() + .any(|range| (port >= range.0 && port <= range.1)), + } + } +} + +impl TunnelMatcher for WireguardMatcher { + fn filter_matching_endpoints(&self, relay: &Relay) -> Option<Relay> { + if self + .peer + .as_ref() + .map(|peer_relay| peer_relay.hostname == relay.hostname) + .unwrap_or(false) + { + return None; + } + + let tunnels = relay + .tunnels + .wireguard + .iter() + .filter(|endpoint| self.matches(*endpoint)) + .cloned() + .collect::<Vec<_>>(); + if tunnels.is_empty() { + return None; + } + let mut relay = relay.clone(); + relay.tunnels = RelayTunnels { + wireguard: tunnels, + openvpn: vec![], + }; + Some(relay) + } + + fn mullvad_endpoint(&self, relay: &Relay) -> Option<MullvadEndpoint> { + relay + .tunnels + .wireguard + .choose(&mut rand::thread_rng()) + .cloned() + .and_then(|wg_tunnel| self.wg_data_to_endpoint(relay, wg_tunnel)) + } +} diff --git a/mullvad-daemon/src/relays.rs b/mullvad-daemon/src/relays/mod.rs index c5284a38d6..1fead05f77 100644 --- a/mullvad-daemon/src/relays.rs +++ b/mullvad-daemon/src/relays/mod.rs @@ -2,63 +2,52 @@ //! updated as well. use chrono::{DateTime, Local}; -use futures::{ - channel::mpsc, - future::{Fuse, FusedFuture}, - FutureExt, SinkExt, StreamExt, -}; use ipnetwork::IpNetwork; -use mullvad_rpc::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle, RelayListProxy}; +use mullvad_rpc::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle}; use mullvad_types::{ - endpoint::MullvadEndpoint, + endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, location::Location, relay_constraints::{ BridgeState, Constraint, InternalBridgeConstraints, LocationConstraint, Match, OpenVpnConstraints, Providers, RelayConstraints, Set, TransportPort, WireguardConstraints, }, - relay_list::{OpenVpnEndpointData, Relay, RelayList, RelayTunnels, WireguardEndpointData}, + relay_list::{Relay, RelayList, WireguardEndpointData}, }; use parking_lot::Mutex; -use rand::{self, rngs::ThreadRng, seq::SliceRandom, Rng}; +use rand::{self, seq::SliceRandom, Rng}; use std::{ - future::Future, io, - net::{IpAddr, SocketAddr}, - path::{Path, PathBuf}, + net::IpAddr, + path::Path, sync::Arc, - time::{self, Duration, Instant, SystemTime}, + time::{self, SystemTime}, }; -use talpid_core::future_retry::{retry_future, ExponentialBackoff, Jittered}; use talpid_types::{ - net::{ - all_of_the_internet, openvpn::ProxySettings, wireguard, IpVersion, TransportProtocol, - TunnelType, - }, + net::{openvpn::ProxySettings, wireguard, IpVersion, TransportProtocol, TunnelType}, ErrorExt, }; -use tokio::fs::File; + +use crate::relays::updater::RelayListUpdater; + +use self::{ + matcher::{RelayMatcher, TunnelMatcher, WireguardMatcher}, + updater::RelayListUpdaterHandle, +}; + +mod matcher; +mod updater; const DATE_TIME_FORMAT_STR: &str = "%Y-%m-%d %H:%M:%S%.3f"; const RELAYS_FILENAME: &str = "relays.json"; -/// How often the updater should wake up to check the cache of the in-memory cache of relays. -/// This check is very cheap. The only reason to not have it very often is because if downloading -/// constantly fails it will try very often and fill the logs etc. -const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 15); -/// How old the cached relays need to be to trigger an update -const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60); - -const EXPONENTIAL_BACKOFF_INITIAL: Duration = Duration::from_secs(16); -const EXPONENTIAL_BACKOFF_FACTOR: u32 = 8; const DEFAULT_WIREGUARD_PORT: u16 = 51820; -const WIREGUARD_EXIT_CONSTRAINTS: WireguardConstraints = WireguardConstraints { +const WIREGUARD_EXIT_CONSTRAINTS: WireguardMatcher = WireguardMatcher { + peer: None, port: Constraint::Only(TransportPort { protocol: TransportProtocol::Udp, port: Constraint::Only(DEFAULT_WIREGUARD_PORT), }), ip_version: Constraint::Only(IpVersion::V4), - use_multihop: false, - entry_location: Constraint::Any, }; const WIREGUARD_TCP_PORTS: [(u16, u16); 3] = [(80, 80), (443, 443), (5001, 5001)]; @@ -174,7 +163,6 @@ impl ParsedRelays { pub struct RelaySelector { parsed_relays: Arc<Mutex<ParsedRelays>>, - rng: ThreadRng, updater: Option<RelayListUpdaterHandle>, } @@ -216,7 +204,6 @@ impl RelaySelector { RelaySelector { parsed_relays, - rng: rand::thread_rng(), updater: Some(updater), } } @@ -237,114 +224,240 @@ impl RelaySelector { /// Returns a random relay and relay endpoint matching the given constraints and with /// preferences applied. pub fn get_tunnel_endpoint( - &mut self, + &self, relay_constraints: &RelayConstraints, bridge_state: BridgeState, retry_attempt: u32, wg_key_exists: bool, - ) -> Result<(Relay, Option<Relay>, MullvadEndpoint), Error> { - let mut exit_relay_constraints = relay_constraints.clone(); - let wg_entry_is_subset = if exit_relay_constraints.wireguard_constraints.use_multihop { - let use_multihop = exit_relay_constraints.wireguard_constraints.use_multihop; - let entry_location = exit_relay_constraints.wireguard_constraints.entry_location; - let is_subset = entry_location.is_subset(&exit_relay_constraints.location); - exit_relay_constraints.wireguard_constraints = WireguardConstraints { - use_multihop, - entry_location, - ..WIREGUARD_EXIT_CONSTRAINTS - }; - is_subset + ) -> Result<RelaySelectorResult, Error> { + match relay_constraints.tunnel_protocol { + Constraint::Only(TunnelType::OpenVpn) => self.get_openvpn_endpoint( + &relay_constraints.location, + &relay_constraints.providers, + relay_constraints.openvpn_constraints.clone(), + bridge_state, + retry_attempt, + ), + + Constraint::Only(TunnelType::Wireguard) => self.get_wireguard_endpoint( + &relay_constraints.location, + &relay_constraints.providers, + &relay_constraints.wireguard_constraints, + retry_attempt, + ), + Constraint::Any => self.get_any_tunnel_endpoint( + relay_constraints, + bridge_state, + retry_attempt, + wg_key_exists, + ), + } + } + + /// Returns an OpenVpn endpoint, should only ever be used when the user has specified the tunnel + /// protocol as only OpenVPN. + fn get_openvpn_endpoint( + &self, + location: &Constraint<LocationConstraint>, + providers: &Constraint<Providers>, + openvpn_constraints: OpenVpnConstraints, + bridge_state: BridgeState, + retry_attempt: u32, + ) -> Result<RelaySelectorResult, Error> { + let mut relay_matcher = RelayMatcher { + location: location.clone(), + providers: providers.clone(), + tunnel: openvpn_constraints, + }; + + if relay_matcher.tunnel.port.is_any() && bridge_state == BridgeState::On { + relay_matcher.tunnel.port = Constraint::Only(TransportPort { + protocol: TransportProtocol::Tcp, + port: Constraint::Any, + }); + + return self.get_tunnel_endpoint_internal(&relay_matcher); + } + + let mut preferred_relay_matcher = relay_matcher.clone(); + + let (preferred_port, preferred_protocol) = + Self::preferred_openvpn_constraints(retry_attempt); + let should_try_preferred = match &mut preferred_relay_matcher.tunnel.port { + any @ Constraint::Any => { + *any = Constraint::Only(TransportPort { + protocol: preferred_protocol, + port: preferred_port, + }); + true + } + Constraint::Only(ref mut port_constraints) + if port_constraints.protocol == preferred_protocol + && port_constraints.port.is_any() => + { + port_constraints.port = preferred_port; + true + } + _ => false, + }; + + if should_try_preferred { + self.get_tunnel_endpoint_internal(&preferred_relay_matcher) + .or_else(|_| self.get_tunnel_endpoint_internal(&relay_matcher)) } else { - false + self.get_tunnel_endpoint_internal(&relay_matcher) + } + } + + fn get_wireguard_multi_hop_endpoint( + &self, + mut entry_matcher: RelayMatcher<WireguardMatcher>, + exit_location: Constraint<LocationConstraint>, + ) -> Result<RelaySelectorResult, Error> { + let mut exit_matcher = RelayMatcher { + location: exit_location, + tunnel: WIREGUARD_EXIT_CONSTRAINTS.clone().into(), + ..entry_matcher.clone() }; - let entry_endpoint = - if wg_entry_is_subset && relay_constraints.wireguard_constraints.use_multihop { - self.select_entry_endpoint(None, &relay_constraints, retry_attempt) + let (exit_relay, entry_relay, exit_endpoint, mut entry_endpoint) = + if entry_matcher.location.is_subset(&exit_matcher.location) { + let (entry_relay, entry_endpoint) = self.get_entry_endpoint(&entry_matcher)?; + exit_matcher.set_peer(entry_relay.clone()); + let exit_result = self.get_tunnel_endpoint_internal(&exit_matcher)?; + ( + exit_result.exit_relay, + entry_relay, + exit_result.endpoint, + entry_endpoint, + ) } else { - None + let exit_result = self.get_tunnel_endpoint_internal(&exit_matcher)?; + + entry_matcher.set_peer(exit_result.exit_relay.clone()); + let (entry_relay, entry_endpoint) = self.get_entry_endpoint(&entry_matcher)?; + ( + exit_result.exit_relay, + entry_relay, + exit_result.endpoint, + entry_endpoint, + ) }; - let (exit_relay, mut endpoint) = self.get_tunnel_exit_endpoint( - &exit_relay_constraints, - bridge_state, - retry_attempt, - wg_key_exists, - entry_endpoint.as_ref().and_then(|(_relay, endpoint)| { - if let MullvadEndpoint::Wireguard { peer, .. } = &endpoint { - Some(peer) - } else { - None - } - }), - )?; + Self::set_entry_peers(&exit_endpoint.unwrap_wireguard().peer, &mut entry_endpoint); - let mut entry_endpoint = entry_endpoint.or_else(|| { - if !wg_entry_is_subset && relay_constraints.wireguard_constraints.use_multihop { - if let MullvadEndpoint::Wireguard { peer, .. } = &endpoint { - self.select_entry_endpoint(Some(peer), &relay_constraints, retry_attempt) - } else { - None - } - } else { - None - } - }); + log::info!( + "Selected entry relay {} at {} going through {} at {}", + entry_relay.hostname, + entry_endpoint.peer.endpoint.ip(), + exit_relay.hostname, + exit_endpoint.to_endpoint().address.ip(), + ); + let result = RelaySelectorResult::wireguard_multihop_endpoint( + exit_relay, + entry_endpoint, + entry_relay, + ); + return Ok(result); + } - if let MullvadEndpoint::Wireguard { peer, .. } = &mut endpoint { - if let Some((entry_relay, mut entry_endpoint)) = entry_endpoint.take() { - self.set_entry_peers(peer, &mut entry_endpoint); - let addr_in = entry_endpoint.to_endpoint().address.ip(); - log::info!( - "Selected entry relay {} at {}", - entry_relay.hostname, - addr_in - ); - return Ok((exit_relay, Some(entry_relay), entry_endpoint)); - } else if relay_constraints.wireguard_constraints.use_multihop { - return Err(Error::NoRelay); - } + /// Returns a WireGuard endpoint, should only ever be used when the user has specified the + /// tunnel protocol as only WireGuard. + fn get_wireguard_endpoint( + &self, + location: &Constraint<LocationConstraint>, + providers: &Constraint<Providers>, + wireguard_constraints: &WireguardConstraints, + retry_attempt: u32, + ) -> Result<RelaySelectorResult, Error> { + let mut entry_relay_matcher = RelayMatcher { + location: location.clone(), + providers: providers.clone(), + tunnel: wireguard_constraints.clone().into(), + }; + + let mut preferred_matcher: RelayMatcher<WireguardMatcher> = entry_relay_matcher.clone(); + preferred_matcher.tunnel.port = preferred_matcher + .tunnel + .port + .or(Self::preferred_wireguard_port(retry_attempt)); + + if !wireguard_constraints.use_multihop { + return self + .get_tunnel_endpoint_internal(&preferred_matcher) + .or_else(|_| self.get_tunnel_endpoint_internal(&entry_relay_matcher)); } - Ok((exit_relay, None, endpoint)) + entry_relay_matcher.location = wireguard_constraints.entry_location.clone(); + self.get_wireguard_multi_hop_endpoint(entry_relay_matcher, location.clone()) } - fn get_tunnel_exit_endpoint( - &mut self, + /// Returns a tunnel endpoint of any type, should only be used when the user hasn't specified a + /// tunnel protocol. + fn get_any_tunnel_endpoint( + &self, relay_constraints: &RelayConstraints, bridge_state: BridgeState, retry_attempt: u32, wg_key_exists: bool, - wg_entry_peer: Option<&wireguard::PeerConfig>, - ) -> Result<(Relay, MullvadEndpoint), Error> { + ) -> Result<RelaySelectorResult, Error> { let preferred_constraints = self.preferred_constraints( &relay_constraints, bridge_state, retry_attempt, wg_key_exists, ); - if let Some((relay, endpoint)) = - self.get_tunnel_endpoint_internal(&preferred_constraints, wg_entry_peer) - { - log::debug!( - "Relay matched on highest preference for retry attempt {}", - retry_attempt - ); - Ok((relay, endpoint)) - } else if let Some((relay, endpoint)) = - self.get_tunnel_endpoint_internal(&relay_constraints, wg_entry_peer) - { - log::debug!( - "Relay matched on second preference for retry attempt {}", - retry_attempt - ); - Ok((relay, endpoint)) - } else { - log::warn!("No relays matching {}", &relay_constraints); - Err(Error::NoRelay) + let original_matcher: RelayMatcher<_> = relay_constraints.clone().into(); + + let preferred_tunnel_protocol = preferred_constraints.tunnel_protocol; + let preferred_matcher: RelayMatcher<_> = preferred_constraints.into(); + + match preferred_tunnel_protocol { + Constraint::Only(TunnelType::Wireguard) + if relay_constraints.wireguard_constraints.use_multihop => + { + let exit_location = relay_constraints.location.clone(); + let mut preferred_entry_matcher = preferred_matcher.to_wireguard_matcher(); + preferred_entry_matcher.location = relay_constraints + .wireguard_constraints + .entry_location + .clone(); + let mut original_entry_matcher = original_matcher.to_wireguard_matcher(); + original_entry_matcher.location = relay_constraints + .wireguard_constraints + .entry_location + .clone(); + self.get_wireguard_multi_hop_endpoint( + preferred_entry_matcher, + exit_location.clone(), + ) + .or_else(|_| { + self.get_wireguard_multi_hop_endpoint(original_entry_matcher, exit_location) + }) + } + + _ => { + if let Ok(result) = self.get_tunnel_endpoint_internal(&preferred_matcher) { + log::debug!( + "Relay matched on highest preference for retry attempt {}", + retry_attempt + ); + Ok(result) + } else if let Ok(result) = self.get_tunnel_endpoint_internal(&original_matcher) { + log::debug!( + "Relay matched on second preference for retry attempt {}", + retry_attempt + ); + Ok(result) + } else { + log::warn!("No relays matching {}", &relay_constraints); + Err(Error::NoRelay) + } + } } } + // This function ignores the tunnel type constraint on purpose. fn preferred_constraints( &self, original_constraints: &RelayConstraints, @@ -417,73 +530,57 @@ impl RelaySelector { Constraint::Only(TunnelType::Wireguard) => { relay_constraints.wireguard_constraints = original_constraints.wireguard_constraints.clone(); - // This ensures that if after the first 2 failed attempts the daemon does not - // connect, then afterwards 2 of each 4 successive attempts will try to connect - // on port 53. - if retry_attempt % 4 > 1 && relay_constraints.wireguard_constraints.port.is_any() { + if relay_constraints.wireguard_constraints.port.is_any() { relay_constraints.wireguard_constraints.port = - Constraint::Only(TransportPort { - protocol: TransportProtocol::Udp, - port: Constraint::Only(53), - }); + Self::preferred_wireguard_port(retry_attempt); } } + }; + + if relay_constraints.wireguard_constraints.port.is_any() { + relay_constraints.wireguard_constraints.port = Constraint::Only(TransportPort { + port: preferred_port, + protocol: TransportProtocol::Udp, + }); } + relay_constraints.tunnel_protocol = Constraint::Only(preferred_tunnel); + relay_constraints } - fn select_entry_endpoint( - &mut self, - exit_peer: Option<&wireguard::PeerConfig>, - relay_constraints: &RelayConstraints, - retry_attempt: u32, - ) -> Option<(Relay, MullvadEndpoint)> { - if !relay_constraints.wireguard_constraints.use_multihop { - return None; - } - let entry_location = relay_constraints - .wireguard_constraints - .entry_location - .clone(); - let entry_constraints = RelayConstraints { - location: entry_location, - tunnel_protocol: Constraint::Only(TunnelType::Wireguard), - ..relay_constraints.clone() - }; - let entry_constraints = - self.preferred_constraints(&entry_constraints, BridgeState::Off, retry_attempt, true); - + fn get_entry_endpoint( + &self, + matcher: &RelayMatcher<WireguardMatcher>, + ) -> Result<(Relay, MullvadWireguardEndpoint), Error> { let matching_relays: Vec<Relay> = self .parsed_relays .lock() .relays() .iter() .filter(|relay| relay.active) - .filter_map(|relay| Self::matching_relay(relay, &entry_constraints, exit_peer)) + .filter_map(|relay| matcher.filter_matching_relay(relay)) .collect(); let relay = self .pick_random_relay(&matching_relays) - .map(|relay| relay.clone())?; - let endpoint = self.get_random_tunnel(&relay, &entry_constraints)?; - Some((relay, endpoint)) + .map(|relay| relay.clone()) + .ok_or(Error::NoRelay)?; + let endpoint = matcher + .mullvad_endpoint(&relay) + .ok_or(Error::NoRelay)? + .unwrap_wireguard() + .clone(); + + Ok((relay, endpoint)) } fn set_entry_peers( - &mut self, - new_exit_peer: &wireguard::PeerConfig, - entry_endpoint: &mut MullvadEndpoint, + exit_peer: &wireguard::PeerConfig, + entry_endpoint: &mut MullvadWireguardEndpoint, ) { - if let MullvadEndpoint::Wireguard { - ref mut peer, - exit_peer, - .. - } = entry_endpoint - { - peer.allowed_ips = vec![IpNetwork::from(new_exit_peer.endpoint.ip())]; - *exit_peer = Some(new_exit_peer.clone()); - } + entry_endpoint.peer.allowed_ips = vec![IpNetwork::from(exit_peer.endpoint.ip())]; + entry_endpoint.exit_peer = Some(exit_peer.clone()); } pub fn get_auto_proxy_settings( @@ -602,10 +699,29 @@ impl RelaySelector { } } + fn preferred_wireguard_port(retry_attempt: u32) -> Constraint<TransportPort> { + // This ensures that if after the first 2 failed attempts the daemon does not + // connect, then afterwards 2 of each 4 successive attempts will try to connect + // on port 53. + let port = match retry_attempt % 4 { + 0 | 1 => Constraint::Any, + _ => Constraint::Only(53), + }; + Constraint::Only(TransportPort { + port, + protocol: TransportProtocol::Udp, + }) + } + fn preferred_openvpn_constraints(retry_attempt: u32) -> (Constraint<u16>, TransportProtocol) { // Prefer UDP by default. But if that has failed a couple of times, then try TCP port // 443, which works for many with UDP problems. After that, just alternate // between protocols. + // If the tunnel type constraint is set OpenVpn, from the 4th attempt onwards, every two + // retry attempts OpenVpn constraints should be set to TCP as a bridge will be used, + // and to UDP for the next two attempts. If the tunnel type is specified to be _Any_ + // and on not-Windows, the first two tries are used for WireGuard and don't + // affect counting here. match retry_attempt { 0 | 1 => (Constraint::Any, TransportProtocol::Udp), 2 | 3 => (Constraint::Only(443), TransportProtocol::Tcp), @@ -615,115 +731,30 @@ impl RelaySelector { } /// Returns a random relay endpoint if any is matching the given constraints. - fn get_tunnel_endpoint_internal( - &mut self, - constraints: &RelayConstraints, - wg_entry_peer: Option<&wireguard::PeerConfig>, - ) -> Option<(Relay, MullvadEndpoint)> { + fn get_tunnel_endpoint_internal<T: TunnelMatcher>( + &self, + matcher: &RelayMatcher<T>, + ) -> Result<RelaySelectorResult, Error> { let matching_relays: Vec<Relay> = self .parsed_relays .lock() .relays() .iter() .filter(|relay| relay.active) - .filter_map(|relay| Self::matching_relay(relay, constraints, wg_entry_peer)) + .filter_map(|relay| matcher.filter_matching_relay(relay)) .collect(); self.pick_random_relay(&matching_relays) .and_then(|selected_relay| { - let endpoint = self.get_random_tunnel(&selected_relay, &constraints); + let endpoint = matcher.mullvad_endpoint(&selected_relay); let addr_in = endpoint .as_ref() .map(|endpoint| endpoint.to_endpoint().address.ip()) .unwrap_or(IpAddr::from(selected_relay.ipv4_addr_in)); log::info!("Selected relay {} at {}", selected_relay.hostname, addr_in); - endpoint.map(|endpoint| (selected_relay.clone(), endpoint)) + endpoint.map(|endpoint| RelaySelectorResult::new(endpoint, selected_relay.clone())) }) - } - - /// Takes a `Relay` and a corresponding `RelayConstraints` and returns a new `Relay` if the - /// given relay matches the constraints. - fn matching_relay( - relay: &Relay, - constraints: &RelayConstraints, - skip_wg_peer: Option<&wireguard::PeerConfig>, - ) -> Option<Relay> { - if !constraints.location.matches(relay) { - return None; - } - if !constraints.providers.matches(&relay) { - return None; - } - - let include_wg = if let Some(wg_peer) = skip_wg_peer { - let peer_ip = wg_peer.endpoint.ip(); - peer_ip != IpAddr::V4(relay.ipv4_addr_in) - && Some(peer_ip) != relay.ipv6_addr_in.map(IpAddr::V6) - } else { - true - }; - - let relay = match constraints.tunnel_protocol { - Constraint::Any => { - let mut relay = relay.clone(); - relay.tunnels = RelayTunnels { - wireguard: if include_wg { - Self::matching_wireguard_tunnels( - &relay.tunnels, - &constraints.wireguard_constraints, - ) - } else { - vec![] - }, - openvpn: Self::matching_openvpn_tunnels( - &relay.tunnels, - constraints.openvpn_constraints, - ), - }; - relay - } - Constraint::Only(TunnelType::Wireguard) => { - let mut relay = relay.clone(); - relay.tunnels = RelayTunnels { - wireguard: if include_wg { - Self::matching_wireguard_tunnels( - &relay.tunnels, - &constraints.wireguard_constraints, - ) - } else { - vec![] - }, - openvpn: vec![], - }; - relay - } - - Constraint::Only(TunnelType::OpenVpn) => { - let mut relay = relay.clone(); - relay.tunnels = RelayTunnels { - openvpn: Self::matching_openvpn_tunnels( - &relay.tunnels, - constraints.openvpn_constraints, - ), - wireguard: vec![], - }; - relay - } - }; - - let relay_matches = match constraints.tunnel_protocol { - Constraint::Any => { - !relay.tunnels.openvpn.is_empty() || !relay.tunnels.wireguard.is_empty() - } - Constraint::Only(TunnelType::OpenVpn) => !relay.tunnels.openvpn.is_empty(), - Constraint::Only(TunnelType::Wireguard) => !relay.tunnels.wireguard.is_empty(), - }; - - if relay_matches { - Some(relay) - } else { - None - } + .ok_or(Error::NoRelay) } fn matching_bridge_relay( @@ -749,42 +780,19 @@ impl RelaySelector { Some(filtered_relay) } - fn matching_openvpn_tunnels( - tunnels: &RelayTunnels, - constraints: OpenVpnConstraints, - ) -> Vec<OpenVpnEndpointData> { - tunnels - .openvpn - .iter() - .filter(|endpoint| constraints.matches(*endpoint)) - .cloned() - .collect() - } - - fn matching_wireguard_tunnels( - tunnels: &RelayTunnels, - constraints: &WireguardConstraints, - ) -> Vec<WireguardEndpointData> { - tunnels - .wireguard - .iter() - .filter(|endpoint| constraints.matches(*endpoint)) - .cloned() - .collect() - } - /// Pick a random relay from the given slice. Will return `None` if the given slice is empty. /// If all of the relays have a weight of 0, one will be picked at random without bias, /// otherwise roulette wheel selection will be used to pick only relays with non-zero /// weights. - fn pick_random_relay<'a>(&mut self, relays: &'a [Relay]) -> Option<&'a Relay> { + fn pick_random_relay<'a>(&self, relays: &'a [Relay]) -> Option<&'a Relay> { let total_weight: u64 = relays.iter().map(|relay| relay.weight).sum(); + let mut rng = rand::thread_rng(); if total_weight == 0 { - relays.choose(&mut self.rng) + relays.choose(&mut rng) } else { - // Pick a random number in the range 1 - total_weight. This choses the relay with a + // Pick a random number in the range 1..=total_weight. This choses the relay with a // non-zero weight. - let mut i: u64 = self.rng.gen_range(1, total_weight + 1); + let mut i: u64 = rng.gen_range(1, total_weight + 1); Some( relays .iter() @@ -798,11 +806,11 @@ impl RelaySelector { } /// Picks a random bridge from a relay. - fn pick_random_bridge(&mut self, relay: &Relay) -> Option<ProxySettings> { + fn pick_random_bridge(&self, relay: &Relay) -> Option<ProxySettings> { relay .bridges .shadowsocks - .choose(&mut self.rng) + .choose(&mut rand::thread_rng()) .map(|shadowsocks_endpoint| { log::info!( "Selected Shadowsocks bridge {} at {}:{}/{}", @@ -817,133 +825,6 @@ impl RelaySelector { }) } - fn get_random_tunnel( - &mut self, - relay: &Relay, - constraints: &RelayConstraints, - ) -> Option<MullvadEndpoint> { - #[cfg(not(target_os = "android"))] - let mut thread_rng = self.rng.clone(); - #[cfg(not(target_os = "android"))] - let mut new_openvpn_endpoint = || { - relay - .tunnels - .openvpn - .choose(&mut thread_rng) - .cloned() - .map(|endpoint| endpoint.into_mullvad_endpoint(relay.ipv4_addr_in.into())) - }; - - let mut new_wg_endpoint = || { - relay - .tunnels - .wireguard - .choose(&mut self.rng) - .cloned() - .and_then(|wg_tunnel| { - self.wg_data_to_endpoint(relay, wg_tunnel, &constraints.wireguard_constraints) - }) - }; - - #[cfg(not(target_os = "android"))] - match constraints.tunnel_protocol { - Constraint::Only(TunnelType::OpenVpn) => new_openvpn_endpoint(), - - Constraint::Any => vec![new_openvpn_endpoint(), new_wg_endpoint()] - .into_iter() - .filter_map(|relay| relay) - .collect::<Vec<_>>() - .choose(&mut self.rng) - .cloned(), - - Constraint::Only(TunnelType::Wireguard) => new_wg_endpoint(), - } - #[cfg(target_os = "android")] - new_wg_endpoint() - } - - fn wg_data_to_endpoint( - &mut self, - relay: &Relay, - data: WireguardEndpointData, - constraints: &WireguardConstraints, - ) -> Option<MullvadEndpoint> { - let host = self.get_address_for_wireguard_relay(relay, constraints)?; - let port = self.get_port_for_wireguard_relay(&data, constraints)?; - let peer_config = wireguard::PeerConfig { - public_key: data.public_key, - endpoint: SocketAddr::new(host, port), - allowed_ips: all_of_the_internet(), - protocol: constraints - .port - .map(|port| port.protocol) - .unwrap_or(TransportProtocol::Udp), - }; - Some(MullvadEndpoint::Wireguard { - peer: peer_config, - exit_peer: None, - ipv4_gateway: data.ipv4_gateway, - ipv6_gateway: data.ipv6_gateway, - }) - } - - fn get_address_for_wireguard_relay( - &mut self, - relay: &Relay, - constraints: &WireguardConstraints, - ) -> Option<IpAddr> { - match constraints.ip_version { - Constraint::Any | Constraint::Only(IpVersion::V4) => Some(relay.ipv4_addr_in.into()), - Constraint::Only(IpVersion::V6) => relay.ipv6_addr_in.map(|addr| addr.into()), - } - } - - fn get_port_for_wireguard_relay( - &mut self, - data: &WireguardEndpointData, - constraints: &WireguardConstraints, - ) -> Option<u16> { - match constraints - .port - .as_ref() - .map(|port| port.port) - .unwrap_or(Constraint::Any) - { - Constraint::Any => { - let get_port_amount = - |range: &(u16, u16)| -> u64 { (1 + range.1 - range.0) as u64 }; - let port_amount: u64 = data.port_ranges.iter().map(get_port_amount).sum(); - - if port_amount < 1 { - return None; - } - - let mut port_index = self.rng.gen_range(0, port_amount); - - for range in data.port_ranges.iter() { - let ports_in_range = get_port_amount(range); - if port_index < ports_in_range { - return Some(port_index as u16 + range.0); - } - port_index -= ports_in_range; - } - log::error!("Port selection algorithm is broken!"); - None - } - Constraint::Only(port) => { - if data - .port_ranges - .iter() - .any(|range| (range.0 <= port && port <= range.1)) - { - Some(port) - } else { - None - } - } - } - } - /// Try to read the relays from disk, preferring the newer ones. fn read_relays_from_disk( cache_path: &Path, @@ -972,187 +853,32 @@ impl RelaySelector { } } -#[derive(Clone)] -pub struct RelayListUpdaterHandle { - tx: mpsc::Sender<()>, -} - -impl RelayListUpdaterHandle { - pub async fn update_relay_list(&mut self) -> Result<(), Error> { - self.tx - .send(()) - .await - .map_err(|_| Error::DownloaderShutDown) - } -} - -struct RelayListUpdater { - rpc_client: RelayListProxy, - cache_path: PathBuf, - parsed_relays: Arc<Mutex<ParsedRelays>>, - on_update: Box<dyn Fn(&RelayList) + Send + 'static>, - earliest_next_try: Instant, - api_availability: ApiAvailabilityHandle, +#[derive(Debug)] +pub struct RelaySelectorResult { + pub exit_relay: Relay, + pub endpoint: MullvadEndpoint, + pub entry_relay: Option<Relay>, } -impl RelayListUpdater { - pub fn new( - rpc_handle: MullvadRestHandle, - cache_path: PathBuf, - parsed_relays: Arc<Mutex<ParsedRelays>>, - on_update: Box<dyn Fn(&RelayList) + Send + 'static>, - api_availability: ApiAvailabilityHandle, - ) -> RelayListUpdaterHandle { - let (tx, cmd_rx) = mpsc::channel(1); - let service = rpc_handle.service(); - let rpc_client = RelayListProxy::new(rpc_handle); - let updater = RelayListUpdater { - rpc_client, - cache_path, - parsed_relays, - on_update, - earliest_next_try: Instant::now() + UPDATE_INTERVAL, - api_availability, - }; - - service.spawn(updater.run(cmd_rx)); - - RelayListUpdaterHandle { tx } - } - - async fn run(mut self, mut cmd_rx: mpsc::Receiver<()>) { - let mut check_interval = - tokio_stream::wrappers::IntervalStream::new(tokio::time::interval_at( - (Instant::now() + UPDATE_CHECK_INTERVAL).into(), - UPDATE_CHECK_INTERVAL, - )) - .fuse(); - let mut download_future = Box::pin(Fuse::terminated()); - loop { - futures::select! { - _check_update = check_interval.next() => { - if download_future.is_terminated() && self.should_update() { - let tag = self.parsed_relays.lock().tag().map(|tag| tag.to_string()); - download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.rpc_client.clone(), tag).fuse()); - self.earliest_next_try = Instant::now() + UPDATE_INTERVAL; - } - }, - - new_relay_list = download_future => { - self.consume_new_relay_list(new_relay_list).await; - - }, - - cmd = cmd_rx.next() => { - match cmd { - Some(()) => { - let tag = self.parsed_relays.lock().tag().map(|tag| tag.to_string()); - download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.rpc_client.clone(), tag).fuse()); - }, - None => { - log::trace!("Relay list updater shutting down"); - return; - } - } - } - - }; +impl RelaySelectorResult { + fn new(endpoint: MullvadEndpoint, exit_relay: Relay) -> Self { + Self { + exit_relay, + endpoint, + entry_relay: None, } } - async fn consume_new_relay_list( - &mut self, - result: Result<Option<RelayList>, mullvad_rpc::Error>, - ) { - match result { - Ok(Some(relay_list)) => { - if let Err(err) = self.update_cache(relay_list).await { - log::error!("Failed to update relay list cache: {}", err); - } - } - Ok(None) => log::debug!("Relay list is up-to-date"), - Err(err) => { - log::error!( - "Failed to fetch new relay list: {}. Will retry in {} minutes", - err, - self.earliest_next_try - .saturating_duration_since(Instant::now()) - .as_secs() - / 60 - ); - } - } - } - - /// Returns true if the current parsed_relays is older than UPDATE_INTERVAL - fn should_update(&mut self) -> bool { - match SystemTime::now().duration_since(self.parsed_relays.lock().last_updated()) { - Ok(duration) => duration > UPDATE_INTERVAL && self.earliest_next_try <= Instant::now(), - // If the clock is skewed we have no idea by how much or when the last update - // actually was, better download again to get in sync and get a `last_updated` - // timestamp corresponding to the new time. - Err(_) => true, - } - } - - fn download_relay_list( - api_handle: ApiAvailabilityHandle, - rpc_handle: RelayListProxy, - tag: Option<String>, - ) -> impl Future<Output = Result<Option<RelayList>, mullvad_rpc::Error>> + 'static { - let download_futures = move || { - let available = api_handle.wait_background(); - let req = rpc_handle.relay_list(tag.clone()); - async move { - available.await?; - req.await.map_err(mullvad_rpc::Error::from) - } - }; - - let exponential_backoff = - ExponentialBackoff::new(EXPONENTIAL_BACKOFF_INITIAL, EXPONENTIAL_BACKOFF_FACTOR) - .max_delay(UPDATE_INTERVAL * 2); - - let download_future = retry_future( - download_futures, - |result| result.is_err(), - Jittered::jitter(exponential_backoff), - ); - download_future - } - - async fn update_cache(&mut self, new_relay_list: RelayList) -> Result<(), Error> { - if let Err(error) = Self::cache_relays(&self.cache_path, &new_relay_list).await { - log::error!( - "{}", - error.display_chain_with_msg("Failed to update relay cache on disk") - ); + fn wireguard_multihop_endpoint( + exit_relay: Relay, + endpoint: MullvadWireguardEndpoint, + entry: Relay, + ) -> Self { + Self { + exit_relay, + endpoint: MullvadEndpoint::Wireguard(endpoint), + entry_relay: Some(entry), } - - 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 = new_parsed_relays; - (self.on_update)(parsed_relays.locations()); - Ok(()) - } - - /// Write a `RelayList` to the cache file. - async fn cache_relays(cache_path: &Path, relays: &RelayList) -> Result<(), Error> { - log::debug!("Writing relays cache to {}", cache_path.display()); - let mut file = File::create(cache_path) - .await - .map_err(Error::OpenRelayCache)?; - let bytes = serde_json::to_vec_pretty(relays).map_err(Error::Serialize)?; - let mut slice: &[u8] = bytes.as_slice(); - let _ = tokio::io::copy(&mut slice, &mut file) - .await - .map_err(Error::WriteRelayCache)?; - Ok(()) } } @@ -1162,8 +888,8 @@ mod test { use mullvad_types::{ relay_constraints::RelayConstraints, relay_list::{ - Relay, RelayBridges, RelayListCity, RelayListCountry, RelayTunnels, - WireguardEndpointData, + OpenVpnEndpointData, Relay, RelayBridges, RelayListCity, RelayListCountry, + RelayTunnels, WireguardEndpointData, }, }; use talpid_types::net::wireguard::PublicKey; @@ -1279,14 +1005,13 @@ mod test { RELAYS.clone(), SystemTime::now(), ))), - rng: rand::thread_rng(), updater: None, } } #[test] fn test_preferred_tunnel_protocol() { - let mut relay_selector = new_relay_selector(); + let relay_selector = new_relay_selector(); // Prefer WG if the location only supports it let location = LocationConstraint::Hostname( @@ -1309,7 +1034,7 @@ mod test { for attempt in 0..10 { assert!(relay_selector - .get_tunnel_exit_endpoint(&relay_constraints, BridgeState::Off, attempt, true, None) + .get_any_tunnel_endpoint(&relay_constraints, BridgeState::Off, attempt, true) .is_ok()); } @@ -1334,7 +1059,7 @@ mod test { for attempt in 0..10 { assert!(relay_selector - .get_tunnel_exit_endpoint(&relay_constraints, BridgeState::Off, attempt, true, None) + .get_any_tunnel_endpoint(&relay_constraints, BridgeState::Off, attempt, true) .is_ok()); } @@ -1353,14 +1078,13 @@ mod test { preferred.tunnel_protocol, Constraint::Only(TunnelType::OpenVpn) ); - match relay_selector.get_tunnel_exit_endpoint( + match relay_selector.get_any_tunnel_endpoint( &relay_constraints, BridgeState::Off, attempt, true, - None, ) { - Ok((_, MullvadEndpoint::OpenVpn(_))) => (), + Ok(result) if matches!(result.endpoint, MullvadEndpoint::OpenVpn(_)) => (), _ => panic!("OpenVPN endpoint was not selected"), } } @@ -1369,7 +1093,7 @@ mod test { #[test] fn test_wg_entry_hostname_collision() { - let mut relay_selector = new_relay_selector(); + let relay_selector = new_relay_selector(); let location1 = LocationConstraint::Hostname( "se".to_string(), @@ -1406,7 +1130,7 @@ mod test { #[test] fn test_wg_entry_filter() -> Result<(), String> { - let mut relay_selector = new_relay_selector(); + let relay_selector = new_relay_selector(); let specific_hostname = "se10-wireguard"; @@ -1428,9 +1152,10 @@ mod test { Constraint::Only(location_specific.clone()); // The exit must not equal the entry - let (exit_relay, _entry_relay, _exit_endpoint) = relay_selector + let exit_relay = relay_selector .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, true) - .map_err(|error| error.to_string())?; + .map_err(|error| error.to_string())? + .exit_relay; assert_ne!(exit_relay.hostname, specific_hostname); @@ -1438,20 +1163,22 @@ mod test { relay_constraints.wireguard_constraints.entry_location = Constraint::Only(location_general); // The entry must not equal the exit - let (exit_relay, _entry_relay, exit_endpoint) = relay_selector + let RelaySelectorResult { + exit_relay, + endpoint, + .. + } = relay_selector .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, true) .map_err(|error| error.to_string())?; assert_eq!(exit_relay.hostname, specific_hostname); - match exit_endpoint { - MullvadEndpoint::OpenVpn { .. } => return Err("Expected WireGuard relay".to_string()), - MullvadEndpoint::Wireguard { - peer, exit_peer, .. - } => { - assert_eq!(exit_relay.ipv4_addr_in, exit_peer.unwrap().endpoint.ip()); - assert_ne!(exit_relay.ipv4_addr_in, peer.endpoint.ip()); - } - } + + let endpoint = endpoint.unwrap_wireguard(); + assert_eq!( + exit_relay.ipv4_addr_in, + endpoint.exit_peer.as_ref().unwrap().endpoint.ip() + ); + assert_ne!(exit_relay.ipv4_addr_in, endpoint.peer.endpoint.ip()); Ok(()) } @@ -1543,4 +1270,113 @@ mod test { Ok(()) } + + #[test] + fn test_wg_relay_with_no_key() { + let mut relay_constraints = RelayConstraints { + tunnel_protocol: Constraint::Only(TunnelType::Wireguard), + ..RelayConstraints::default() + }; + + let relay_selector = new_relay_selector(); + + let result = relay_selector + .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, false) + .expect("Failed to get WireGuard relay when WireGuard relay was specified as the only tunnel protocol"); + + assert!(matches!(result.endpoint, MullvadEndpoint::Wireguard(_))); + + relay_constraints.tunnel_protocol = Constraint::Any; + let result = relay_selector + .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, false) + .expect("Failed to get OpenVPN relay with tunnel protocol constraint set to Any and without a WireGuard key"); + + assert!(matches!(result.endpoint, MullvadEndpoint::OpenVpn(_))); + + let wireguard_specific_location = LocationConstraint::Hostname( + "se".to_string(), + "got".to_string(), + "se9-wireguard".to_string(), + ); + relay_constraints.location = Constraint::Only(wireguard_specific_location); + + let result = relay_selector + .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, false) + .expect( + "Failed to get a valid WireGuard relay when tunnel constraints are set to any + tunnel protocol and with a wireguard specific location without a wireguard key", + ); + + assert!(matches!(result.endpoint, MullvadEndpoint::Wireguard(_))); + + let result = relay_selector + .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, true) + .expect( + "Failed to get a valid WireGuard relay when tunnel constraints are set to any + tunnel protocol and with a wireguard specific location with a wireguard key", + ); + + assert!(matches!(result.endpoint, MullvadEndpoint::Wireguard(_))); + } + + #[test] + fn test_selecting_any_relay_will_consider_multihop() { + let relay_constraints = RelayConstraints { + wireguard_constraints: WireguardConstraints { + use_multihop: true, + ..WireguardConstraints::default() + }, + // This has to be explicit otherwise Android will chose WireGuard when default + // constructing. + tunnel_protocol: Constraint::Any, + ..RelayConstraints::default() + }; + + let relay_selector = new_relay_selector(); + + let result = relay_selector.get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, true) + .expect("Failed to get relay when tunnel constraints are set to Any and retrying the selection"); + // Windows will ignore WireGuard until WireGuard is supported well enough + // TODO: Remove this caveat once Windows defaults to using WireGuard + #[cfg(target_os = "windows")] + assert!( + matches!(result.endpoint, MullvadEndpoint::OpenVpn(_)) && result.entry_relay.is_none() + ); + + #[cfg(not(target_os = "windows"))] + assert!( + matches!(result.endpoint, MullvadEndpoint::Wireguard(_)) + && result.entry_relay.is_some() + ); + } + + #[test] + fn test_selecting_wireguard_location_will_consider_multihop() { + let wireguard_specific_location = LocationConstraint::Hostname( + "se".to_string(), + "got".to_string(), + "se9-wireguard".to_string(), + ); + + let relay_constraints = RelayConstraints { + location: Constraint::Only(wireguard_specific_location), + wireguard_constraints: WireguardConstraints { + use_multihop: true, + ..WireguardConstraints::default() + }, + // This has to be explicit otherwise Android will chose WireGuard when default + // constructing. + tunnel_protocol: Constraint::Any, + ..RelayConstraints::default() + }; + + let relay_selector = new_relay_selector(); + + let result = relay_selector.get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, true) + .expect("Failed to get relay when tunnel constraints are set to Any and retrying the selection"); + assert!( + matches!(result.endpoint, MullvadEndpoint::Wireguard(_)) + && result.entry_relay.is_some() + ); + } } diff --git a/mullvad-daemon/src/relays/updater.rs b/mullvad-daemon/src/relays/updater.rs new file mode 100644 index 0000000000..521eee3cda --- /dev/null +++ b/mullvad-daemon/src/relays/updater.rs @@ -0,0 +1,211 @@ +use super::{Error, ParsedRelays}; +use futures::{ + channel::mpsc, + future::{Fuse, FusedFuture}, + Future, FutureExt, SinkExt, StreamExt, +}; +use mullvad_rpc::{availability::ApiAvailabilityHandle, rest::MullvadRestHandle, RelayListProxy}; +use mullvad_types::relay_list::RelayList; +use parking_lot::Mutex; +use std::{ + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; +use talpid_core::future_retry::{retry_future, ExponentialBackoff, Jittered}; +use talpid_types::ErrorExt; +use tokio::fs::File; + +/// How often the updater should wake up to check the cache of the in-memory cache of relays. +/// This check is very cheap. The only reason to not have it very often is because if downloading +/// constantly fails it will try very often and fill the logs etc. +const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 15); +/// How old the cached relays need to be to trigger an update +const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60); + +const EXPONENTIAL_BACKOFF_INITIAL: Duration = Duration::from_secs(16); +const EXPONENTIAL_BACKOFF_FACTOR: u32 = 8; + +#[derive(Clone)] +pub struct RelayListUpdaterHandle { + tx: mpsc::Sender<()>, +} + +impl RelayListUpdaterHandle { + pub async fn update_relay_list(&mut self) -> Result<(), Error> { + self.tx + .send(()) + .await + .map_err(|_| Error::DownloaderShutDown) + } +} + +pub struct RelayListUpdater { + rpc_client: RelayListProxy, + cache_path: PathBuf, + parsed_relays: Arc<Mutex<ParsedRelays>>, + on_update: Box<dyn Fn(&RelayList) + Send + 'static>, + earliest_next_try: Instant, + api_availability: ApiAvailabilityHandle, +} + +impl RelayListUpdater { + pub(super) fn new( + rpc_handle: MullvadRestHandle, + cache_path: PathBuf, + parsed_relays: Arc<Mutex<ParsedRelays>>, + on_update: Box<dyn Fn(&RelayList) + Send + 'static>, + api_availability: ApiAvailabilityHandle, + ) -> RelayListUpdaterHandle { + let (tx, cmd_rx) = mpsc::channel(1); + let service = rpc_handle.service(); + let rpc_client = RelayListProxy::new(rpc_handle); + let updater = RelayListUpdater { + rpc_client, + cache_path, + parsed_relays, + on_update, + earliest_next_try: Instant::now() + UPDATE_INTERVAL, + api_availability, + }; + + service.spawn(updater.run(cmd_rx)); + + RelayListUpdaterHandle { tx } + } + + async fn run(mut self, mut cmd_rx: mpsc::Receiver<()>) { + let mut check_interval = + tokio_stream::wrappers::IntervalStream::new(tokio::time::interval_at( + (Instant::now() + UPDATE_CHECK_INTERVAL).into(), + UPDATE_CHECK_INTERVAL, + )) + .fuse(); + let mut download_future = Box::pin(Fuse::terminated()); + loop { + futures::select! { + _check_update = check_interval.next() => { + if download_future.is_terminated() && self.should_update() { + let tag = self.parsed_relays.lock().tag().map(|tag| tag.to_string()); + download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.rpc_client.clone(), tag).fuse()); + self.earliest_next_try = Instant::now() + UPDATE_INTERVAL; + } + }, + + new_relay_list = download_future => { + self.consume_new_relay_list(new_relay_list).await; + + }, + + cmd = cmd_rx.next() => { + match cmd { + Some(()) => { + let tag = self.parsed_relays.lock().tag().map(|tag| tag.to_string()); + download_future = Box::pin(Self::download_relay_list(self.api_availability.clone(), self.rpc_client.clone(), tag).fuse()); + }, + None => { + log::trace!("Relay list updater shutting down"); + return; + } + } + } + + }; + } + } + + async fn consume_new_relay_list( + &mut self, + result: Result<Option<RelayList>, mullvad_rpc::Error>, + ) { + match result { + Ok(Some(relay_list)) => { + if let Err(err) = self.update_cache(relay_list).await { + log::error!("Failed to update relay list cache: {}", err); + } + } + Ok(None) => log::debug!("Relay list is up-to-date"), + Err(err) => { + log::error!( + "Failed to fetch new relay list: {}. Will retry in {} minutes", + err, + self.earliest_next_try + .saturating_duration_since(Instant::now()) + .as_secs() + / 60 + ); + } + } + } + + /// Returns true if the current parsed_relays is older than UPDATE_INTERVAL + fn should_update(&mut self) -> bool { + match SystemTime::now().duration_since(self.parsed_relays.lock().last_updated()) { + Ok(duration) => duration > UPDATE_INTERVAL && self.earliest_next_try <= Instant::now(), + // If the clock is skewed we have no idea by how much or when the last update + // actually was, better download again to get in sync and get a `last_updated` + // timestamp corresponding to the new time. + Err(_) => true, + } + } + + fn download_relay_list( + api_handle: ApiAvailabilityHandle, + rpc_handle: RelayListProxy, + tag: Option<String>, + ) -> impl Future<Output = Result<Option<RelayList>, mullvad_rpc::Error>> + 'static { + let download_futures = move || { + let available = api_handle.wait_background(); + let req = rpc_handle.relay_list(tag.clone()); + async move { + available.await?; + req.await.map_err(mullvad_rpc::Error::from) + } + }; + + let exponential_backoff = + ExponentialBackoff::new(EXPONENTIAL_BACKOFF_INITIAL, EXPONENTIAL_BACKOFF_FACTOR) + .max_delay(UPDATE_INTERVAL * 2); + + let download_future = retry_future( + download_futures, + |result| result.is_err(), + Jittered::jitter(exponential_backoff), + ); + download_future + } + + async fn update_cache(&mut self, new_relay_list: RelayList) -> Result<(), Error> { + if let Err(error) = Self::cache_relays(&self.cache_path, &new_relay_list).await { + log::error!( + "{}", + error.display_chain_with_msg("Failed to update relay cache on disk") + ); + } + + 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 = new_parsed_relays; + (self.on_update)(parsed_relays.locations()); + Ok(()) + } + + /// Write a `RelayList` to the cache file. + async fn cache_relays(cache_path: &Path, relays: &RelayList) -> Result<(), Error> { + log::debug!("Writing relays cache to {}", cache_path.display()); + let mut file = File::create(cache_path) + .await + .map_err(Error::OpenRelayCache)?; + let bytes = serde_json::to_vec_pretty(relays).map_err(Error::Serialize)?; + let mut slice: &[u8] = bytes.as_slice(); + let _ = tokio::io::copy(&mut slice, &mut file) + .await + .map_err(Error::WriteRelayCache)?; + Ok(()) + } +} diff --git a/mullvad-types/src/endpoint.rs b/mullvad-types/src/endpoint.rs index 75e021de8f..bfba11a481 100644 --- a/mullvad-types/src/endpoint.rs +++ b/mullvad-types/src/endpoint.rs @@ -11,12 +11,16 @@ use crate::relay_list::{OpenVpnEndpointData, WireguardEndpointData}; #[derive(Debug, Clone)] pub enum MullvadEndpoint { OpenVpn(Endpoint), - Wireguard { - peer: wireguard::PeerConfig, - exit_peer: Option<wireguard::PeerConfig>, - ipv4_gateway: Ipv4Addr, - ipv6_gateway: Ipv6Addr, - }, + Wireguard(MullvadWireguardEndpoint), +} + +/// Contains WireGuard server data needed to connect to a WireGuard endpoint +#[derive(Debug, Clone)] +pub struct MullvadWireguardEndpoint { + pub peer: wireguard::PeerConfig, + pub exit_peer: Option<wireguard::PeerConfig>, + pub ipv4_gateway: Ipv4Addr, + pub ipv6_gateway: Ipv6Addr, } impl MullvadEndpoint { @@ -24,13 +28,22 @@ impl MullvadEndpoint { pub fn to_endpoint(&self) -> Endpoint { match self { MullvadEndpoint::OpenVpn(endpoint) => *endpoint, - MullvadEndpoint::Wireguard { peer, .. } => Endpoint::new( - peer.endpoint.ip(), - peer.endpoint.port(), + MullvadEndpoint::Wireguard(wireguard_relay) => Endpoint::new( + wireguard_relay.peer.endpoint.ip(), + wireguard_relay.peer.endpoint.port(), TransportProtocol::Udp, ), } } + + pub fn unwrap_wireguard(&self) -> &MullvadWireguardEndpoint { + match self { + Self::Wireguard(endpoint) => endpoint, + other => { + panic!("Expected WireGuard enum variant but got {:?}", other); + } + } + } } /// TunnelEndpointData contains data required to connect to a given tunnel endpoint. /// Different endpoint types can require different types of data. diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index 18cdfda2d9..6962951eb1 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -3,7 +3,7 @@ use crate::{ location::{CityCode, CountryCode, Hostname}, - relay_list::{OpenVpnEndpointData, Relay, WireguardEndpointData}, + relay_list::{OpenVpnEndpointData, Relay}, CustomTunnelEndpoint, }; #[cfg(target_os = "android")] @@ -380,48 +380,6 @@ impl fmt::Display for LocationConstraint { } } -/// Deprecated. Contains protocol-specific constraints for relay selection. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -pub enum TunnelConstraints { - #[serde(rename = "openvpn")] - OpenVpn(OpenVpnConstraints), - #[serde(rename = "wireguard")] - Wireguard(WireguardConstraints), -} - -impl fmt::Display for TunnelConstraints { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - TunnelConstraints::OpenVpn(openvpn_constraints) => { - write!(f, "OpenVPN over ")?; - openvpn_constraints.fmt(f) - } - TunnelConstraints::Wireguard(wireguard_constraints) => { - write!(f, "Wireguard over ")?; - wireguard_constraints.fmt(f) - } - } - } -} - -impl Match<OpenVpnEndpointData> for TunnelConstraints { - fn matches(&self, endpoint: &OpenVpnEndpointData) -> bool { - match *self { - TunnelConstraints::OpenVpn(ref constraints) => constraints.matches(endpoint), - _ => false, - } - } -} - -impl Match<WireguardEndpointData> for TunnelConstraints { - fn matches(&self, endpoint: &WireguardEndpointData) -> bool { - match *self { - TunnelConstraints::Wireguard(ref constraints) => constraints.matches(endpoint), - _ => false, - } - } -} - #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct TransportPort { pub protocol: TransportProtocol, @@ -502,24 +460,6 @@ impl fmt::Display for WireguardConstraints { } } -impl Match<WireguardEndpointData> for WireguardConstraints { - fn matches(&self, endpoint: &WireguardEndpointData) -> bool { - match self.port { - Constraint::Any => true, - Constraint::Only(transport_port) => { - transport_port.protocol == endpoint.protocol - && match transport_port.port { - Constraint::Any => true, - Constraint::Only(port) => endpoint - .port_ranges - .iter() - .any(|range| (port >= range.0 && port <= range.1)), - } - } - } - } -} - /// Specifies a specific endpoint or [`BridgeConstraints`] to use when `mullvad-daemon` selects a /// bridge server. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] |
