diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index e8f8d96e1806..3461a85ac5e8 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -568,6 +568,10 @@ export class DaemonRpc { await this.callEmpty(this.client.updateDevice); } + public async prepareRestart(quit: boolean) { + await this.callBool(this.client.prepareRestartV2, quit); + } + public async setDaitaSettings(daitaSettings: IDaitaSettings): Promise { const grpcDaitaSettings = new grpcTypes.DaitaSettings(); grpcDaitaSettings.setEnabled(daitaSettings.enabled); diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index ded7ea6ede26..b96bc07f12d6 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -321,7 +321,11 @@ class ApplicationMain } }; - private onBeforeQuit = (event: Electron.Event) => { + private onBeforeQuit = async (event: Electron.Event) => { + if (this.tunnelState.hasReceivedFullDiskAccessError) { + await this.daemonRpc.prepareRestart(true); + } + log.info('before-quit received'); if (this.quitInitiated) { event.preventDefault(); diff --git a/gui/src/main/tunnel-state.ts b/gui/src/main/tunnel-state.ts index 682911881865..297a7e481e18 100644 --- a/gui/src/main/tunnel-state.ts +++ b/gui/src/main/tunnel-state.ts @@ -1,5 +1,5 @@ import { connectEnabled, disconnectEnabled, reconnectEnabled } from '../shared/connect-helper'; -import { ILocation, TunnelState } from '../shared/daemon-rpc-types'; +import { ErrorStateCause, ILocation, TunnelState } from '../shared/daemon-rpc-types'; import { Scheduler } from '../shared/scheduler'; export interface TunnelStateProvider { @@ -20,10 +20,15 @@ export default class TunnelStateHandler { // Scheduler for discarding the assumed next state. private tunnelStateFallbackScheduler = new Scheduler(); + private receivedFullDiskAccessError = false; + private lastKnownDisconnectedLocation: Partial | undefined; public constructor(private delegate: TunnelStateHandlerDelegate) {} + public get hasReceivedFullDiskAccessError() { + return this.receivedFullDiskAccessError; + } public get tunnelState() { return this.tunnelStateValue; } @@ -53,6 +58,12 @@ export default class TunnelStateHandler { } public handleNewTunnelState(newState: TunnelState) { + if (newState.state === 'error' && newState.details) { + if (newState.details.cause === ErrorStateCause.needFullDiskPermissions) { + this.receivedFullDiskAccessError = true; + } + } + // If there's a fallback state set then the app is in an assumed next state and need to check // if it's now reached or if the current state should be ignored and set as the fallback state. if (this.tunnelStateFallback) { diff --git a/mullvad-cli/src/format.rs b/mullvad-cli/src/format.rs index 0d6ea0b0c034..6b092e8939c5 100644 --- a/mullvad-cli/src/format.rs +++ b/mullvad-cli/src/format.rs @@ -225,6 +225,21 @@ fn print_error_state(error_state: &ErrorState) { println!("Blocked: {cause}"); println!("Your kernel might be terribly out of date or missing nftables"); } + #[cfg(target_os = "macos")] + cause @ talpid_types::tunnel::ErrorStateCause::NeedFullDiskPermissions => { + println!("Blocked: {cause}"); + println!(); + println!( + r#"Enable "Full Disk Access" for "Mullvad VPN" in the macOS system settings:"# + ); + println!( + r#"open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles""# + ); + println!(); + println!("Restart the Mullvad daemon for the change to take effect:"); + println!("launchctl unload -w /Library/LaunchDaemons/net.mullvad.daemon.plist"); + println!("launchctl load -w /Library/LaunchDaemons/net.mullvad.daemon.plist"); + } talpid_types::tunnel::ErrorStateCause::AuthFailed(Some(auth_failed)) => { println!( "Blocked: Authentication with remote server failed: {}", diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 3fadfcd81492..3affb7915de3 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -76,9 +76,9 @@ use std::{ sync::{Arc, Weak}, time::Duration, }; -use talpid_core::split_tunnel; use talpid_core::{ mpsc::Sender, + split_tunnel, tunnel_state_machine::{self, TunnelCommand, TunnelStateMachineHandle}, }; #[cfg(target_os = "android")] @@ -350,7 +350,7 @@ pub enum DaemonCommand { SetObfuscationSettings(ResponseTx<(), settings::Error>, ObfuscationSettings), /// Saves the target tunnel state and enters a blocking state. The state is restored /// upon restart. - PrepareRestart, + PrepareRestart(bool), /// Causes a socket to bypass the tunnel. This has no effect when connected. It is only used /// to bypass the tunnel in blocking states. #[cfg(target_os = "android")] @@ -1281,7 +1281,7 @@ where SetObfuscationSettings(tx, settings) => { self.on_set_obfuscation_settings(tx, settings).await } - PrepareRestart => self.on_prepare_restart(), + PrepareRestart(shutdown) => self.on_prepare_restart(shutdown), #[cfg(target_os = "android")] BypassSocket(fd, tx) => self.on_bypass_socket(fd, tx), #[cfg(target_os = "android")] @@ -2688,7 +2688,11 @@ where self.disconnect_tunnel(); } - fn on_prepare_restart(&mut self) { + /// Prepare the daemon for a restart by setting the target state to [`TargetState::Secured`]. + /// + /// - `shutdown`: If the daemon should shut down itself when after setting the secured target + /// state. set to `false` if the intention is to close the daemon process manually. + fn on_prepare_restart(&mut self, shutdown: bool) { // TODO: See if this can be made to also shut down the daemon // without causing the service to be restarted. @@ -2697,6 +2701,11 @@ where self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected(true, tx)); } self.target_state.lock(); + + if shutdown { + self.state.shutdown(&self.tunnel_state); + self.disconnect_tunnel(); + } } #[cfg(target_os = "android")] diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 269432be305b..b5f29a386e96 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -8,14 +8,13 @@ use mullvad_management_interface::{ types::{self, daemon_event, management_service_server::ManagementService}, Code, Request, Response, Status, }; -use mullvad_types::settings::DnsOptions; use mullvad_types::{ account::AccountToken, relay_constraints::{ BridgeSettings, BridgeState, ObfuscationSettings, RelayOverride, RelaySettings, }, relay_list::RelayList, - settings::Settings, + settings::{DnsOptions, Settings}, states::{TargetState, TunnelState}, version, wireguard::{RotationInterval, RotationIntervalError}, @@ -104,7 +103,15 @@ impl ManagementService for ManagementServiceImpl { async fn prepare_restart(&self, _: Request<()>) -> ServiceResult<()> { log::debug!("prepare_restart"); - self.send_command_to_daemon(DaemonCommand::PrepareRestart)?; + // Note: The old `PrepareRestart` behavior never shutdown the daemon. + let shutdown = false; + self.send_command_to_daemon(DaemonCommand::PrepareRestart(shutdown))?; + Ok(Response::new(())) + } + + async fn prepare_restart_v2(&self, shutdown: Request) -> ServiceResult<()> { + log::debug!("prepare_restart_v2"); + self.send_command_to_daemon(DaemonCommand::PrepareRestart(shutdown.into_inner()))?; Ok(Response::new(())) } diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 3588c6961245..aa279070f09b 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -17,7 +17,11 @@ service ManagementService { // Control the daemon and receive events rpc EventsListen(google.protobuf.Empty) returns (stream DaemonEvent) {} + // DEPRECATED: Prefer PrepareRestartV2. rpc PrepareRestart(google.protobuf.Empty) returns (google.protobuf.Empty) {} + // Takes a a boolean argument which says whether the daemon should stop after + // it is done preparing for a restart. + rpc PrepareRestartV2(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc FactoryReset(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc GetCurrentVersion(google.protobuf.Empty) returns (google.protobuf.StringValue) {} diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index 6a3b794b3979..3d217ace32a5 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -149,11 +149,25 @@ impl MullvadProxyClient { })) } + /// DEPRECATED: Prefer to use `prepare_restart_v2`. pub async fn prepare_restart(&mut self) -> Result<()> { self.0.prepare_restart(()).await.map_err(Error::Rpc)?; Ok(()) } + /// Tell the daemon to get ready for a restart by securing a user, i.e. putting firewall rules + /// in place. + /// + /// - `shutdown`: Whether the daemon should shutdown immediately after its prepare-for-restart + /// routine. + pub async fn prepare_restart_v2(&mut self, shutdown: bool) -> Result<()> { + self.0 + .prepare_restart_v2(shutdown) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + pub async fn factory_reset(&mut self) -> Result<()> { self.0.factory_reset(()).await.map_err(Error::Rpc)?; Ok(())