diff options
| author | Emīls <emils@mullvad.net> | 2020-06-09 15:23:04 +0100 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2020-06-09 15:23:04 +0100 |
| commit | 68d2730eaa9f3fce7d914aab91132f30ea34297e (patch) | |
| tree | 9496d4ddfa0622f631320eb9f0ce85692aab55f6 | |
| parent | deaf23daa2e3aa2a3ece934481d105e0e46c99bc (diff) | |
| parent | 1645577dc1e76adb2d87734ab1381b8cc6a5a84c (diff) | |
| download | mullvadvpn-68d2730eaa9f3fce7d914aab91132f30ea34297e.tar.xz mullvadvpn-68d2730eaa9f3fce7d914aab91132f30ea34297e.zip | |
Merge branch 'macos-use-network-reachability'
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | Cargo.lock | 33 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 4 | ||||
| -rw-r--r-- | talpid-core/src/offline/macos.rs | 195 |
4 files changed, 175 insertions, 60 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a86a29045a..6845ea5c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ Line wrap the file at 100 chars. Th - Send an ICMP reject message or TCP reset packet when blocking outgoing packets to prevent timeouts. +#### macOS +- Use `SCNetworkReachability` to help determine connectivity of host. + #### Android - Show the remaining account time in the Settings screen in days if it's less than 3 months. - Prevent commands to connect or disconnect to be sent when the device is locked. diff --git a/Cargo.lock b/Cargo.lock index df894cd109..6de2bee9a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,15 +307,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "core-foundation" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "core-foundation" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ @@ -325,11 +316,6 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "core-foundation-sys" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2780,19 +2766,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", - "system-configuration-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "system-configuration-sys 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "system-configuration-sys" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2838,7 +2825,7 @@ dependencies = [ "rtnetlink 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", - "system-configuration 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "system-configuration 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "talpid-ipc 0.1.0", "talpid-types 0.1.0", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3912,9 +3899,7 @@ dependencies = [ "checksum colored 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6cdb90b60f2927f8d76139c72dbde7e10c3a2bc47c8594c9c7a66529f2687c03" "checksum combine 3.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" "checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" -"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" "checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" "checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" "checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71" "checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" @@ -4158,8 +4143,8 @@ dependencies = [ "checksum syntex_errors 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "04c48f32867b6114449155b2a82114b86d4b09e1bddb21c47ff104ab9172b646" "checksum syntex_pos 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd49988e52451813c61fecbe9abb5cfd4e1b7bb6cdbb980a6fbcbab859171a6" "checksum syntex_syntax 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7628a0506e8f9666fdabb5f265d0059b059edac9a3f810bda077abb5d826bd8d" -"checksum system-configuration 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df3dc3e701a89dd6764083d19f048b57ec01c26d0904ff8108a507059a6462e6" -"checksum system-configuration-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bfd906a2882d54084bfdf517bf03892ac06820f1c0a3d37e48609f334798ad99" +"checksum system-configuration 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4bc0637a2b8c0b1a5145cca3e21b707865edc7e32285771536af1ade129468" +"checksum system-configuration-sys 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "269e271436d8e4bb2621c535a11fe03d5d012f74b19af72f80288f3a72f6180a" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 163eb641c7..f18c867cfe 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -19,7 +19,7 @@ ipnetwork = "0.16" jsonrpc-core = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } jsonrpc-macros = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } lazy_static = "1.0" -libc = "0.2.20" +libc = "0.2" log = "0.4" openvpn-plugin = { git = "https://github.com/mullvad/openvpn-plugin-rs", branch = "auth-failed-event", features = ["serde"] } os_pipe = "0.8" @@ -69,7 +69,7 @@ tun = "0.4.3" [target.'cfg(target_os = "macos")'.dependencies] pfctl = "0.4" -system-configuration = "0.3" +system-configuration = "0.4" tun = "0.4.3" diff --git a/talpid-core/src/offline/macos.rs b/talpid-core/src/offline/macos.rs index fa0cd3c906..602da6cda9 100644 --- a/talpid-core/src/offline/macos.rs +++ b/talpid-core/src/offline/macos.rs @@ -1,58 +1,196 @@ use crate::tunnel_state_machine::TunnelCommand; use futures01::sync::mpsc::UnboundedSender; -use log::{debug, trace}; use std::{ - sync::{mpsc, Weak}, + net::{Ipv4Addr, SocketAddr}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, Weak, + }, thread, }; use system_configuration::{ core_foundation::{ array::CFArray, + base::{CFType, TCFType, ToVoid}, + boolean::CFBoolean, + dictionary::CFDictionary, runloop::{kCFRunLoopCommonModes, CFRunLoop}, string::CFString, }, dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext}, + network_configuration::{self, SCNetworkInterface, SCNetworkInterfaceType}, + network_reachability::{ + ReachabilityFlags, SCNetworkReachability, SchedulingError, SetCallbackError, + }, }; const PRIMARY_INTERFACE_KEY: &str = "State:/Network/Global/IPv4"; - #[derive(err_derive::Error, Debug)] pub enum Error { #[error(display = "Failed to initialize dynamic store")] DynamicStoreInitError, + #[error(display = "Failed to schedule reachability callback")] + ScheduleReachabilityCallbackError(#[error(source)] SchedulingError), + #[error(display = "Failed to set reachability callback")] + SetCallbackError(#[error(source)] SetCallbackError), + #[error(display = "Panic during initialization")] + InitializationError, } pub struct MonitorHandle; +impl MonitorHandle { + /// Host is considered to be offline if the IPv4 internet is considered to be unreachable by the + /// given reachability flags *or* there are no active physical interfaces. + pub fn is_offline(&self) -> bool { + let reachability = SCNetworkReachability::from(ipv4_internet()); + let store = SCDynamicStoreBuilder::new("talpid-offline-check").build(); + reachability + .reachability() + .map(|flags| check_offline_state(&store, flags)) + .unwrap_or(false) + } +} + pub fn spawn_monitor(sender: Weak<UnboundedSender<TunnelCommand>>) -> Result<MonitorHandle, Error> { 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"); + thread::spawn(move || { + let mut reachability_ref = SCNetworkReachability::from(ipv4_internet()); + let store = SCDynamicStoreBuilder::new("talpid-offline-watcher").build(); + + let is_currently_offline = match reachability_ref.reachability() { + Ok(flags) => check_offline_state(&store, flags), + Err(_) => { + log::error!("Failed to obtain current connectivity, assuming machine is online"); + false + } + }; + + let context = OfflineStateContext { + sender, + is_offline: Arc::new(AtomicBool::new(is_currently_offline)), + }; + + + let result = || -> Result<SCDynamicStore, Error> { + let dynamic_store = create_dynamic_store(context.clone())?; + CFRunLoop::get_current().add_source(&dynamic_store.create_run_loop_source(), unsafe { + kCFRunLoopCommonModes + }); + + reachability_ref.set_callback(move |flags| { + let store = SCDynamicStoreBuilder::new("talpid-offline-watcher").build(); + context.new_state(check_offline_state(&store, flags)); + })?; + + reachability_ref.schedule_with_runloop(&CFRunLoop::get_current(), unsafe { + kCFRunLoopCommonModes + })?; + + + Ok(dynamic_store) + }; + + + match result() { + Ok(_dynamic_store) => { + let _ = result_tx.send(Ok(())); + CFRunLoop::run_current() + } + Err(err) => { + let _ = result_tx.send(Err(err)); + } } - Err(e) => result_tx.send(Err(e)).unwrap(), }); - result_rx.recv().unwrap().map(|_| MonitorHandle) + + let _ = result_rx.recv().map_err(|_| Error::InitializationError)??; + Ok(MonitorHandle {}) } -impl MonitorHandle { - pub fn is_offline(&self) -> bool { - let store = SCDynamicStoreBuilder::new("talpid-primary-interface").build(); - let is_offline = store.get(CFString::new(PRIMARY_INTERFACE_KEY)).is_none(); - is_offline +fn ipv4_internet() -> SocketAddr { + SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0) +} + +fn check_offline_state(store: &SCDynamicStore, flags: ReachabilityFlags) -> bool { + let is_offline = + !flags.contains(ReachabilityFlags::REACHABLE) || !exists_active_physical_iface(store); + is_offline +} + +fn exists_active_physical_iface(store: &SCDynamicStore) -> bool { + network_configuration::get_interfaces().iter().any(|iface| { + let is_physical = iface_is_physical(&*iface); + let is_active = iface_is_active(&*iface, store); + let is_valid = is_physical && is_active; + if is_valid { + log::trace!( + "Considering interface {:?} {:?} to be active and physical", + iface.bsd_name(), + iface.display_name() + ); + } + is_valid + }) +} + +fn iface_is_active(iface: &SCNetworkInterface, store: &SCDynamicStore) -> bool { + || -> Option<bool> { + let path = format!("State:/Network/Interface/{}/Link", iface.bsd_name()?); + let link_properties = store + .get(CFString::from(path.as_ref()))? + .downcast::<CFDictionary>()?; + + let active_ptr = link_properties.find(CFString::from("Active").to_void())?; + if active_ptr.is_null() { + return None; + } + + unsafe { CFType::wrap_under_get_rule(*active_ptr) } + .downcast::<CFBoolean>() + .map(Into::into) + }() + .unwrap_or(false) +} + +fn iface_is_physical(iface: &SCNetworkInterface) -> bool { + use SCNetworkInterfaceType::*; + match iface.interface_type() { + Some(iface_type) => match iface_type { + Bluetooth | Modem | Serial | IrDA | Ethernet | FireWire | WWAN | IEEE80211 => true, + _ => false, + }, + // if interface type is unknown, have to assume it provides internet + None => true, } } -fn create_dynamic_store( +#[derive(Clone)] +struct OfflineStateContext { sender: Weak<UnboundedSender<TunnelCommand>>, -) -> Result<SCDynamicStore, Error> { + is_offline: Arc<AtomicBool>, +} + +impl OfflineStateContext { + fn no_primary_interface(&self) { + self.new_state(true); + } + + fn new_state(&self, is_offline: bool) { + if self.is_offline.swap(is_offline, Ordering::SeqCst) != is_offline { + if let Some(sender) = self.sender.upgrade() { + let cmd = TunnelCommand::IsOffline(is_offline); + let _ = sender.unbounded_send(cmd); + } + } + } +} + +fn create_dynamic_store(context: OfflineStateContext) -> Result<SCDynamicStore, Error> { let callback_context = SCDynamicStoreCallBackContext { callout: primary_interface_change_callback, - info: sender, + info: context, }; let store = SCDynamicStoreBuilder::new("talpid-primary-interface") @@ -63,32 +201,21 @@ fn create_dynamic_store( let watch_patterns: CFArray<CFString> = CFArray::from_CFTypes(&[]); if store.set_notification_keys(&watch_keys, &watch_patterns) { - trace!("Registered for dynamic store notifications"); + log::trace!("Registered for dynamic store notifications"); Ok(store) } else { Err(Error::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 Weak<UnboundedSender<TunnelCommand>>, + state: &mut OfflineStateContext, ) { let is_offline = store.get(CFString::new(PRIMARY_INTERFACE_KEY)).is_none(); - debug!( - "Computer went {}", - if is_offline { "offline" } else { "online" } - ); - if let Some(state) = state.upgrade() { - let _ = state.unbounded_send(TunnelCommand::IsOffline(is_offline)); + if is_offline { + log::debug!("No primary interface, considering host to be offline"); + state.no_primary_interface(); } } |
