diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-01-09 07:46:14 -0200 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-01-09 07:46:14 -0200 |
| commit | 74da64ba1e1f0dda1ac94dfdf5eead7a41576e68 (patch) | |
| tree | 887c4afe59b097dc354047ee2d4d3b8cb8e6bd0d | |
| parent | 3c69bd0c17d3d8a75e9d658d72bef50ba2363321 (diff) | |
| parent | b3176e9a3f10698ecb4d9dda3b56f83eefdd1dbb (diff) | |
| download | mullvadvpn-74da64ba1e1f0dda1ac94dfdf5eead7a41576e68.tar.xz mullvadvpn-74da64ba1e1f0dda1ac94dfdf5eead7a41576e68.zip | |
Merge branch 'detect-missing-tap-adapter'
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | gui/packages/desktop/src/main/daemon-rpc.js | 4 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/NotificationArea.js | 2 | ||||
| -rw-r--r-- | talpid-core/src/tunnel/mod.rs | 26 | ||||
| -rw-r--r-- | talpid-core/src/tunnel/openvpn.rs | 106 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 16 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 63 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnecting_state.rs | 18 | ||||
| -rw-r--r-- | talpid-types/src/tunnel.rs | 3 |
9 files changed, 188 insertions, 53 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0278a1bc91..d83a18fd5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,9 @@ Line wrap the file at 100 chars. Th - Fix error printed from the CLI when issuing `relay update`. - Fix relay list update interval. Should now handle sleep better. +#### Windows +- Gracefully block when TAP adapter is missing or disabled, instead of retrying to connect. + ## [2018.6] - 2018-12-12 This release is identical to 2018.6-beta1 diff --git a/gui/packages/desktop/src/main/daemon-rpc.js b/gui/packages/desktop/src/main/daemon-rpc.js index 26da6e14af..bb55e5d335 100644 --- a/gui/packages/desktop/src/main/daemon-rpc.js +++ b/gui/packages/desktop/src/main/daemon-rpc.js @@ -52,7 +52,8 @@ export type BlockReason = | 'set_dns_error' | 'start_tunnel_error' | 'no_matching_relay' - | 'is_offline', + | 'is_offline' + | 'tap_adapter_problem', } | { reason: 'auth_failed', details: ?string }; @@ -314,6 +315,7 @@ const TunnelStateTransitionSchema = oneOf( 'start_tunnel_error', 'no_matching_relay', 'is_offline', + 'tap_adapter_problem', ), }), object({ diff --git a/gui/packages/desktop/src/renderer/components/NotificationArea.js b/gui/packages/desktop/src/renderer/components/NotificationArea.js index 646dd236c2..3eba431a52 100644 --- a/gui/packages/desktop/src/renderer/components/NotificationArea.js +++ b/gui/packages/desktop/src/renderer/components/NotificationArea.js @@ -55,6 +55,8 @@ function getBlockReasonMessage(blockReason: BlockReason): string { return 'No relay server matches the current settings'; case 'is_offline': return 'This device is offline, no tunnels can be established'; + case 'tap_adapter_problem': + return "Unable to detect a working TAP adapter on this device. If you've disabled it, enable it again. Otherwise, please reinstall the app"; default: return `Unknown error: ${(blockReason.reason: empty)}`; } diff --git a/talpid-core/src/tunnel/mod.rs b/talpid-core/src/tunnel/mod.rs index 078f23cf16..dc0d935ded 100644 --- a/talpid-core/src/tunnel/mod.rs +++ b/talpid-core/src/tunnel/mod.rs @@ -38,9 +38,9 @@ const OPENVPN_BIN_FILENAME: &str = "openvpn.exe"; error_chain! { errors { - /// An error indicating there was an error listening for events from the VPN tunnel. - TunnelMonitoringError { - description("Error while setting up or processing events from the VPN tunnel") + /// There was an error preparing to listen for events from the VPN tunnel. + TunnelMonitorSetUpError { + description("Error while setting up to listen for events from the VPN tunnel") } /// The OpenVPN binary was not found. OpenVpnNotFound(path: PathBuf) { @@ -74,6 +74,12 @@ error_chain! { description("This tunnel protocol is not supported") } } + + links { + OpenVpnTunnelMonitoringError(openvpn::Error, openvpn::ErrorKind) + /// There was an error listening for events from the OpenVPN tunnel + ; + } } @@ -156,7 +162,7 @@ impl TunnelMonitor { tunnel_options: &TunnelOptions, tunnel_alias: Option<OsString>, username: &str, - log: Option<&Path>, + log: Option<PathBuf>, resource_dir: &Path, on_event: L, ) -> Result<Self> @@ -181,7 +187,6 @@ impl TunnelMonitor { Some(ref file) => Some(file.as_ref()), _ => None, }, - log, resource_dir, )?; @@ -212,8 +217,9 @@ impl TunnelMonitor { cmd, on_openvpn_event, Self::get_plugin_path(resource_dir)?, + log, ) - .chain_err(|| ErrorKind::TunnelMonitoringError)?; + .chain_err(|| ErrorKind::TunnelMonitorSetUpError)?; Ok(TunnelMonitor { monitor, _user_pass_file: user_pass_file, @@ -242,7 +248,6 @@ impl TunnelMonitor { options: &TunnelOptions, user_pass_file: &Path, proxy_auth_file: Option<&Path>, - log: Option<&Path>, resource_dir: &Path, ) -> Result<OpenVpnCommand> { let mut cmd = OpenVpnCommand::new(Self::get_openvpn_bin(resource_dir)?); @@ -261,9 +266,6 @@ impl TunnelMonitor { .enable_ipv6(options.enable_ipv6) .tunnel_alias(tunnel_alias) .ca(resource_dir.join("ca.crt")); - if let Some(log) = log { - cmd.log(log); - } if let Some(proxy_auth_file) = proxy_auth_file { cmd.proxy_auth(proxy_auth_file); } @@ -344,9 +346,7 @@ impl TunnelMonitor { /// Consumes the monitor and blocks until the tunnel exits or there is an error. pub fn wait(self) -> Result<()> { - self.monitor - .wait() - .chain_err(|| ErrorKind::TunnelMonitoringError) + self.monitor.wait().map_err(Error::from) } } diff --git a/talpid-core/src/tunnel/openvpn.rs b/talpid-core/src/tunnel/openvpn.rs index fd2437f2e6..9d7e6eed93 100644 --- a/talpid-core/src/tunnel/openvpn.rs +++ b/talpid-core/src/tunnel/openvpn.rs @@ -5,7 +5,7 @@ use crate::process::{ use std::{ collections::HashMap, io, - path::Path, + path::{Path, PathBuf}, process::ExitStatus, sync::{ atomic::{AtomicBool, Ordering}, @@ -29,6 +29,16 @@ mod errors { EventDispatcherError { description("Unable to start or manage the event dispatcher IPC server") } + #[cfg(windows)] + /// No TAP adapter was detected + MissingTapAdapter { + description("No TAP adapter was detected") + } + #[cfg(windows)] + /// TAP adapter seems to be disabled + DisabledTapAdapter { + description("The TAP adapter appears to be disabled") + } } } } @@ -46,38 +56,49 @@ static OPENVPN_DIE_TIMEOUT: Duration = Duration::from_secs(30); pub struct OpenVpnMonitor<C: OpenVpnBuilder = OpenVpnCommand> { child: Arc<C::ProcessHandle>, event_dispatcher: Option<talpid_ipc::IpcServer>, + log_path: Option<PathBuf>, closed: Arc<AtomicBool>, } impl OpenVpnMonitor<OpenVpnCommand> { /// Creates a new `OpenVpnMonitor` with the given listener and using the plugin at the given /// path. - pub fn start<L, P>(cmd: OpenVpnCommand, on_event: L, plugin_path: P) -> Result<Self> + pub fn start<L>( + cmd: OpenVpnCommand, + on_event: L, + plugin_path: impl AsRef<Path>, + log_path: Option<PathBuf>, + ) -> Result<Self> where L: Fn(openvpn_plugin::EventType, HashMap<String, String>) + Send + Sync + 'static, - P: AsRef<Path>, { - Self::new_internal(cmd, on_event, plugin_path) + Self::new_internal(cmd, on_event, plugin_path, log_path) } } impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { - fn new_internal<L, P>(mut cmd: C, on_event: L, plugin_path: P) -> Result<OpenVpnMonitor<C>> + fn new_internal<L>( + mut cmd: C, + on_event: L, + plugin_path: impl AsRef<Path>, + log_path: Option<PathBuf>, + ) -> Result<OpenVpnMonitor<C>> where L: Fn(openvpn_plugin::EventType, HashMap<String, String>) + Send + Sync + 'static, - P: AsRef<Path>, { let event_dispatcher = event_server::start(on_event).chain_err(|| ErrorKind::EventDispatcherError)?; - cmd.plugin(plugin_path, vec![event_dispatcher.path().to_owned()]); let child = cmd + .plugin(plugin_path, vec![event_dispatcher.path().to_owned()]) + .log(log_path.as_ref()) .start() .chain_err(|| ErrorKind::ChildProcessError("Failed to start"))?; Ok(OpenVpnMonitor { child: Arc::new(child), event_dispatcher: Some(event_dispatcher), + log_path, closed: Arc::new(AtomicBool::new(false)), }) } @@ -106,7 +127,7 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { Ok(()) } else { log::error!("OpenVPN died unexpectedly with status: {}", exit_status); - Err(ErrorKind::ChildProcessError("Died unexpectedly").into()) + Err(self.postmortem()) } } WaitResult::Child(Err(e), _) => { @@ -148,6 +169,27 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { let _ = rx.recv().unwrap(); result } + + /// Performs a postmortem analysis to attempt to provide a more detailed error result. + fn postmortem(self) -> Error { + #[cfg(windows)] + { + use std::fs; + + if let Some(log_path) = self.log_path { + if let Ok(log) = fs::read_to_string(log_path) { + if log.contains("There are no TAP-Windows adapters on this system") { + return ErrorKind::MissingTapAdapter.into(); + } + if log.contains("CreateFile failed on TAP device") { + return ErrorKind::DisabledTapAdapter.into(); + } + } + } + } + + ErrorKind::ChildProcessError("Died unexpectedly").into() + } } /// A handle to an `OpenVpnMonitor` for closing it. @@ -181,7 +223,10 @@ pub trait OpenVpnBuilder { type ProcessHandle: ProcessHandle; /// Set the OpenVPN plugin to the given values. - fn plugin<P: AsRef<Path>>(&mut self, path: P, args: Vec<String>) -> &mut Self; + fn plugin(&mut self, path: impl AsRef<Path>, args: Vec<String>) -> &mut Self; + + /// Set the OpenVPN log file path to use. + fn log(&mut self, log_path: Option<impl AsRef<Path>>) -> &mut Self; /// Spawn the subprocess and return a handle. fn start(&self) -> io::Result<Self::ProcessHandle>; @@ -199,10 +244,18 @@ pub trait ProcessHandle: Send + Sync + 'static { impl OpenVpnBuilder for OpenVpnCommand { type ProcessHandle = OpenVpnProcHandle; - fn plugin<P: AsRef<Path>>(&mut self, path: P, args: Vec<String>) -> &mut Self { + fn plugin(&mut self, path: impl AsRef<Path>, args: Vec<String>) -> &mut Self { self.plugin(path, args) } + fn log(&mut self, log_path: Option<impl AsRef<Path>>) -> &mut Self { + if let Some(log_path) = log_path { + self.log(log_path) + } else { + self + } + } + fn start(&self) -> io::Result<OpenVpnProcHandle> { OpenVpnProcHandle::new(self.build()) } @@ -284,17 +337,23 @@ mod tests { #[derive(Debug, Default, Clone)] struct TestOpenVpnBuilder { pub plugin: Arc<Mutex<Option<PathBuf>>>, + pub log: Arc<Mutex<Option<PathBuf>>>, pub process_handle: Option<TestProcessHandle>, } impl OpenVpnBuilder for TestOpenVpnBuilder { type ProcessHandle = TestProcessHandle; - fn plugin<P: AsRef<Path>>(&mut self, path: P, _args: Vec<String>) -> &mut Self { + fn plugin(&mut self, path: impl AsRef<Path>, _args: Vec<String>) -> &mut Self { *self.plugin.lock().unwrap() = Some(path.as_ref().to_path_buf()); self } + fn log(&mut self, log: Option<impl AsRef<Path>>) -> &mut Self { + *self.log.lock().unwrap() = log.as_ref().map(|path| path.as_ref().to_path_buf()); + self + } + fn start(&self) -> io::Result<Self::ProcessHandle> { self.process_handle .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to start")) @@ -325,7 +384,7 @@ mod tests { #[test] fn sets_plugin() { let builder = TestOpenVpnBuilder::default(); - let _ = OpenVpnMonitor::new_internal(builder.clone(), |_, _| {}, "./my_test_plugin"); + let _ = OpenVpnMonitor::new_internal(builder.clone(), |_, _| {}, "./my_test_plugin", None); assert_eq!( Some(PathBuf::from("./my_test_plugin")), *builder.plugin.lock().unwrap() @@ -333,10 +392,25 @@ mod tests { } #[test] + fn sets_log() { + let builder = TestOpenVpnBuilder::default(); + let _ = OpenVpnMonitor::new_internal( + builder.clone(), + |_, _| {}, + "./my_test_plugin", + Some(PathBuf::from("./my_test_log_file")), + ); + assert_eq!( + Some(PathBuf::from("./my_test_log_file")), + *builder.log.lock().unwrap() + ); + } + + #[test] fn exit_successfully() { let mut builder = TestOpenVpnBuilder::default(); builder.process_handle = Some(TestProcessHandle(0)); - let testee = OpenVpnMonitor::new_internal(builder, |_, _| {}, "").unwrap(); + let testee = OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None).unwrap(); assert!(testee.wait().is_ok()); } @@ -344,7 +418,7 @@ mod tests { fn exit_error() { let mut builder = TestOpenVpnBuilder::default(); builder.process_handle = Some(TestProcessHandle(1)); - let testee = OpenVpnMonitor::new_internal(builder, |_, _| {}, "").unwrap(); + let testee = OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None).unwrap(); assert!(testee.wait().is_err()); } @@ -352,7 +426,7 @@ mod tests { fn wait_closed() { let mut builder = TestOpenVpnBuilder::default(); builder.process_handle = Some(TestProcessHandle(1)); - let testee = OpenVpnMonitor::new_internal(builder, |_, _| {}, "").unwrap(); + let testee = OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None).unwrap(); testee.close_handle().close().unwrap(); assert!(testee.wait().is_ok()); } @@ -360,7 +434,7 @@ mod tests { #[test] fn failed_process_start() { let builder = TestOpenVpnBuilder::default(); - let error = OpenVpnMonitor::new_internal(builder, |_, _| {}, "").unwrap_err(); + let error = OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None).unwrap_err(); match error.kind() { ErrorKind::ChildProcessError(_) => (), _ => panic!("Wrong error"), diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 961889d6c2..db28a200ef 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -9,9 +9,9 @@ use talpid_types::{ }; use super::{ - AfterDisconnect, ConnectingState, DisconnectingState, EventConsequence, Result, ResultExt, - SharedTunnelStateValues, TunnelCommand, TunnelParameters, TunnelState, TunnelStateTransition, - TunnelStateWrapper, + AfterDisconnect, BlockedState, ConnectingState, DisconnectingState, EventConsequence, Result, + ResultExt, SharedTunnelStateValues, TunnelCommand, TunnelParameters, TunnelState, + TunnelStateTransition, TunnelStateWrapper, }; use crate::{ security::SecurityPolicy, @@ -22,7 +22,7 @@ pub struct ConnectedStateBootstrap { pub metadata: TunnelMetadata, pub tunnel_events: mpsc::UnboundedReceiver<TunnelEvent>, pub tunnel_parameters: TunnelParameters, - pub tunnel_close_event: oneshot::Receiver<()>, + pub tunnel_close_event: oneshot::Receiver<Option<BlockReason>>, pub close_handle: CloseHandle, } @@ -31,7 +31,7 @@ pub struct ConnectedState { metadata: TunnelMetadata, tunnel_events: mpsc::UnboundedReceiver<TunnelEvent>, tunnel_parameters: TunnelParameters, - tunnel_close_event: oneshot::Receiver<()>, + tunnel_close_event: oneshot::Receiver<Option<BlockReason>>, close_handle: CloseHandle, } @@ -170,7 +170,11 @@ impl ConnectedState { use self::EventConsequence::*; match self.tunnel_close_event.poll() { - Ok(Async::Ready(_)) => {} + Ok(Async::Ready(block_reason)) => { + if let Some(reason) = block_reason { + return NewState(BlockedState::enter(shared_values, reason)); + } + } Ok(Async::NotReady) => return NoEvents(self), Err(_cancelled) => log::warn!("Tunnel monitor thread has stopped unexpectedly"), } diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 20fdb249ff..a549145e90 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -54,7 +54,7 @@ error_chain! { pub struct ConnectingState { tunnel_events: mpsc::UnboundedReceiver<TunnelEvent>, tunnel_parameters: TunnelParameters, - tunnel_close_event: oneshot::Receiver<()>, + tunnel_close_event: oneshot::Receiver<Option<BlockReason>>, close_handle: CloseHandle, retry_attempt: u32, } @@ -124,7 +124,7 @@ impl ConnectingState { ¶meters.options, TUNNEL_INTERFACE_ALIAS.to_owned().map(OsString::from), ¶meters.username, - log_file.as_ref().map(PathBuf::as_path), + log_file.clone(), resource_dir, on_tunnel_event, )?) @@ -147,25 +147,23 @@ impl ConnectingState { } } - fn spawn_tunnel_monitor_wait_thread(tunnel_monitor: TunnelMonitor) -> oneshot::Receiver<()> { + fn spawn_tunnel_monitor_wait_thread( + tunnel_monitor: TunnelMonitor, + ) -> oneshot::Receiver<Option<BlockReason>> { let (tunnel_close_event_tx, tunnel_close_event_rx) = oneshot::channel(); thread::spawn(move || { let start = Instant::now(); - match tunnel_monitor.wait() { - Ok(_) => debug!("Tunnel has finished without errors"), - Err(error) => { - let chained_error = error.chain_err(|| "Tunnel has stopped unexpectedly"); - warn!("{}", chained_error.display_chain()); - } - } + let block_reason = Self::wait_for_tunnel_monitor(tunnel_monitor); - if let Some(remaining_time) = MIN_TUNNEL_ALIVE_TIME.checked_sub(start.elapsed()) { - thread::sleep(remaining_time); + if block_reason.is_none() { + if let Some(remaining_time) = MIN_TUNNEL_ALIVE_TIME.checked_sub(start.elapsed()) { + thread::sleep(remaining_time); + } } - if tunnel_close_event_tx.send(()).is_err() { + if tunnel_close_event_tx.send(block_reason).is_err() { warn!("Tunnel state machine stopped before receiving tunnel closed event"); } @@ -175,6 +173,39 @@ impl ConnectingState { tunnel_close_event_rx } + fn wait_for_tunnel_monitor(tunnel_monitor: TunnelMonitor) -> Option<BlockReason> { + match tunnel_monitor.wait() { + Ok(_) => { + debug!("Tunnel has finished without errors"); + None + } + Err(error) => match error { + #[cfg(windows)] + error @ tunnel::Error( + tunnel::ErrorKind::OpenVpnTunnelMonitoringError( + tunnel::openvpn::ErrorKind::DisabledTapAdapter, + ), + _, + ) + | error @ tunnel::Error( + tunnel::ErrorKind::OpenVpnTunnelMonitoringError( + tunnel::openvpn::ErrorKind::MissingTapAdapter, + ), + _, + ) => { + let chained_error = error.chain_err(|| "TAP adapter problem detected"); + warn!("{}", chained_error.display_chain()); + Some(BlockReason::TapAdapterProblem) + } + error => { + let chained_error = error.chain_err(|| "Tunnel has stopped unexpectedly"); + warn!("{}", chained_error.display_chain()); + None + } + }, + } + } + fn into_connected_state_bootstrap(self, metadata: TunnelMetadata) -> ConnectedStateBootstrap { ConnectedStateBootstrap { metadata, @@ -300,7 +331,11 @@ impl ConnectingState { shared_values: &mut SharedTunnelStateValues, ) -> EventConsequence<Self> { match self.tunnel_close_event.poll() { - Ok(Async::Ready(_)) => {} + Ok(Async::Ready(block_reason)) => { + if let Some(reason) = block_reason { + return EventConsequence::NewState(BlockedState::enter(shared_values, reason)); + } + } Ok(Async::NotReady) => return EventConsequence::NoEvents(self), Err(_cancelled) => warn!("Tunnel monitor thread has stopped unexpectedly"), } diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index ec8be8cfe1..6f470fe370 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -16,7 +16,7 @@ use crate::tunnel::CloseHandle; /// This state is active from when we manually trigger a tunnel kill until the tunnel wait /// operation (TunnelExit) returned. pub struct DisconnectingState { - exited: oneshot::Receiver<()>, + exited: oneshot::Receiver<Option<BlockReason>>, after_disconnect: AfterDisconnect, } @@ -103,14 +103,22 @@ impl DisconnectingState { match self.exited.poll() { Ok(Async::NotReady) => NoEvents(self), - Ok(Async::Ready(_)) | Err(_) => NewState(self.after_disconnect(shared_values)), + Ok(Async::Ready(block_reason)) => { + NewState(self.after_disconnect(block_reason, shared_values)) + } + Err(_) => NewState(self.after_disconnect(None, shared_values)), } } fn after_disconnect( self, + block_reason: Option<BlockReason>, shared_values: &mut SharedTunnelStateValues, ) -> (TunnelStateWrapper, TunnelStateTransition) { + if let Some(reason) = block_reason { + return BlockedState::enter(shared_values, reason); + } + match self.after_disconnect { AfterDisconnect::Nothing => DisconnectedState::enter(shared_values, ()), AfterDisconnect::Block(reason) => BlockedState::enter(shared_values, reason), @@ -122,7 +130,11 @@ impl DisconnectingState { } impl TunnelState for DisconnectingState { - type Bootstrap = (CloseHandle, oneshot::Receiver<()>, AfterDisconnect); + type Bootstrap = ( + CloseHandle, + oneshot::Receiver<Option<BlockReason>>, + AfterDisconnect, + ); fn enter( _: &mut SharedTunnelStateValues, diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index f3b28e8bfe..613204ab19 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -56,6 +56,8 @@ pub enum BlockReason { NoMatchingRelay, /// This device is offline, no tunnels can be established. IsOffline, + /// A problem with the TAP adapter has been detected. + TapAdapterProblem, } impl fmt::Display for BlockReason { @@ -78,6 +80,7 @@ impl fmt::Display for BlockReason { StartTunnelError => "Failed to start connection to remote server", NoMatchingRelay => "No relay server matches the current settings", IsOffline => "This device is offline, no tunnels can be established", + TapAdapterProblem => "A problem with the TAP adapter has been detected", }; write!(f, "{}", description) |
