Skip to content

Commit d567fe1

Browse files
Merge branch 'implement-feature-list-in-rpc-des-1082'
2 parents 2b59f82 + 7992055 commit d567fe1

File tree

14 files changed

+468
-89
lines changed

14 files changed

+468
-89
lines changed

mullvad-cli/src/cmds/status.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,15 @@ impl Status {
5454
// Do print an updated state if the lockdown setting was changed
5555
) if was_locked_down == locked_down => continue,
5656
(
57-
Some(TunnelState::Connected { .. }),
58-
TunnelState::Connected { .. },
59-
) => continue,
57+
Some(TunnelState::Connected {
58+
feature_indicators: old_feature_indicators,
59+
..
60+
}),
61+
TunnelState::Connected {
62+
feature_indicators, ..
63+
},
64+
// Do print an updated state if the feature indicators changed
65+
) if old_feature_indicators == feature_indicators => continue,
6066
_ => {}
6167
}
6268
format::print_state(&new_state, args.verbose);

mullvad-cli/src/format.rs

+28-26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use mullvad_types::{auth_failed::AuthFailed, location::GeoIpLocation, states::TunnelState};
1+
use itertools::Itertools;
2+
use mullvad_types::{
3+
auth_failed::AuthFailed, features::FeatureIndicators, location::GeoIpLocation,
4+
states::TunnelState,
5+
};
26
use talpid_types::{
37
net::{Endpoint, TunnelEndpoint},
48
tunnel::ErrorState,
@@ -19,18 +23,30 @@ pub fn print_state(state: &TunnelState, verbose: bool) {
1923

2024
match state {
2125
Error(error) => print_error_state(error),
22-
Connected { endpoint, location } => {
26+
Connected {
27+
endpoint,
28+
location,
29+
feature_indicators,
30+
} => {
2331
println!(
2432
"Connected to {}",
2533
format_relay_connection(endpoint, location.as_ref(), verbose)
2634
);
2735
if verbose {
36+
println!(
37+
"Active features: {}",
38+
format_feature_indicators(feature_indicators)
39+
);
2840
if let Some(tunnel_interface) = &endpoint.tunnel_interface {
2941
println!("Tunnel interface: {tunnel_interface}")
3042
}
3143
}
3244
}
33-
Connecting { endpoint, location } => {
45+
Connecting {
46+
endpoint,
47+
location,
48+
feature_indicators: _,
49+
} => {
3450
let ellipsis = if !verbose { "..." } else { "" };
3551
println!(
3652
"Connecting to {}{ellipsis}",
@@ -166,44 +182,30 @@ fn format_relay_connection(
166182
} else {
167183
String::new()
168184
};
169-
let quantum_resistant = if !verbose {
170-
""
171-
} else if endpoint.quantum_resistant {
172-
"\nQuantum resistant tunnel: yes"
173-
} else {
174-
"\nQuantum resistant tunnel: no"
175-
};
176-
177-
#[cfg(daita)]
178-
let daita = if !verbose {
179-
""
180-
} else if endpoint.daita {
181-
"\nDAITA: yes"
182-
} else {
183-
"\nDAITA: no"
184-
};
185-
#[cfg(not(daita))]
186-
let daita = "";
187185

188186
let mut bridge_type = String::new();
189-
let mut obfuscator_type = String::new();
190187
if verbose {
191188
if let Some(bridge) = &endpoint.proxy {
192189
bridge_type = format!("\nBridge type: {}", bridge.proxy_type);
193190
}
194-
if let Some(obfuscator) = &endpoint.obfuscation {
195-
obfuscator_type = format!("\nObfuscator: {}", obfuscator.obfuscation_type);
196-
}
197191
}
198192

199193
format!(
200-
"{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{quantum_resistant}{daita}{bridge_type}{obfuscator_type}",
194+
"{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{bridge_type}",
201195
first_hop = first_hop.unwrap_or_default(),
202196
bridge = bridge.unwrap_or_default(),
203197
obfuscator = obfuscator.unwrap_or_default(),
204198
)
205199
}
206200

201+
fn format_feature_indicators(feature_indicators: &FeatureIndicators) -> String {
202+
feature_indicators
203+
.active_features()
204+
// Sort the features alphabetically (Just to have some order, arbitrarily chosen)
205+
.sorted_by_key(|feature| feature.to_string())
206+
.join(", ")
207+
}
208+
207209
fn format_endpoint(hostname: Option<&str>, endpoint: &Endpoint, verbose: bool) -> String {
208210
match (hostname, verbose) {
209211
(Some(hostname), true) => format!("{hostname} ({endpoint})"),

mullvad-daemon/src/custom_list.rs

+1-10
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ where
108108
///
109109
/// If `custom_list_id` is `Some`, only changes to that custom list will trigger a reconnect.
110110
fn change_should_cause_reconnect(&self, custom_list_id: Option<Id>) -> bool {
111-
use mullvad_types::states::TunnelState;
112111
let mut need_to_reconnect = false;
113112

114113
let RelaySettings::Normal(relay_settings) = &self.settings.relay_settings else {
@@ -121,15 +120,7 @@ where
121120
need_to_reconnect |= custom_list_id.map(|id| &id == list_id).unwrap_or(true);
122121
}
123122

124-
if let TunnelState::Connecting {
125-
endpoint,
126-
location: _,
127-
}
128-
| TunnelState::Connected {
129-
endpoint,
130-
location: _,
131-
} = &self.tunnel_state
132-
{
123+
if let Some(endpoint) = self.tunnel_state.endpoint() {
133124
match endpoint.tunnel_type {
134125
TunnelType::Wireguard => {
135126
if relay_settings.wireguard_constraints.multihop() {

mullvad-daemon/src/lib.rs

+136-27
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ use mullvad_types::{
5252
auth_failed::AuthFailed,
5353
custom_list::CustomList,
5454
device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent},
55+
features::{FeatureIndicator, FeatureIndicators},
5556
location::{GeoIpLocation, LocationEventData},
5657
relay_constraints::{
5758
BridgeSettings, BridgeState, BridgeType, ObfuscationSettings, RelayOverride, RelaySettings,
5859
},
5960
relay_list::RelayList,
60-
settings::{DnsOptions, Settings},
61+
settings::{DnsOptions, DnsState, Settings},
6162
states::{TargetState, TunnelState},
6263
version::{AppVersion, AppVersionInfo},
6364
wireguard::{PublicKey, QuantumResistantState, RotationInterval},
@@ -85,7 +86,7 @@ use talpid_types::android::AndroidContext;
8586
#[cfg(target_os = "windows")]
8687
use talpid_types::split_tunnel::ExcludedProcess;
8788
use talpid_types::{
88-
net::{IpVersion, TunnelEndpoint, TunnelType},
89+
net::{IpVersion, ObfuscationType, TunnelType},
8990
tunnel::{ErrorStateCause, TunnelStateTransition},
9091
ErrorExt,
9192
};
@@ -369,6 +370,8 @@ pub enum DaemonCommand {
369370
ApplyJsonSettings(ResponseTx<(), settings::patch::Error>, String),
370371
/// Return a JSON blob containing all overridable settings, if there are any
371372
ExportJsonSettings(ResponseTx<String, settings::patch::Error>),
373+
/// Request the current feature indicators.
374+
GetFeatureIndicators(oneshot::Sender<FeatureIndicators>),
372375
}
373376

374377
/// All events that can happen in the daemon. Sent from various threads and exposed interfaces.
@@ -393,6 +396,8 @@ pub(crate) enum InternalDaemonEvent {
393396
DeviceMigrationEvent(Result<PrivateAccountAndDevice, device::Error>),
394397
/// A geographical location has has been received from am.i.mullvad.net
395398
LocationEvent(LocationEventData),
399+
/// A generic event for when any settings change.
400+
SettingsChanged,
396401
/// The split tunnel paths or state were updated.
397402
#[cfg(any(windows, target_os = "android", target_os = "macos"))]
398403
ExcludedPathsEvent(ExcludedPathsUpdate, oneshot::Sender<Result<(), Error>>),
@@ -755,6 +760,13 @@ where
755760
let _ = param_gen_tx.unbounded_send(settings.tunnel_options.to_owned());
756761
});
757762

763+
// Register a listener for generic settings changes.
764+
// This is useful for example for updating feature indicators when the settings change.
765+
let settings_changed_event_sender = internal_event_tx.clone();
766+
settings.register_change_listener(move |_settings| {
767+
let _ = settings_changed_event_sender.send(InternalDaemonEvent::SettingsChanged);
768+
});
769+
758770
let (offline_state_tx, offline_state_rx) = mpsc::unbounded();
759771
#[cfg(target_os = "windows")]
760772
let (volume_update_tx, volume_update_rx) = mpsc::unbounded();
@@ -947,6 +959,9 @@ where
947959
} => self.handle_access_method_event(event, endpoint_active_tx),
948960
DeviceMigrationEvent(event) => self.handle_device_migration_event(event),
949961
LocationEvent(location_data) => self.handle_location_event(location_data),
962+
SettingsChanged => {
963+
self.handle_feature_indicator_event();
964+
}
950965
#[cfg(any(windows, target_os = "android", target_os = "macos"))]
951966
ExcludedPathsEvent(update, tx) => self.handle_new_excluded_paths(update, tx).await,
952967
}
@@ -969,11 +984,13 @@ where
969984
TunnelStateTransition::Connecting(endpoint) => TunnelState::Connecting {
970985
endpoint,
971986
location: self.parameters_generator.get_last_location().await,
987+
feature_indicators: self.get_feature_indicators(),
988+
},
989+
TunnelStateTransition::Connected(endpoint) => TunnelState::Connected {
990+
endpoint,
991+
location: self.parameters_generator.get_last_location().await,
992+
feature_indicators: self.get_feature_indicators(),
972993
},
973-
TunnelStateTransition::Connected(endpoint) => {
974-
let location = self.parameters_generator.get_last_location().await;
975-
TunnelState::Connected { endpoint, location }
976-
}
977994
TunnelStateTransition::Disconnecting(after_disconnect) => {
978995
TunnelState::Disconnecting(after_disconnect)
979996
}
@@ -1097,6 +1114,23 @@ where
10971114
.notify_new_state(self.tunnel_state.clone());
10981115
}
10991116

1117+
/// Update the set of feature indicators.
1118+
fn handle_feature_indicator_event(&mut self) {
1119+
// Note: If the current tunnel state carries information about active feature indicators,
1120+
// we should care to update the known set of feature indicators (i.e. in the connecting /
1121+
// connected state). Otherwise, we can just skip broadcasting a new tunnel state.
1122+
if let Some(current_feature_indicators) = self.tunnel_state.get_feature_indicators() {
1123+
let new_feature_indicators = self.get_feature_indicators();
1124+
if *current_feature_indicators != new_feature_indicators {
1125+
// Make sure to update the daemon's actual tunnel state. Otherwise feature indicator changes won't be persisted.
1126+
self.tunnel_state
1127+
.set_feature_indicators(new_feature_indicators);
1128+
self.event_listener
1129+
.notify_new_state(self.tunnel_state.clone());
1130+
}
1131+
}
1132+
}
1133+
11001134
fn reset_rpc_sockets_on_tunnel_state_transition(
11011135
&mut self,
11021136
tunnel_state_transition: &TunnelStateTransition,
@@ -1248,6 +1282,7 @@ where
12481282
}
12491283
ApplyJsonSettings(tx, blob) => self.on_apply_json_settings(tx, blob).await,
12501284
ExportJsonSettings(tx) => self.on_export_json_settings(tx),
1285+
GetFeatureIndicators(tx) => self.on_get_feature_indicators(tx),
12511286
}
12521287
}
12531288

@@ -2718,6 +2753,14 @@ where
27182753
Self::oneshot_send(tx, result, "export_json_settings response");
27192754
}
27202755

2756+
fn on_get_feature_indicators(&self, tx: oneshot::Sender<FeatureIndicators>) {
2757+
Self::oneshot_send(
2758+
tx,
2759+
self.get_feature_indicators(),
2760+
"get_feature_indicators response",
2761+
);
2762+
}
2763+
27212764
/// Set the target state of the client. If it changed trigger the operations needed to
27222765
/// progress towards that state.
27232766
/// Returns a bool representing whether or not a state change was initiated.
@@ -2752,30 +2795,15 @@ where
27522795
}
27532796
}
27542797

2755-
fn get_connected_tunnel_type(&self) -> Option<TunnelType> {
2756-
if let TunnelState::Connected {
2757-
endpoint: TunnelEndpoint { tunnel_type, .. },
2758-
..
2759-
} = self.tunnel_state
2760-
{
2761-
Some(tunnel_type)
2762-
} else {
2763-
None
2798+
const fn get_connected_tunnel_type(&self) -> Option<TunnelType> {
2799+
match self.tunnel_state.get_tunnel_type() {
2800+
Some(tunnel_type) if self.tunnel_state.is_connected() => Some(tunnel_type),
2801+
Some(_) | None => None,
27642802
}
27652803
}
27662804

2767-
fn get_target_tunnel_type(&self) -> Option<TunnelType> {
2768-
match self.tunnel_state {
2769-
TunnelState::Connected {
2770-
endpoint: TunnelEndpoint { tunnel_type, .. },
2771-
..
2772-
}
2773-
| TunnelState::Connecting {
2774-
endpoint: TunnelEndpoint { tunnel_type, .. },
2775-
..
2776-
} => Some(tunnel_type),
2777-
_ => None,
2778-
}
2805+
const fn get_target_tunnel_type(&self) -> Option<TunnelType> {
2806+
self.tunnel_state.get_tunnel_type()
27792807
}
27802808

27812809
fn send_tunnel_command(&self, command: TunnelCommand) {
@@ -2790,6 +2818,87 @@ where
27902818
tx: self.tx.clone(),
27912819
}
27922820
}
2821+
2822+
/// Source all active [`FeatureIndicators`].
2823+
///
2824+
/// Note that [`FeatureIndicators`] only affect an active connection, which means that when the
2825+
/// daemon is disconnected while calling this function the caller will see an empty set of
2826+
/// [`FeatureIndicators`].
2827+
fn get_feature_indicators(&self) -> FeatureIndicators {
2828+
// Check if there is an active tunnel.
2829+
let Some(endpoint) = self.tunnel_state.endpoint() else {
2830+
// If there is not, no features are actually active and thus should not be displayed.
2831+
return Default::default();
2832+
};
2833+
let settings = self.settings.to_settings();
2834+
2835+
#[cfg(any(windows, target_os = "android", target_os = "macos"))]
2836+
let split_tunneling = self.settings.split_tunnel.enable_exclusions;
2837+
#[cfg(not(any(windows, target_os = "android", target_os = "macos")))]
2838+
let split_tunneling = false;
2839+
2840+
let lockdown_mode = settings.block_when_disconnected;
2841+
let lan_sharing = settings.allow_lan;
2842+
let dns_content_blockers = settings
2843+
.tunnel_options
2844+
.dns_options
2845+
.default_options
2846+
.any_blockers_enabled();
2847+
let custom_dns = settings.tunnel_options.dns_options.state == DnsState::Custom;
2848+
let server_ip_override = !settings.relay_overrides.is_empty();
2849+
2850+
let generic_features = [
2851+
(split_tunneling, FeatureIndicator::SplitTunneling),
2852+
(lockdown_mode, FeatureIndicator::LockdownMode),
2853+
(lan_sharing, FeatureIndicator::LanSharing),
2854+
(dns_content_blockers, FeatureIndicator::DnsContentBlockers),
2855+
(custom_dns, FeatureIndicator::CustomDns),
2856+
(server_ip_override, FeatureIndicator::ServerIpOverride),
2857+
];
2858+
2859+
// Pick protocol-specific features and whether they are currently enabled.
2860+
let protocol_features = match endpoint.tunnel_type {
2861+
TunnelType::OpenVpn => {
2862+
let bridge_mode = endpoint.proxy.is_some();
2863+
let mss_fix = settings.tunnel_options.openvpn.mssfix.is_some();
2864+
2865+
vec![
2866+
(bridge_mode, FeatureIndicator::BridgeMode),
2867+
(mss_fix, FeatureIndicator::CustomMssFix),
2868+
]
2869+
}
2870+
TunnelType::Wireguard => {
2871+
let quantum_resistant = endpoint.quantum_resistant;
2872+
let multihop = endpoint.entry_endpoint.is_some();
2873+
let udp_tcp = endpoint
2874+
.obfuscation
2875+
.as_ref()
2876+
.filter(|obfuscation| obfuscation.obfuscation_type == ObfuscationType::Udp2Tcp)
2877+
.is_some();
2878+
2879+
let mtu = settings.tunnel_options.wireguard.mtu.is_some();
2880+
2881+
#[cfg(daita)]
2882+
let daita = endpoint.daita;
2883+
2884+
vec![
2885+
(quantum_resistant, FeatureIndicator::QuantumResistance),
2886+
(multihop, FeatureIndicator::Multihop),
2887+
(udp_tcp, FeatureIndicator::Udp2Tcp),
2888+
(mtu, FeatureIndicator::CustomMtu),
2889+
#[cfg(daita)]
2890+
(daita, FeatureIndicator::Daita),
2891+
]
2892+
}
2893+
};
2894+
2895+
// use the booleans to filter into a list of only the active features
2896+
generic_features
2897+
.into_iter()
2898+
.chain(protocol_features)
2899+
.filter_map(|(active, feature)| active.then_some(feature))
2900+
.collect()
2901+
}
27932902
}
27942903

27952904
#[derive(Clone)]

0 commit comments

Comments
 (0)