summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-05-29 12:56:03 +0200
committerDavid Lönnhager <david.l@mullvad.net>2024-05-29 12:56:03 +0200
commit596286dffee268327265879469588f8760cd43a6 (patch)
tree97176e347d6fb96143c573fc4b651604e01e512d
parent2b04fed8d6a486d97af47f1add45b0eeb1071db8 (diff)
parent6289b4c7feb587e8fe2a9a4be175a8bd1aafa9f7 (diff)
downloadmullvadvpn-596286dffee268327265879469588f8760cd43a6.tar.xz
mullvadvpn-596286dffee268327265879469588f8760cd43a6.zip
Merge branch 'macos-add-split-tunnel-error-info'
-rw-r--r--gui/locales/messages.pot4
-rw-r--r--gui/src/main/daemon-rpc.ts6
-rw-r--r--gui/src/shared/notifications/error.ts17
-rw-r--r--mullvad-management-interface/proto/management_interface.proto1
-rw-r--r--mullvad-management-interface/src/types/conversions/states.rs10
-rw-r--r--talpid-core/src/split_tunnel/macos/mod.rs196
-rw-r--r--talpid-core/src/split_tunnel/macos/process.rs10
-rw-r--r--talpid-core/src/split_tunnel/macos/tun.rs6
-rw-r--r--talpid-core/src/tunnel_state_machine/connected_state.rs6
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs6
-rw-r--r--talpid-core/src/tunnel_state_machine/mod.rs8
-rw-r--r--talpid-types/src/tunnel.rs5
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}")