diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-05-29 12:56:03 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-05-29 12:56:03 +0200 |
| commit | 596286dffee268327265879469588f8760cd43a6 (patch) | |
| tree | 97176e347d6fb96143c573fc4b651604e01e512d | |
| parent | 2b04fed8d6a486d97af47f1add45b0eeb1071db8 (diff) | |
| parent | 6289b4c7feb587e8fe2a9a4be175a8bd1aafa9f7 (diff) | |
| download | mullvadvpn-596286dffee268327265879469588f8760cd43a6.tar.xz mullvadvpn-596286dffee268327265879469588f8760cd43a6.zip | |
Merge branch 'macos-add-split-tunnel-error-info'
| -rw-r--r-- | gui/locales/messages.pot | 4 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 6 | ||||
| -rw-r--r-- | gui/src/shared/notifications/error.ts | 17 | ||||
| -rw-r--r-- | mullvad-management-interface/proto/management_interface.proto | 1 | ||||
| -rw-r--r-- | mullvad-management-interface/src/types/conversions/states.rs | 10 | ||||
| -rw-r--r-- | talpid-core/src/split_tunnel/macos/mod.rs | 196 | ||||
| -rw-r--r-- | talpid-core/src/split_tunnel/macos/process.rs | 10 | ||||
| -rw-r--r-- | talpid-core/src/split_tunnel/macos/tun.rs | 6 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 6 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 6 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 8 | ||||
| -rw-r--r-- | talpid-types/src/tunnel.rs | 5 |
12 files changed, 218 insertions, 57 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 9126cb9c99..65267fd5bd 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -1087,6 +1087,10 @@ msgid "Disconnected and unsecure" msgstr "" msgctxt "notifications" +msgid "Failed to enable split tunneling. Please try again or disable it." +msgstr "" + +msgctxt "notifications" msgid "Lockdown mode active, connection blocked" msgstr "" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 10e39f020d..c51524f1ee 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -1046,6 +1046,12 @@ function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): Erro ...baseError, cause: ErrorStateCause.splitTunnelError, }; + case grpcTypes.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS: + // TODO: handle correctly + return { + ...baseError, + cause: ErrorStateCause.splitTunnelError, + }; case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: // VPN_PERMISSION_DENIED is only ever created on Android throw invalidErrorStateCause; diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts index 673d6cea28..1d36b7e1de 100644 --- a/gui/src/shared/notifications/error.ts +++ b/gui/src/shared/notifications/error.ts @@ -183,10 +183,18 @@ function getMessage(errorState: ErrorState): string { 'Your device is offline. The tunnel will automatically connect once your device is back online.', ); case ErrorStateCause.splitTunnelError: - return messages.pgettext( - 'notifications', - 'Unable to communicate with Mullvad kernel driver. Try reconnecting or send a problem report.', - ); + switch (process.platform ?? window.env.platform) { + case 'darwin': + return messages.pgettext( + 'notifications', + 'Failed to enable split tunneling. Please try again or disable it.', + ); + default: + return messages.pgettext( + 'notifications', + 'Unable to communicate with Mullvad kernel driver. Try reconnecting or send a problem report.', + ); + } } } } @@ -265,6 +273,7 @@ function getActions(errorState: ErrorState): InAppNotificationAction | void { }, }; } else if (errorState.cause === ErrorStateCause.splitTunnelError) { + // TODO: macos: handle this and full disk access error return { type: 'troubleshoot-dialog', troubleshoot: { diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index efe9f9eb87..3f9de9b189 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -142,6 +142,7 @@ message ErrorState { IS_OFFLINE = 7; VPN_PERMISSION_DENIED = 8; SPLIT_TUNNEL_ERROR = 9; + NEED_FULL_DISK_PERMISSIONS = 10; } enum AuthFailedError { diff --git a/mullvad-management-interface/src/types/conversions/states.rs b/mullvad-management-interface/src/types/conversions/states.rs index 881cbc6c9a..58766a9925 100644 --- a/mullvad-management-interface/src/types/conversions/states.rs +++ b/mullvad-management-interface/src/types/conversions/states.rs @@ -107,6 +107,10 @@ impl From<mullvad_types::states::TunnelState> for proto::TunnelState { talpid_tunnel::ErrorStateCause::SplitTunnelError => { i32::from(Cause::SplitTunnelError) } + #[cfg(target_os = "macos")] + talpid_tunnel::ErrorStateCause::NeedFullDiskPermissions => { + i32::from(Cause::NeedFullDiskPermissions) + } }, blocking_error: error_state.block_failure().map(map_firewall_error), auth_failed_error: mullvad_types::auth_failed::AuthFailed::try_from( @@ -325,10 +329,14 @@ impl TryFrom<proto::TunnelState> for mullvad_types::states::TunnelState { Ok(proto::error_state::Cause::VpnPermissionDenied) => { talpid_tunnel::ErrorStateCause::VpnPermissionDenied } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] Ok(proto::error_state::Cause::SplitTunnelError) => { talpid_tunnel::ErrorStateCause::SplitTunnelError } + #[cfg(target_os = "macos")] + Ok(proto::error_state::Cause::NeedFullDiskPermissions) => { + talpid_tunnel::ErrorStateCause::NeedFullDiskPermissions + } _ => { return Err(FromProtobufTypeError::InvalidArgument( "invalid error cause", diff --git a/talpid-core/src/split_tunnel/macos/mod.rs b/talpid-core/src/split_tunnel/macos/mod.rs index 980097d94b..000ee87dd0 100644 --- a/talpid-core/src/split_tunnel/macos/mod.rs +++ b/talpid-core/src/split_tunnel/macos/mod.rs @@ -1,6 +1,7 @@ +use core::fmt; use std::collections::HashSet; use std::path::PathBuf; -use std::sync::Weak; +use std::sync::{Arc, Weak}; use talpid_routing::RouteManagerHandle; use talpid_types::tunnel::ErrorStateCause; use talpid_types::ErrorExt; @@ -19,8 +20,52 @@ use crate::tunnel_state_machine::TunnelCommand; pub use tun::VpnInterface; /// Errors caused by split tunneling +#[derive(Debug, Clone)] +pub struct Error { + inner: Arc<InnerError>, +} + +impl Error { + fn unavailable() -> Self { + Self { + inner: Arc::new(InnerError::Unavailable), + } + } +} + +impl From<&Error> for ErrorStateCause { + fn from(value: &Error) -> Self { + match &*value.inner { + InnerError::Process(error) => ErrorStateCause::from(error), + _v if _v.is_offline() => ErrorStateCause::IsOffline, + _ => ErrorStateCause::SplitTunnelError, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&*self.inner, f) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.inner.source() + } +} + +impl<T: Into<InnerError>> From<T> for Error { + fn from(inner: T) -> Self { + Self { + inner: Arc::new(inner.into()), + } + } +} + +/// Errors caused by split tunneling #[derive(thiserror::Error, Debug)] -pub enum Error { +enum InnerError { /// Process monitor error #[error("Process monitor error")] Process(#[from] process::Error), @@ -35,10 +80,10 @@ pub enum Error { Unavailable, } -impl Error { +impl InnerError { /// Return whether the error is due to a missing default route - pub fn is_offline(&self) -> bool { - matches!(self, Error::Default(_)) + fn is_offline(&self) -> bool { + matches!(self, InnerError::Default(_)) } } @@ -99,7 +144,7 @@ impl Handle { pub async fn set_exclude_paths(&self, paths: HashSet<PathBuf>) -> Result<(), Error> { let (result_tx, result_rx) = oneshot::channel(); let _ = self.tx.send(Message::SetExcludePaths { result_tx, paths }); - result_rx.await.map_err(|_| Error::Unavailable)? + result_rx.await.map_err(|_| Error::unavailable())? } /// Set VPN tunnel interface @@ -109,7 +154,7 @@ impl Handle { result_tx, new_vpn_interface, }); - result_rx.await.map_err(|_| Error::Unavailable)? + result_rx.await.map_err(|_| Error::unavailable())? } } @@ -171,9 +216,13 @@ impl SplitTunnel { /// Handle process monitor unexpectedly stopping fn handle_process_monitor_shutdown(&mut self, result: Result<(), process::Error>) { + let cause = match result { + Ok(_) => ErrorStateCause::SplitTunnelError, + Err(ref error) => ErrorStateCause::from(error), + }; match result { Ok(()) => log::error!("Process monitor stopped unexpectedly with no error"), - Err(error) => { + Err(ref error) => { log::error!( "{}", error.display_chain_with_msg("Process monitor stopped unexpectedly") @@ -185,12 +234,11 @@ impl SplitTunnel { // decisions for new processes if self.state.active() { if let Some(tunnel_tx) = self.tunnel_tx.upgrade() { - let _ = tunnel_tx - .unbounded_send(TunnelCommand::Block(ErrorStateCause::SplitTunnelError)); + let _ = tunnel_tx.unbounded_send(TunnelCommand::Block(cause)); } } - self.state.fail(); + self.state.fail(result.err().map(Error::from)); } /// Handle an incoming message @@ -219,7 +267,7 @@ impl SplitTunnel { /// Shut down split tunnel async fn shutdown(&mut self) { - match self.state.fail() { + match self.state.fail(None) { State::ProcessMonitorOnly { mut process, .. } => { process.shutdown().await; } @@ -269,6 +317,7 @@ enum State { Failed { route_manager: RouteManagerHandle, vpn_interface: Option<VpnInterface>, + cause: Option<Error>, }, } @@ -300,13 +349,23 @@ impl State { } } + fn fail_cause(&self) -> Option<&Error> { + match self { + State::Failed { cause, .. } => cause.as_ref(), + _ => None, + } + } + /// Take `self`, leaving a failed state in its place. The original value is returned - fn fail(&mut self) -> Self { + /// `cause` optionally specifies a failure cause. Unless specified, the last known error will be + /// used instead. + fn fail(&mut self, cause: Option<Error>) -> Self { std::mem::replace( self, State::Failed { route_manager: self.route_manager().clone(), vpn_interface: self.vpn_interface().cloned(), + cause: cause.or_else(|| self.fail_cause().cloned()), }, ) } @@ -320,12 +379,14 @@ impl State { /// Set paths to exclude. For a non-empty path, this will initialize split tunneling if a tunnel /// device is also set. async fn set_exclude_paths(&mut self, paths: HashSet<PathBuf>) -> Result<(), Error> { - let state = self.fail(); - *self = state.set_exclude_paths_inner(paths).await?; - Ok(()) + self.transition(move |self_| self_.set_exclude_paths_inner(paths)) + .await } - async fn set_exclude_paths_inner(mut self, paths: HashSet<PathBuf>) -> Result<Self, Error> { + async fn set_exclude_paths_inner( + mut self, + paths: HashSet<PathBuf>, + ) -> Result<Self, ErrorWithTransition> { match self { // If there are currently no paths and no process monitor, initialize it State::NoExclusions { @@ -360,6 +421,7 @@ impl State { State::Failed { route_manager, vpn_interface, + cause: _, } if paths.is_empty() => { log::debug!("Transitioning out of split tunnel error state"); @@ -369,32 +431,44 @@ impl State { }) } // Otherwise, remain in the failed state - State::Failed { .. } => Err(Error::Unavailable), + State::Failed { cause, .. } => Err(cause.unwrap_or(Error::unavailable()).into()), } } /// Update VPN tunnel interface that non-excluded packets are sent on async fn set_tunnel(&mut self, new_vpn_interface: Option<VpnInterface>) -> Result<(), Error> { - let state = self.fail(); - *self = state.set_tunnel_inner(new_vpn_interface).await?; - Ok(()) + self.transition(move |self_| self_.set_tunnel_inner(new_vpn_interface)) + .await } async fn set_tunnel_inner( mut self, new_vpn_interface: Option<VpnInterface>, - ) -> Result<Self, Error> { + ) -> Result<Self, ErrorWithTransition> { match self { // If split tunneling is already initialized, just update the interfaces State::Initialized { route_manager, mut process, tun_handle, - vpn_interface: _, + vpn_interface, } => { // Try to update the default interface first // If this fails, remain in the current state and just fail - let default_interface = default::get_default_interface(&route_manager).await?; + let default_interface = match default::get_default_interface(&route_manager).await { + Ok(default_interface) => default_interface, + Err(error) => { + return Err(ErrorWithTransition { + error: error.into(), + next_state: Some(State::Initialized { + route_manager, + process, + tun_handle, + vpn_interface, + }), + }); + } + }; log::debug!("Updating split tunnel device"); @@ -421,7 +495,18 @@ impl State { } if new_vpn_interface.is_some() => { // Try to update the default interface first // If this fails, remain in the current state and just fail - let default_interface = default::get_default_interface(&route_manager).await?; + let default_interface = match default::get_default_interface(&route_manager).await { + Ok(default_interface) => default_interface, + Err(error) => { + return Err(ErrorWithTransition { + error: error.into(), + next_state: Some(State::ProcessMonitorOnly { + route_manager, + process, + }), + }); + } + }; log::debug!("Initializing split tunnel device"); @@ -468,15 +553,64 @@ impl State { // Remain in the failed state and return error if VPN is up State::Failed { ref mut vpn_interface, + cause, .. - } => { + } if new_vpn_interface.is_some() => { *vpn_interface = new_vpn_interface; - if vpn_interface.is_some() { - Err(Error::Unavailable) - } else { - Ok(self) - } + Err(cause.unwrap_or(Error::unavailable()).into()) + } + // Remain in the failed state without failing otherwise + State::Failed { + ref mut vpn_interface, + .. + } => { + *vpn_interface = None; + Ok(self) + } + } + } + + /// Helper function that tries to perform a state transition using `transition`. + /// On error, transition to `next_state` specified alongside the error. If not specified, + /// transition to or remain in `State::Failed`. + async fn transition<F: std::future::Future<Output = Result<Self, ErrorWithTransition>>>( + &mut self, + transition: impl FnOnce(Self) -> F, + ) -> Result<(), Error> { + let state = self.fail(None); + match (transition)(state).await { + Ok(new_state) => { + *self = new_state; + Ok(()) + } + Err(ErrorWithTransition { + error, + next_state: Some(next_state), + }) => { + *self = next_state; + Err(error) } + Err(ErrorWithTransition { + error, + next_state: None, + }) => { + self.fail(Some(error.clone())); + Err(error) + } + } + } +} + +struct ErrorWithTransition { + error: Error, + next_state: Option<State>, +} + +impl<T: Into<Error>> From<T> for ErrorWithTransition { + fn from(error: T) -> Self { + Self { + error: error.into(), + next_state: None, } } } diff --git a/talpid-core/src/split_tunnel/macos/process.rs b/talpid-core/src/split_tunnel/macos/process.rs index f913b9bb49..013aca34e4 100644 --- a/talpid-core/src/split_tunnel/macos/process.rs +++ b/talpid-core/src/split_tunnel/macos/process.rs @@ -21,6 +21,7 @@ use std::{ time::Duration, }; use talpid_platform_metadata::MacosVersion; +use talpid_types::tunnel::ErrorStateCause; use tokio::io::{AsyncBufReadExt, BufReader}; const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3); @@ -56,6 +57,15 @@ pub enum Error { FindProcessPath(#[source] io::Error, u32), } +impl From<&Error> for ErrorStateCause { + fn from(value: &Error) -> Self { + match value { + Error::NeedFullDiskPermissions => ErrorStateCause::NeedFullDiskPermissions, + _ => ErrorStateCause::SplitTunnelError, + } + } +} + pub struct ProcessMonitor(()); #[derive(Debug)] diff --git a/talpid-core/src/split_tunnel/macos/tun.rs b/talpid-core/src/split_tunnel/macos/tun.rs index 3f70e1c655..3bf828f167 100644 --- a/talpid-core/src/split_tunnel/macos/tun.rs +++ b/talpid-core/src/split_tunnel/macos/tun.rs @@ -51,9 +51,6 @@ pub enum Error { /// Failed to begin capture on split tunnel utun #[error("Failed to begin capture on split tunnel utun")] CaptureSplitTunnelDevice(#[source] pcap::Error), - /// Failed to set direction on capture - #[error("Failed to set direction on pcap")] - SetDirection(#[source] pcap::Error), /// Failed to enable nonblocking I/O #[error("Failed to enable nonblocking I/O")] EnableNonblock(#[source] pcap::Error), @@ -72,9 +69,6 @@ pub enum Error { /// Failed to configure BPF device for default interface #[error("Failed to configure BPF device for default interface")] ConfigDefaultBpf(#[source] bpf::Error), - /// Failed to retrieve BPF buffer size - #[error("Failed to retrieve BPF buffer size")] - GetBpfBufferSize(#[source] bpf::Error), /// Failed to create BPF device for VPN tunnel #[error("Failed to create BPF device for VPN tunnel")] CreateVpnBpf(#[source] bpf::Error), diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 80ca26ec56..395a67a057 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -353,11 +353,9 @@ impl ConnectedState { } } Err(error) => { + let cause = ErrorStateCause::from(&error); let _ = result_tx.send(Err(error)); - return self.disconnect( - shared_values, - AfterDisconnect::Block(ErrorStateCause::SplitTunnelError), - ); + return self.disconnect(shared_values, AfterDisconnect::Block(cause)); } } SameState(self) diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 5df58a6adf..a4267d0d56 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -498,11 +498,9 @@ impl ConnectingState { } } Err(error) => { + let cause = ErrorStateCause::from(&error); let _ = result_tx.send(Err(error)); - return self.disconnect( - shared_values, - AfterDisconnect::Block(ErrorStateCause::SplitTunnelError), - ); + return self.disconnect(shared_values, AfterDisconnect::Block(cause)); } } SameState(self) diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index e31dec3624..a74d7e39b5 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -540,13 +540,7 @@ impl SharedTunnelStateValues { error.display_chain_with_msg("Failed to set VPN interface for split tunnel") ) }) - .map_err(|error| { - if error.is_offline() { - ErrorStateCause::IsOffline - } else { - ErrorStateCause::SplitTunnelError - } - }) + .map_err(|error| ErrorStateCause::from(&error)) } pub fn set_allow_lan(&mut self, allow_lan: bool) -> Result<(), ErrorStateCause> { diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index e67db2f0c4..686f7ae19a 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -110,6 +110,9 @@ pub enum ErrorStateCause { /// Error reported by split tunnel module. #[cfg(any(target_os = "windows", target_os = "macos"))] SplitTunnelError, + /// Missing permissions required by macOS split tunneling. + #[cfg(target_os = "macos")] + NeedFullDiskPermissions, } impl ErrorStateCause { @@ -217,6 +220,8 @@ impl fmt::Display for ErrorStateCause { VpnPermissionDenied => "The Android VPN permission was denied when creating the tunnel", #[cfg(any(target_os = "windows", target_os = "macos"))] SplitTunnelError => "The split tunneling module reported an error", + #[cfg(target_os = "macos")] + NeedFullDiskPermissions => "Need full disk access to enable split tunneling", }; write!(f, "{description}") |
