From 7992055557e32fdbed2f8267ae6fb1b57ed9c1d8 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Tue, 9 Jul 2024 11:10:08 +0200 Subject: [PATCH] Implement feature indicators in daemon - Add a GetFeatureIndicators gRPC call that get's the current set of active "features" that should be shown in the UI. - Extend the TunnelState with a FeatureIndicators value. Clients who listens for TunnelState events will get updates automatically. --- mullvad-cli/src/cmds/status.rs | 12 +- mullvad-cli/src/format.rs | 54 +++--- mullvad-daemon/src/custom_list.rs | 11 +- mullvad-daemon/src/lib.rs | 163 +++++++++++++++--- mullvad-daemon/src/management_interface.rs | 17 ++ .../proto/management_interface.proto | 31 +++- mullvad-management-interface/src/client.rs | 10 ++ .../src/types/conversions/features.rs | 64 +++++++ .../src/types/conversions/mod.rs | 1 + .../src/types/conversions/states.rs | 50 ++++-- mullvad-types/src/features.rs | 62 +++++++ mullvad-types/src/lib.rs | 1 + mullvad-types/src/settings/dns.rs | 21 +++ mullvad-types/src/states.rs | 60 ++++++- 14 files changed, 468 insertions(+), 89 deletions(-) create mode 100644 mullvad-management-interface/src/types/conversions/features.rs create mode 100644 mullvad-types/src/features.rs diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index 939beeca4ac7..96631d02d0be 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -54,9 +54,15 @@ impl Status { // Do print an updated state if the lockdown setting was changed ) if was_locked_down == locked_down => continue, ( - Some(TunnelState::Connected { .. }), - TunnelState::Connected { .. }, - ) => continue, + Some(TunnelState::Connected { + feature_indicators: old_feature_indicators, + .. + }), + TunnelState::Connected { + feature_indicators, .. + }, + // Do print an updated state if the feature indicators changed + ) if old_feature_indicators == feature_indicators => continue, _ => {} } format::print_state(&new_state, args.verbose); diff --git a/mullvad-cli/src/format.rs b/mullvad-cli/src/format.rs index 86606839190c..63395ea87afc 100644 --- a/mullvad-cli/src/format.rs +++ b/mullvad-cli/src/format.rs @@ -1,4 +1,8 @@ -use mullvad_types::{auth_failed::AuthFailed, location::GeoIpLocation, states::TunnelState}; +use itertools::Itertools; +use mullvad_types::{ + auth_failed::AuthFailed, features::FeatureIndicators, location::GeoIpLocation, + states::TunnelState, +}; use talpid_types::{ net::{Endpoint, TunnelEndpoint}, tunnel::ErrorState, @@ -19,18 +23,30 @@ pub fn print_state(state: &TunnelState, verbose: bool) { match state { Error(error) => print_error_state(error), - Connected { endpoint, location } => { + Connected { + endpoint, + location, + feature_indicators, + } => { println!( "Connected to {}", format_relay_connection(endpoint, location.as_ref(), verbose) ); if verbose { + println!( + "Active features: {}", + format_feature_indicators(feature_indicators) + ); if let Some(tunnel_interface) = &endpoint.tunnel_interface { println!("Tunnel interface: {tunnel_interface}") } } } - Connecting { endpoint, location } => { + Connecting { + endpoint, + location, + feature_indicators: _, + } => { let ellipsis = if !verbose { "..." } else { "" }; println!( "Connecting to {}{ellipsis}", @@ -166,44 +182,30 @@ fn format_relay_connection( } else { String::new() }; - let quantum_resistant = if !verbose { - "" - } else if endpoint.quantum_resistant { - "\nQuantum resistant tunnel: yes" - } else { - "\nQuantum resistant tunnel: no" - }; - - #[cfg(daita)] - let daita = if !verbose { - "" - } else if endpoint.daita { - "\nDAITA: yes" - } else { - "\nDAITA: no" - }; - #[cfg(not(daita))] - let daita = ""; let mut bridge_type = String::new(); - let mut obfuscator_type = String::new(); if verbose { if let Some(bridge) = &endpoint.proxy { bridge_type = format!("\nBridge type: {}", bridge.proxy_type); } - if let Some(obfuscator) = &endpoint.obfuscation { - obfuscator_type = format!("\nObfuscator: {}", obfuscator.obfuscation_type); - } } format!( - "{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{quantum_resistant}{daita}{bridge_type}{obfuscator_type}", + "{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{bridge_type}", first_hop = first_hop.unwrap_or_default(), bridge = bridge.unwrap_or_default(), obfuscator = obfuscator.unwrap_or_default(), ) } +fn format_feature_indicators(feature_indicators: &FeatureIndicators) -> String { + feature_indicators + .active_features() + // Sort the features alphabetically (Just to have some order, arbitrarily chosen) + .sorted_by_key(|feature| feature.to_string()) + .join(", ") +} + fn format_endpoint(hostname: Option<&str>, endpoint: &Endpoint, verbose: bool) -> String { match (hostname, verbose) { (Some(hostname), true) => format!("{hostname} ({endpoint})"), diff --git a/mullvad-daemon/src/custom_list.rs b/mullvad-daemon/src/custom_list.rs index f3e92f3c9761..459ef9b93295 100644 --- a/mullvad-daemon/src/custom_list.rs +++ b/mullvad-daemon/src/custom_list.rs @@ -108,7 +108,6 @@ where /// /// If `custom_list_id` is `Some`, only changes to that custom list will trigger a reconnect. fn change_should_cause_reconnect(&self, custom_list_id: Option) -> bool { - use mullvad_types::states::TunnelState; let mut need_to_reconnect = false; let RelaySettings::Normal(relay_settings) = &self.settings.relay_settings else { @@ -121,15 +120,7 @@ where need_to_reconnect |= custom_list_id.map(|id| &id == list_id).unwrap_or(true); } - if let TunnelState::Connecting { - endpoint, - location: _, - } - | TunnelState::Connected { - endpoint, - location: _, - } = &self.tunnel_state - { + if let Some(endpoint) = self.tunnel_state.endpoint() { match endpoint.tunnel_type { TunnelType::Wireguard => { if relay_settings.wireguard_constraints.multihop() { diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 2e685d778f34..ba83da27f50f 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -52,12 +52,13 @@ use mullvad_types::{ auth_failed::AuthFailed, custom_list::CustomList, device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent}, + features::{FeatureIndicator, FeatureIndicators}, location::{GeoIpLocation, LocationEventData}, relay_constraints::{ BridgeSettings, BridgeState, BridgeType, ObfuscationSettings, RelayOverride, RelaySettings, }, relay_list::RelayList, - settings::{DnsOptions, Settings}, + settings::{DnsOptions, DnsState, Settings}, states::{TargetState, TunnelState}, version::{AppVersion, AppVersionInfo}, wireguard::{PublicKey, QuantumResistantState, RotationInterval}, @@ -85,7 +86,7 @@ use talpid_types::android::AndroidContext; #[cfg(target_os = "windows")] use talpid_types::split_tunnel::ExcludedProcess; use talpid_types::{ - net::{IpVersion, TunnelEndpoint, TunnelType}, + net::{IpVersion, ObfuscationType, TunnelType}, tunnel::{ErrorStateCause, TunnelStateTransition}, ErrorExt, }; @@ -369,6 +370,8 @@ pub enum DaemonCommand { ApplyJsonSettings(ResponseTx<(), settings::patch::Error>, String), /// Return a JSON blob containing all overridable settings, if there are any ExportJsonSettings(ResponseTx), + /// Request the current feature indicators. + GetFeatureIndicators(oneshot::Sender), } /// All events that can happen in the daemon. Sent from various threads and exposed interfaces. @@ -393,6 +396,8 @@ pub(crate) enum InternalDaemonEvent { DeviceMigrationEvent(Result), /// A geographical location has has been received from am.i.mullvad.net LocationEvent(LocationEventData), + /// A generic event for when any settings change. + SettingsChanged, /// The split tunnel paths or state were updated. #[cfg(any(windows, target_os = "android", target_os = "macos"))] ExcludedPathsEvent(ExcludedPathsUpdate, oneshot::Sender>), @@ -755,6 +760,13 @@ where let _ = param_gen_tx.unbounded_send(settings.tunnel_options.to_owned()); }); + // Register a listener for generic settings changes. + // This is useful for example for updating feature indicators when the settings change. + let settings_changed_event_sender = internal_event_tx.clone(); + settings.register_change_listener(move |_settings| { + let _ = settings_changed_event_sender.send(InternalDaemonEvent::SettingsChanged); + }); + let (offline_state_tx, offline_state_rx) = mpsc::unbounded(); #[cfg(target_os = "windows")] let (volume_update_tx, volume_update_rx) = mpsc::unbounded(); @@ -947,6 +959,9 @@ where } => self.handle_access_method_event(event, endpoint_active_tx), DeviceMigrationEvent(event) => self.handle_device_migration_event(event), LocationEvent(location_data) => self.handle_location_event(location_data), + SettingsChanged => { + self.handle_feature_indicator_event(); + } #[cfg(any(windows, target_os = "android", target_os = "macos"))] ExcludedPathsEvent(update, tx) => self.handle_new_excluded_paths(update, tx).await, } @@ -969,11 +984,13 @@ where TunnelStateTransition::Connecting(endpoint) => TunnelState::Connecting { endpoint, location: self.parameters_generator.get_last_location().await, + feature_indicators: self.get_feature_indicators(), + }, + TunnelStateTransition::Connected(endpoint) => TunnelState::Connected { + endpoint, + location: self.parameters_generator.get_last_location().await, + feature_indicators: self.get_feature_indicators(), }, - TunnelStateTransition::Connected(endpoint) => { - let location = self.parameters_generator.get_last_location().await; - TunnelState::Connected { endpoint, location } - } TunnelStateTransition::Disconnecting(after_disconnect) => { TunnelState::Disconnecting(after_disconnect) } @@ -1097,6 +1114,23 @@ where .notify_new_state(self.tunnel_state.clone()); } + /// Update the set of feature indicators. + fn handle_feature_indicator_event(&mut self) { + // Note: If the current tunnel state carries information about active feature indicators, + // we should care to update the known set of feature indicators (i.e. in the connecting / + // connected state). Otherwise, we can just skip broadcasting a new tunnel state. + if let Some(current_feature_indicators) = self.tunnel_state.get_feature_indicators() { + let new_feature_indicators = self.get_feature_indicators(); + if *current_feature_indicators != new_feature_indicators { + // Make sure to update the daemon's actual tunnel state. Otherwise feature indicator changes won't be persisted. + self.tunnel_state + .set_feature_indicators(new_feature_indicators); + self.event_listener + .notify_new_state(self.tunnel_state.clone()); + } + } + } + fn reset_rpc_sockets_on_tunnel_state_transition( &mut self, tunnel_state_transition: &TunnelStateTransition, @@ -1248,6 +1282,7 @@ where } ApplyJsonSettings(tx, blob) => self.on_apply_json_settings(tx, blob).await, ExportJsonSettings(tx) => self.on_export_json_settings(tx), + GetFeatureIndicators(tx) => self.on_get_feature_indicators(tx), } } @@ -2718,6 +2753,14 @@ where Self::oneshot_send(tx, result, "export_json_settings response"); } + fn on_get_feature_indicators(&self, tx: oneshot::Sender) { + Self::oneshot_send( + tx, + self.get_feature_indicators(), + "get_feature_indicators response", + ); + } + /// Set the target state of the client. If it changed trigger the operations needed to /// progress towards that state. /// Returns a bool representing whether or not a state change was initiated. @@ -2752,30 +2795,15 @@ where } } - fn get_connected_tunnel_type(&self) -> Option { - if let TunnelState::Connected { - endpoint: TunnelEndpoint { tunnel_type, .. }, - .. - } = self.tunnel_state - { - Some(tunnel_type) - } else { - None + const fn get_connected_tunnel_type(&self) -> Option { + match self.tunnel_state.get_tunnel_type() { + Some(tunnel_type) if self.tunnel_state.is_connected() => Some(tunnel_type), + Some(_) | None => None, } } - fn get_target_tunnel_type(&self) -> Option { - match self.tunnel_state { - TunnelState::Connected { - endpoint: TunnelEndpoint { tunnel_type, .. }, - .. - } - | TunnelState::Connecting { - endpoint: TunnelEndpoint { tunnel_type, .. }, - .. - } => Some(tunnel_type), - _ => None, - } + const fn get_target_tunnel_type(&self) -> Option { + self.tunnel_state.get_tunnel_type() } fn send_tunnel_command(&self, command: TunnelCommand) { @@ -2790,6 +2818,87 @@ where tx: self.tx.clone(), } } + + /// Source all active [`FeatureIndicators`]. + /// + /// Note that [`FeatureIndicators`] only affect an active connection, which means that when the + /// daemon is disconnected while calling this function the caller will see an empty set of + /// [`FeatureIndicators`]. + fn get_feature_indicators(&self) -> FeatureIndicators { + // Check if there is an active tunnel. + let Some(endpoint) = self.tunnel_state.endpoint() else { + // If there is not, no features are actually active and thus should not be displayed. + return Default::default(); + }; + let settings = self.settings.to_settings(); + + #[cfg(any(windows, target_os = "android", target_os = "macos"))] + let split_tunneling = self.settings.split_tunnel.enable_exclusions; + #[cfg(not(any(windows, target_os = "android", target_os = "macos")))] + let split_tunneling = false; + + let lockdown_mode = settings.block_when_disconnected; + let lan_sharing = settings.allow_lan; + let dns_content_blockers = settings + .tunnel_options + .dns_options + .default_options + .any_blockers_enabled(); + let custom_dns = settings.tunnel_options.dns_options.state == DnsState::Custom; + let server_ip_override = !settings.relay_overrides.is_empty(); + + let generic_features = [ + (split_tunneling, FeatureIndicator::SplitTunneling), + (lockdown_mode, FeatureIndicator::LockdownMode), + (lan_sharing, FeatureIndicator::LanSharing), + (dns_content_blockers, FeatureIndicator::DnsContentBlockers), + (custom_dns, FeatureIndicator::CustomDns), + (server_ip_override, FeatureIndicator::ServerIpOverride), + ]; + + // Pick protocol-specific features and whether they are currently enabled. + let protocol_features = match endpoint.tunnel_type { + TunnelType::OpenVpn => { + let bridge_mode = endpoint.proxy.is_some(); + let mss_fix = settings.tunnel_options.openvpn.mssfix.is_some(); + + vec![ + (bridge_mode, FeatureIndicator::BridgeMode), + (mss_fix, FeatureIndicator::CustomMssFix), + ] + } + TunnelType::Wireguard => { + let quantum_resistant = endpoint.quantum_resistant; + let multihop = endpoint.entry_endpoint.is_some(); + let udp_tcp = endpoint + .obfuscation + .as_ref() + .filter(|obfuscation| obfuscation.obfuscation_type == ObfuscationType::Udp2Tcp) + .is_some(); + + let mtu = settings.tunnel_options.wireguard.mtu.is_some(); + + #[cfg(daita)] + let daita = endpoint.daita; + + vec![ + (quantum_resistant, FeatureIndicator::QuantumResistance), + (multihop, FeatureIndicator::Multihop), + (udp_tcp, FeatureIndicator::Udp2Tcp), + (mtu, FeatureIndicator::CustomMtu), + #[cfg(daita)] + (daita, FeatureIndicator::Daita), + ] + } + }; + + // use the booleans to filter into a list of only the active features + generic_features + .into_iter() + .chain(protocol_features) + .filter_map(|(active, feature)| active.then_some(feature)) + .collect() + } } #[derive(Clone)] diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index bc6ebe82874b..a7e2b23f4f4f 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -1022,6 +1022,23 @@ impl ManagementService for ManagementServiceImpl { log::error!("Called `verify_play_purchase` on non-Android platform"); Ok(Response::new(())) } + + async fn get_feature_indicators( + &self, + _: Request<()>, + ) -> ServiceResult { + log::debug!("get_feature_indicators"); + + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetFeatureIndicators(tx))?; + + let feature_indicators = self + .wait_for_result(rx) + .await + .map(types::FeatureIndicators::from)?; + + Ok(Response::new(feature_indicators)) + } } impl ManagementServiceImpl { diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 798e3a3beeb7..7e78cffde868 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -119,6 +119,9 @@ service ManagementService { rpc ApplyJsonSettings(google.protobuf.StringValue) returns (google.protobuf.Empty) {} // Return a JSON blob containing all overridable settings, if there are any rpc ExportJsonSettings(google.protobuf.Empty) returns (google.protobuf.StringValue) {} + + // Get current feature indicators + rpc GetFeatureIndicators(google.protobuf.Empty) returns (FeatureIndicators) {} } message UUID { string value = 1; } @@ -200,8 +203,14 @@ message TunnelState { GeoIpLocation disconnected_location = 1; bool locked_down = 2; } - message Connecting { TunnelStateRelayInfo relay_info = 1; } - message Connected { TunnelStateRelayInfo relay_info = 1; } + message Connecting { + TunnelStateRelayInfo relay_info = 1; + FeatureIndicators feature_indicators = 2; + } + message Connected { + TunnelStateRelayInfo relay_info = 1; + FeatureIndicators feature_indicators = 2; + } message Disconnecting { AfterDisconnect after_disconnect = 1; } message Error { ErrorState error_state = 1; } @@ -236,6 +245,24 @@ message TunnelEndpoint { bool daita = 9; } +message FeatureIndicators { repeated FeatureIndicator active_features = 1; } + +enum FeatureIndicator { + QUANTUM_RESISTANCE = 0; + MULTIHOP = 1; + BRIDGE_MODE = 2; + SPLIT_TUNNELING = 3; + LOCKDOWN_MODE = 4; + UDP_2_TCP = 5; + LAN_SHARING = 6; + DNS_CONTENT_BLOCKERS = 7; + CUSTOM_DNS = 8; + SERVER_IP_OVERRIDE = 9; + CUSTOM_MTU = 10; + CUSTOM_MSS_FIX = 11; + DAITA = 12; +} + enum ObfuscationType { UDP2TCP = 0; } diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index 4db40cac521e..b0876093fa2a 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -20,6 +20,7 @@ use mullvad_types::{ account::{AccountData, AccountToken, VoucherSubmission}, custom_list::{CustomList, Id}, device::{Device, DeviceId, DeviceState}, + features::FeatureIndicators, relay_constraints::{ BridgeSettings, BridgeState, ObfuscationSettings, RelayOverride, RelaySettings, }, @@ -741,6 +742,15 @@ impl MullvadProxyClient { let blob = self.0.export_json_settings(()).await.map_err(Error::Rpc)?; Ok(blob.into_inner()) } + + pub async fn get_feature_indicators(&mut self) -> Result { + self.0 + .get_feature_indicators(()) + .await + .map_err(Error::Rpc) + .map(|response| response.into_inner()) + .map(FeatureIndicators::from) + } } #[cfg(not(target_os = "android"))] diff --git a/mullvad-management-interface/src/types/conversions/features.rs b/mullvad-management-interface/src/types/conversions/features.rs new file mode 100644 index 000000000000..ae04fc9099e1 --- /dev/null +++ b/mullvad-management-interface/src/types/conversions/features.rs @@ -0,0 +1,64 @@ +use crate::types::proto; + +impl From for proto::FeatureIndicator { + fn from(feature: mullvad_types::features::FeatureIndicator) -> Self { + use proto::FeatureIndicator::*; + match feature { + mullvad_types::features::FeatureIndicator::QuantumResistance => QuantumResistance, + mullvad_types::features::FeatureIndicator::Multihop => Multihop, + mullvad_types::features::FeatureIndicator::BridgeMode => BridgeMode, + mullvad_types::features::FeatureIndicator::SplitTunneling => SplitTunneling, + mullvad_types::features::FeatureIndicator::LockdownMode => LockdownMode, + mullvad_types::features::FeatureIndicator::Udp2Tcp => Udp2Tcp, + mullvad_types::features::FeatureIndicator::LanSharing => LanSharing, + mullvad_types::features::FeatureIndicator::DnsContentBlockers => DnsContentBlockers, + mullvad_types::features::FeatureIndicator::CustomDns => CustomDns, + mullvad_types::features::FeatureIndicator::ServerIpOverride => ServerIpOverride, + mullvad_types::features::FeatureIndicator::CustomMtu => CustomMtu, + mullvad_types::features::FeatureIndicator::CustomMssFix => CustomMssFix, + mullvad_types::features::FeatureIndicator::Daita => Daita, + } + } +} + +impl From for mullvad_types::features::FeatureIndicator { + fn from(feature: proto::FeatureIndicator) -> Self { + match feature { + proto::FeatureIndicator::QuantumResistance => Self::QuantumResistance, + proto::FeatureIndicator::Multihop => Self::Multihop, + proto::FeatureIndicator::BridgeMode => Self::BridgeMode, + proto::FeatureIndicator::SplitTunneling => Self::SplitTunneling, + proto::FeatureIndicator::LockdownMode => Self::LockdownMode, + proto::FeatureIndicator::Udp2Tcp => Self::Udp2Tcp, + proto::FeatureIndicator::LanSharing => Self::LanSharing, + proto::FeatureIndicator::DnsContentBlockers => Self::DnsContentBlockers, + proto::FeatureIndicator::CustomDns => Self::CustomDns, + proto::FeatureIndicator::ServerIpOverride => Self::ServerIpOverride, + proto::FeatureIndicator::CustomMtu => Self::CustomMtu, + proto::FeatureIndicator::CustomMssFix => Self::CustomMssFix, + proto::FeatureIndicator::Daita => Self::Daita, + } + } +} + +impl From for mullvad_types::features::FeatureIndicators { + fn from(features: proto::FeatureIndicators) -> Self { + features + .active_features() + .map(mullvad_types::features::FeatureIndicator::from) + .collect() + } +} + +impl From for proto::FeatureIndicators { + fn from(features: mullvad_types::features::FeatureIndicators) -> Self { + let mut proto_features = Self::default(); + + features + .active_features() + .map(proto::FeatureIndicator::from) + .for_each(|feature| proto_features.push_active_features(feature)); + + proto_features + } +} diff --git a/mullvad-management-interface/src/types/conversions/mod.rs b/mullvad-management-interface/src/types/conversions/mod.rs index 02281846c3fd..0654cbb64123 100644 --- a/mullvad-management-interface/src/types/conversions/mod.rs +++ b/mullvad-management-interface/src/types/conversions/mod.rs @@ -5,6 +5,7 @@ mod account; mod custom_list; mod custom_tunnel; mod device; +mod features; mod location; mod net; pub mod relay_constraints; diff --git a/mullvad-management-interface/src/types/conversions/states.rs b/mullvad-management-interface/src/types/conversions/states.rs index 80dbb8bf2f2f..777c661c6d3f 100644 --- a/mullvad-management-interface/src/types/conversions/states.rs +++ b/mullvad-management-interface/src/types/conversions/states.rs @@ -39,22 +39,28 @@ impl From for proto::TunnelState { disconnected_location: disconnected_location.map(proto::GeoIpLocation::from), locked_down, }), - MullvadTunnelState::Connecting { endpoint, location } => { - proto::tunnel_state::State::Connecting(proto::tunnel_state::Connecting { - relay_info: Some(proto::TunnelStateRelayInfo { - tunnel_endpoint: Some(proto::TunnelEndpoint::from(endpoint)), - location: location.map(proto::GeoIpLocation::from), - }), - }) - } - MullvadTunnelState::Connected { endpoint, location } => { - proto::tunnel_state::State::Connected(proto::tunnel_state::Connected { - relay_info: Some(proto::TunnelStateRelayInfo { - tunnel_endpoint: Some(proto::TunnelEndpoint::from(endpoint)), - location: location.map(proto::GeoIpLocation::from), - }), - }) - } + MullvadTunnelState::Connecting { + endpoint, + location, + feature_indicators, + } => proto::tunnel_state::State::Connecting(proto::tunnel_state::Connecting { + relay_info: Some(proto::TunnelStateRelayInfo { + tunnel_endpoint: Some(proto::TunnelEndpoint::from(endpoint)), + location: location.map(proto::GeoIpLocation::from), + }), + feature_indicators: Some(proto::FeatureIndicators::from(feature_indicators)), + }), + MullvadTunnelState::Connected { + endpoint, + location, + feature_indicators, + } => proto::tunnel_state::State::Connected(proto::tunnel_state::Connected { + relay_info: Some(proto::TunnelStateRelayInfo { + tunnel_endpoint: Some(proto::TunnelEndpoint::from(endpoint)), + location: location.map(proto::GeoIpLocation::from), + }), + feature_indicators: Some(proto::FeatureIndicators::from(feature_indicators)), + }), MullvadTunnelState::Disconnecting(after_disconnect) => { proto::tunnel_state::State::Disconnecting(proto::tunnel_state::Disconnecting { after_disconnect: match after_disconnect { @@ -233,11 +239,17 @@ impl TryFrom for mullvad_types::states::TunnelState { tunnel_endpoint: Some(tunnel_endpoint), location, }), + feature_indicators, })) => MullvadState::Connecting { endpoint: talpid_net::TunnelEndpoint::try_from(tunnel_endpoint)?, location: location .map(mullvad_types::location::GeoIpLocation::try_from) .transpose()?, + feature_indicators: feature_indicators + .map(mullvad_types::features::FeatureIndicators::from) + .ok_or(FromProtobufTypeError::InvalidArgument( + "Missing feature indicators", + ))?, }, Some(proto::tunnel_state::State::Connected(proto::tunnel_state::Connected { relay_info: @@ -245,11 +257,17 @@ impl TryFrom for mullvad_types::states::TunnelState { tunnel_endpoint: Some(tunnel_endpoint), location, }), + feature_indicators, })) => MullvadState::Connected { endpoint: talpid_net::TunnelEndpoint::try_from(tunnel_endpoint)?, location: location .map(mullvad_types::location::GeoIpLocation::try_from) .transpose()?, + feature_indicators: feature_indicators + .map(mullvad_types::features::FeatureIndicators::from) + .ok_or(FromProtobufTypeError::InvalidArgument( + "Missing feature indicators", + ))?, }, Some(proto::tunnel_state::State::Disconnecting( proto::tunnel_state::Disconnecting { after_disconnect }, diff --git a/mullvad-types/src/features.rs b/mullvad-types/src/features.rs new file mode 100644 index 000000000000..9a6b7c7e6466 --- /dev/null +++ b/mullvad-types/src/features.rs @@ -0,0 +1,62 @@ +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; + +/// Feature indicators are active settings that should be shown to the user to make them aware of +/// what is affecting their connection at any given time. +/// +/// Note that the feature indicators are not ordered. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FeatureIndicators(HashSet); + +impl FeatureIndicators { + pub fn active_features(&self) -> impl Iterator { + self.0.clone().into_iter() + } +} + +impl FromIterator for FeatureIndicators { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// All possible feature indicators. These represent a subset of all VPN settings in a +/// non-technical fashion. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum FeatureIndicator { + QuantumResistance, + Multihop, + BridgeMode, + SplitTunneling, + LockdownMode, + Udp2Tcp, + LanSharing, + DnsContentBlockers, + CustomDns, + ServerIpOverride, + CustomMtu, + CustomMssFix, + Daita, +} + +impl std::fmt::Display for FeatureIndicator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let feature = match self { + FeatureIndicator::QuantumResistance => "Quantum Resistance", + FeatureIndicator::Multihop => "Multihop", + FeatureIndicator::BridgeMode => "Bridge Mode", + FeatureIndicator::SplitTunneling => "Split Tunneling", + FeatureIndicator::LockdownMode => "Lockdown Mode", + FeatureIndicator::Udp2Tcp => "Udp2Tcp", + FeatureIndicator::LanSharing => "LAN Sharing", + FeatureIndicator::DnsContentBlockers => "Dns Content Blocker", + FeatureIndicator::CustomDns => "Custom Dns", + FeatureIndicator::ServerIpOverride => "Server Ip Override", + FeatureIndicator::CustomMtu => "Custom MTU", + FeatureIndicator::CustomMssFix => "Custom MSS", + FeatureIndicator::Daita => "DAITA", + }; + write!(f, "{feature}") + } +} diff --git a/mullvad-types/src/lib.rs b/mullvad-types/src/lib.rs index d1a50fa0ea02..e9fde63653d2 100644 --- a/mullvad-types/src/lib.rs +++ b/mullvad-types/src/lib.rs @@ -5,6 +5,7 @@ pub mod constraints; pub mod custom_list; pub mod device; pub mod endpoint; +pub mod features; pub mod location; pub mod relay_constraints; pub mod relay_list; diff --git a/mullvad-types/src/settings/dns.rs b/mullvad-types/src/settings/dns.rs index 82d6fe35af29..3136efd79fd7 100644 --- a/mullvad-types/src/settings/dns.rs +++ b/mullvad-types/src/settings/dns.rs @@ -35,3 +35,24 @@ pub struct DefaultDnsOptions { pub struct CustomDnsOptions { pub addresses: Vec, } + +impl DefaultDnsOptions { + /// Return whether any content blockers are enabled. + pub fn any_blockers_enabled(&self) -> bool { + let DefaultDnsOptions { + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + block_social_media, + } = *self; + + block_ads + || block_trackers + || block_malware + || block_adult_content + || block_gambling + || block_social_media + } +} diff --git a/mullvad-types/src/states.rs b/mullvad-types/src/states.rs index f31b48aa2f3c..cae80ae9b610 100644 --- a/mullvad-types/src/states.rs +++ b/mullvad-types/src/states.rs @@ -1,8 +1,8 @@ -use crate::location::GeoIpLocation; +use crate::{features::FeatureIndicators, location::GeoIpLocation}; use serde::{Deserialize, Serialize}; use std::fmt; use talpid_types::{ - net::TunnelEndpoint, + net::{TunnelEndpoint, TunnelType}, tunnel::{ActionAfterDisconnect, ErrorState}, }; @@ -38,10 +38,12 @@ pub enum TunnelState { Connecting { endpoint: TunnelEndpoint, location: Option, + feature_indicators: FeatureIndicators, }, Connected { endpoint: TunnelEndpoint, location: Option, + feature_indicators: FeatureIndicators, }, Disconnecting(ActionAfterDisconnect), Error(ErrorState), @@ -49,17 +51,65 @@ pub enum TunnelState { impl TunnelState { /// Returns true if the tunnel state is in the error state. - pub fn is_in_error_state(&self) -> bool { + pub const fn is_in_error_state(&self) -> bool { matches!(self, TunnelState::Error(_)) } /// Returns true if the tunnel state is in the connected state. - pub fn is_connected(&self) -> bool { + pub const fn is_connected(&self) -> bool { matches!(self, TunnelState::Connected { .. }) } /// Returns true if the tunnel state is in the disconnected state. - pub fn is_disconnected(&self) -> bool { + pub const fn is_disconnected(&self) -> bool { matches!(self, TunnelState::Disconnected { .. }) } + + /// Returns the tunnel endpoint for an active connection. + /// This value exists in the connecting and connected states. + pub const fn endpoint(&self) -> Option<&TunnelEndpoint> { + match self { + TunnelState::Connecting { endpoint, .. } | TunnelState::Connected { endpoint, .. } => { + Some(endpoint) + } + _ => None, + } + } + + /// Returns the tunnel type for an active connection. + /// This value exists in the connecting and connected states. + pub const fn get_tunnel_type(&self) -> Option { + match self.endpoint() { + Some(endpoint) => Some(endpoint.tunnel_type), + None => None, + } + } + + /// Returns the current feature indicators for an active connection. + /// This value exists in the connecting and connected states. + pub const fn get_feature_indicators(&self) -> Option<&FeatureIndicators> { + match self { + TunnelState::Connecting { + feature_indicators, .. + } + | TunnelState::Connected { + feature_indicators, .. + } => Some(feature_indicators), + _ => None, + } + } + + /// Update the set of feature indicators for this [`TunnelState`]. This is only applicable in + /// the connecting and connected states. + pub fn set_feature_indicators(&mut self, new_feature_indicators: FeatureIndicators) { + if let TunnelState::Connecting { + feature_indicators, .. + } + | TunnelState::Connected { + feature_indicators, .. + } = self + { + *feature_indicators = new_feature_indicators; + } + } }