diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2018-11-09 12:59:01 +0100 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2018-11-09 12:59:01 +0100 |
| commit | 900833dedc44c99aa235a11d6daf62feeddd7c0f (patch) | |
| tree | b71cdfc62135f203123ab38512c959f1d5444b80 | |
| parent | 6470f1c4651b3041144f43728af12b95d29d2750 (diff) | |
| parent | b81d8e748f1b31bc9b156d28e42e5383faad4314 (diff) | |
| download | mullvadvpn-900833dedc44c99aa235a11d6daf62feeddd7c0f.tar.xz mullvadvpn-900833dedc44c99aa235a11d6daf62feeddd7c0f.zip | |
Merge branch 'add-macos-offline-state'
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/NotificationArea.js | 2 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/daemon-rpc.js | 4 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 2 | ||||
| -rw-r--r-- | talpid-core/src/offline/dummy.rs | 12 | ||||
| -rw-r--r-- | talpid-core/src/offline/macos.rs | 83 | ||||
| -rw-r--r-- | talpid-core/src/offline/mod.rs | 9 | ||||
| -rw-r--r-- | talpid-core/src/security/macos/dns.rs | 4 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/blocked_state.rs | 16 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 15 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 18 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnected_state.rs | 4 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnecting_state.rs | 20 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 17 | ||||
| -rw-r--r-- | talpid-types/src/tunnel.rs | 22 |
15 files changed, 214 insertions, 18 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4795142d..1cd4059435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ Line wrap the file at 100 chars. Th - Allow DHCPv6 in the firewall. - CLI command `relay update` that triggers an update of the relay list in the daemon. +# macOS +- Detect if the computer is offline. If so, don't sit in a reconnect loop, instead block and show + an error message. + ### Fixed - Pick new random relay for each reconnect attempt instead of just retrying with the same one. - Make the `problem-report` tool fall back to the bundled API IP if DNS resolution fails. diff --git a/gui/packages/desktop/src/renderer/components/NotificationArea.js b/gui/packages/desktop/src/renderer/components/NotificationArea.js index df8c63706c..936a29b5d0 100644 --- a/gui/packages/desktop/src/renderer/components/NotificationArea.js +++ b/gui/packages/desktop/src/renderer/components/NotificationArea.js @@ -45,6 +45,8 @@ function getBlockReasonMessage(blockReason: BlockReason): string { return 'Failed to start tunnel connection'; case 'no_matching_relay': return 'No relay server matches the current settings'; + case 'is_offline': + return 'This device is offline, no tunnels can be established'; default: return `Unknown error: ${(blockReason.reason: empty)}`; } diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js index 1b2bb0523e..5417ec4f21 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js @@ -50,7 +50,8 @@ export type BlockReason = | 'ipv6_unavailable' | 'set_security_policy_error' | 'start_tunnel_error' - | 'no_matching_relay', + | 'no_matching_relay' + | 'is_offline', } | { reason: 'auth_failed', details: ?string }; @@ -310,6 +311,7 @@ const TunnelStateTransitionSchema = oneOf( 'set_security_policy_error', 'start_tunnel_error', 'no_matching_relay', + 'is_offline', ), }), object({ diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index f2bfb7718f..20c9302c8a 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -43,6 +43,8 @@ extern crate talpid_types; #[cfg(windows)] mod winnet; +mod offline; + /// Working with processes. pub mod process; diff --git a/talpid-core/src/offline/dummy.rs b/talpid-core/src/offline/dummy.rs new file mode 100644 index 0000000000..c654b846b8 --- /dev/null +++ b/talpid-core/src/offline/dummy.rs @@ -0,0 +1,12 @@ +use futures::sync::mpsc::UnboundedSender; +use tunnel_state_machine::TunnelCommand; + +error_chain!{} + +pub fn spawn_monitor(_sender: UnboundedSender<TunnelCommand>) -> Result<()> { + Ok(()) +} + +pub fn is_offline() -> bool { + false +} diff --git a/talpid-core/src/offline/macos.rs b/talpid-core/src/offline/macos.rs new file mode 100644 index 0000000000..12362eb16d --- /dev/null +++ b/talpid-core/src/offline/macos.rs @@ -0,0 +1,83 @@ +extern crate system_configuration; + +use self::system_configuration::{ + core_foundation::{ + array::CFArray, + runloop::{kCFRunLoopCommonModes, CFRunLoop}, + string::CFString, + }, + dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext}, +}; +use futures::sync::mpsc::UnboundedSender; +use log::{debug, trace}; +use std::{sync::mpsc, thread}; +use tunnel_state_machine::TunnelCommand; + +const PRIMARY_INTERFACE_KEY: &str = "State:/Network/Global/IPv4"; + +error_chain! { + errors { + DynamicStoreInitError { description("Failed to initialize dynamic store") } + } +} + +pub fn spawn_monitor(sender: UnboundedSender<TunnelCommand>) -> Result<()> { + let (result_tx, result_rx) = mpsc::channel(); + thread::spawn(move || match create_dynamic_store(sender) { + Ok(store) => { + result_tx.send(Ok(())).unwrap(); + run_dynamic_store_runloop(store); + log::error!("Core Foundation main loop exited! It should run forever"); + } + Err(e) => result_tx.send(Err(e)).unwrap(), + }); + result_rx.recv().unwrap() +} + +pub fn is_offline() -> bool { + let store = SCDynamicStoreBuilder::new("talpid-primary-interface").build(); + let is_offline = store.get(CFString::new(PRIMARY_INTERFACE_KEY)).is_none(); + is_offline +} + +fn create_dynamic_store(sender: UnboundedSender<TunnelCommand>) -> Result<SCDynamicStore> { + let callback_context = SCDynamicStoreCallBackContext { + callout: primary_interface_change_callback, + info: sender, + }; + + let store = SCDynamicStoreBuilder::new("talpid-primary-interface") + .callback_context(callback_context) + .build(); + + let watch_keys = CFArray::from_CFTypes(&[CFString::new(PRIMARY_INTERFACE_KEY)]); + let watch_patterns: CFArray<CFString> = CFArray::from_CFTypes(&[]); + + if store.set_notification_keys(&watch_keys, &watch_patterns) { + trace!("Registered for dynamic store notifications"); + Ok(store) + } else { + bail!(ErrorKind::DynamicStoreInitError) + } +} + +fn run_dynamic_store_runloop(store: SCDynamicStore) { + let run_loop_source = store.create_run_loop_source(); + CFRunLoop::get_current().add_source(&run_loop_source, unsafe { kCFRunLoopCommonModes }); + + trace!("Entering primary interface CFRunLoop"); + CFRunLoop::run_current(); +} + +fn primary_interface_change_callback( + store: SCDynamicStore, + _changed_keys: CFArray<CFString>, + state: &mut UnboundedSender<TunnelCommand>, +) { + let is_offline = store.get(CFString::new(PRIMARY_INTERFACE_KEY)).is_none(); + debug!( + "Computer went {}", + if is_offline { "offline" } else { "online" } + ); + let _ = state.unbounded_send(TunnelCommand::IsOffline(is_offline)); +} diff --git a/talpid-core/src/offline/mod.rs b/talpid-core/src/offline/mod.rs new file mode 100644 index 0000000000..696e7225b3 --- /dev/null +++ b/talpid-core/src/offline/mod.rs @@ -0,0 +1,9 @@ +#[cfg(target_os = "macos")] +#[path = "macos.rs"] +mod imp; + +#[cfg(not(target_os = "macos"))] +#[path = "dummy.rs"] +mod imp; + +pub use self::imp::{is_offline, spawn_monitor}; diff --git a/talpid-core/src/security/macos/dns.rs b/talpid-core/src/security/macos/dns.rs index 391dbe7887..3ae909f10c 100644 --- a/talpid-core/src/security/macos/dns.rs +++ b/talpid-core/src/security/macos/dns.rs @@ -218,7 +218,7 @@ fn create_dynamic_store(state: Arc<Mutex<Option<State>>>) -> Result<SCDynamicSto info: state, }; - let store = SCDynamicStoreBuilder::new("mullvad-dns-monitor") + let store = SCDynamicStoreBuilder::new("talpid-dns-monitor") .callback_context(callback_context) .build(); @@ -240,7 +240,7 @@ fn run_dynamic_store_runloop(store: SCDynamicStore) { let run_loop_source = store.create_run_loop_source(); CFRunLoop::get_current().add_source(&run_loop_source, unsafe { kCFRunLoopCommonModes }); - trace!("Entering CFRunLoop"); + trace!("Entering DNS CFRunLoop"); CFRunLoop::run_current(); } diff --git a/talpid-core/src/tunnel_state_machine/blocked_state.rs b/talpid-core/src/tunnel_state_machine/blocked_state.rs index 432dea3999..3e63551110 100644 --- a/talpid-core/src/tunnel_state_machine/blocked_state.rs +++ b/talpid-core/src/tunnel_state_machine/blocked_state.rs @@ -10,7 +10,9 @@ use super::{ use crate::security::SecurityPolicy; /// No tunnel is running and all network connections are blocked. -pub struct BlockedState; +pub struct BlockedState { + block_reason: BlockReason, +} impl BlockedState { fn set_security_policy(shared_values: &mut SharedTunnelStateValues) { @@ -36,7 +38,9 @@ impl TunnelState for BlockedState { ) -> (TunnelStateWrapper, TunnelStateTransition) { Self::set_security_policy(shared_values); ( - TunnelStateWrapper::from(BlockedState), + TunnelStateWrapper::from(BlockedState { + block_reason: block_reason.clone(), + }), TunnelStateTransition::Blocked(block_reason), ) } @@ -54,6 +58,14 @@ impl TunnelState for BlockedState { Self::set_security_policy(shared_values); SameState(self) } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + if !is_offline && self.block_reason == BlockReason::IsOffline { + NewState(ConnectingState::enter(shared_values, 0)) + } else { + SameState(self) + } + } Ok(TunnelCommand::Connect) => NewState(ConnectingState::enter(shared_values, 0)), Ok(TunnelCommand::Disconnect) | Err(_) => { NewState(DisconnectedState::enter(shared_values, ())) diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 66f9b78bc1..577f435260 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -94,6 +94,21 @@ impl ConnectedState { } } } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + if is_offline { + NewState(DisconnectingState::enter( + shared_values, + ( + self.close_handle, + self.tunnel_close_event, + AfterDisconnect::Block(BlockReason::IsOffline), + ), + )) + } else { + SameState(self) + } + } Ok(TunnelCommand::Connect) => NewState(DisconnectingState::enter( shared_values, ( diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index f34c0e6667..df0a00744e 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -211,6 +211,21 @@ impl ConnectingState { } } } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + if is_offline { + NewState(DisconnectingState::enter( + shared_values, + ( + self.close_handle, + self.tunnel_close_event, + AfterDisconnect::Block(BlockReason::IsOffline), + ), + )) + } else { + SameState(self) + } + } Ok(TunnelCommand::Connect) => NewState(DisconnectingState::enter( shared_values, ( @@ -300,6 +315,9 @@ impl TunnelState for ConnectingState { shared_values: &mut SharedTunnelStateValues, retry_attempt: u32, ) -> (TunnelStateWrapper, TunnelStateTransition) { + if shared_values.is_offline { + return BlockedState::enter(shared_values, BlockReason::IsOffline); + } match shared_values .tunnel_parameters_generator .generate(retry_attempt) diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index b1819af3f0..60444e9cc7 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -45,6 +45,10 @@ impl TunnelState for DisconnectedState { shared_values.allow_lan = allow_lan; SameState(self) } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + SameState(self) + } Ok(TunnelCommand::Connect) => NewState(ConnectingState::enter(shared_values, 0)), Ok(TunnelCommand::Block(reason)) => { NewState(BlockedState::enter(shared_values, reason)) diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index 339c6e3aa3..7ebb637d2d 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -33,6 +33,10 @@ impl DisconnectingState { shared_values.allow_lan = allow_lan; AfterDisconnect::Nothing } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + AfterDisconnect::Nothing + } Ok(TunnelCommand::Connect) => AfterDisconnect::Reconnect(0), Ok(TunnelCommand::Block(reason)) => AfterDisconnect::Block(reason), _ => AfterDisconnect::Nothing, @@ -42,6 +46,14 @@ impl DisconnectingState { shared_values.allow_lan = allow_lan; AfterDisconnect::Block(reason) } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + if !is_offline && reason == BlockReason::IsOffline { + AfterDisconnect::Reconnect(0) + } else { + AfterDisconnect::Block(reason) + } + } Ok(TunnelCommand::Connect) => AfterDisconnect::Reconnect(0), Ok(TunnelCommand::Disconnect) => AfterDisconnect::Nothing, Ok(TunnelCommand::Block(new_reason)) => AfterDisconnect::Block(new_reason), @@ -52,6 +64,14 @@ impl DisconnectingState { shared_values.allow_lan = allow_lan; AfterDisconnect::Reconnect(retry_attempt) } + Ok(TunnelCommand::IsOffline(is_offline)) => { + shared_values.is_offline = is_offline; + if is_offline { + AfterDisconnect::Block(BlockReason::IsOffline) + } else { + AfterDisconnect::Reconnect(retry_attempt) + } + } Ok(TunnelCommand::Connect) => AfterDisconnect::Reconnect(retry_attempt), Ok(TunnelCommand::Disconnect) | Err(_) => AfterDisconnect::Nothing, Ok(TunnelCommand::Block(reason)) => AfterDisconnect::Block(reason), diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 6582eb0fbc..e44ec91234 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -24,8 +24,7 @@ use self::connected_state::{ConnectedState, ConnectedStateBootstrap}; use self::connecting_state::ConnectingState; use self::disconnected_state::DisconnectedState; use self::disconnecting_state::{AfterDisconnect, DisconnectingState}; -use super::mpsc::IntoSender; -use super::security::NetworkSecurity; +use crate::{mpsc::IntoSender, offline, security::NetworkSecurity}; error_chain! { errors { @@ -55,11 +54,15 @@ where T: From<TunnelStateTransition> + Send + 'static, { let (command_tx, command_rx) = mpsc::unbounded(); - let (startup_result_tx, startup_result_rx) = sync_mpsc::channel(); + offline::spawn_monitor(command_tx.clone()) + .chain_err(|| "Unable to spawn offline state monitor")?; + let is_offline = offline::is_offline(); + let (startup_result_tx, startup_result_rx) = sync_mpsc::channel(); thread::spawn(move || { match create_event_loop( allow_lan, + is_offline, tunnel_parameters_generator, log_dir, resource_dir, @@ -94,6 +97,7 @@ where fn create_event_loop<T>( allow_lan: bool, + is_offline: bool, tunnel_parameters_generator: impl TunnelParametersGenerator, log_dir: Option<PathBuf>, resource_dir: PathBuf, @@ -107,6 +111,7 @@ where let reactor = Core::new().chain_err(|| ErrorKind::ReactorError)?; let state_machine = TunnelStateMachine::new( allow_lan, + is_offline, tunnel_parameters_generator, log_dir, resource_dir, @@ -127,6 +132,8 @@ where pub enum TunnelCommand { /// Enable or disable LAN access in the firewall. AllowLan(bool), + /// Notify the state machine of the connectivity of the device. + IsOffline(bool), /// Open tunnel connection. Connect, /// Close tunnel connection. @@ -161,6 +168,7 @@ struct TunnelStateMachine { impl TunnelStateMachine { fn new( allow_lan: bool, + is_offline: bool, tunnel_parameters_generator: impl TunnelParametersGenerator, log_dir: Option<PathBuf>, resource_dir: PathBuf, @@ -172,6 +180,7 @@ impl TunnelStateMachine { let mut shared_values = SharedTunnelStateValues { security, allow_lan, + is_offline, tunnel_parameters_generator: Box::new(tunnel_parameters_generator), log_dir, resource_dir, @@ -249,6 +258,8 @@ struct SharedTunnelStateValues { security: NetworkSecurity, /// Should LAN access be allowed outside the tunnel. allow_lan: bool, + /// True when the computer is known to be offline. + is_offline: bool, /// The generator of new `TunnelParameter`s tunnel_parameters_generator: Box<dyn TunnelParametersGenerator>, /// Directory to store tunnel log file. diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index cfe1472cdf..8614f09ad0 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -52,14 +52,17 @@ pub enum BlockReason { StartTunnelError, /// No relay server matching the current filter parameters. NoMatchingRelay, + /// This device is offline, no tunnels can be established. + IsOffline, } impl fmt::Display for BlockReason { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::BlockReason::*; let description = match *self { - BlockReason::AuthFailed(ref reason) => { + AuthFailed(ref reason) => { return write!( - formatter, + f, "Authentication with remote server failed: {}", match reason { Some(ref reason) => reason.as_str(), @@ -67,14 +70,13 @@ impl fmt::Display for BlockReason { } ); } - BlockReason::Ipv6Unavailable => { - "Failed to configure IPv6 because it's disabled in the platform" - } - BlockReason::SetSecurityPolicyError => "Failed to set security policy", - BlockReason::StartTunnelError => "Failed to start connection to remote server", - BlockReason::NoMatchingRelay => "No relay server matches the current settings", + Ipv6Unavailable => "Failed to configure IPv6 because it's disabled in the platform", + SetSecurityPolicyError => "Failed to set security policy", + 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", }; - write!(formatter, "{}", description) + write!(f, "{}", description) } } |
