diff options
| -rw-r--r-- | talpid-core/Cargo.toml | 2 | ||||
| -rw-r--r-- | talpid-core/src/firewall/macos/dns.rs | 269 | ||||
| -rw-r--r-- | talpid-core/src/firewall/macos/mod.rs (renamed from talpid-core/src/firewall/macos.rs) | 40 | ||||
| -rw-r--r-- | talpid-core/src/firewall/mod.rs | 17 |
4 files changed, 309 insertions, 19 deletions
diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 9ef9999560..32e36c9ed2 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -24,4 +24,6 @@ libc = "0.2.20" [target.'cfg(target_os = "macos")'.dependencies] pfctl = { git = "https://github.com/mullvad/pfctl-rs", rev = "dae436f6ee4e3529fc995c5192b314f1cc8dccec" } +system-configuration = { git = "https://github.com/mullvad/system-configuration-rs", version = "0.1.0" } +core-foundation = "0.4.6" tokio-core = "0.1" diff --git a/talpid-core/src/firewall/macos/dns.rs b/talpid-core/src/firewall/macos/dns.rs new file mode 100644 index 0000000000..8368de66ac --- /dev/null +++ b/talpid-core/src/firewall/macos/dns.rs @@ -0,0 +1,269 @@ +extern crate core_foundation; +extern crate system_configuration; + +use self::core_foundation::array::{CFArray, CFArrayRef}; +use self::core_foundation::base::{CFType, TCFType}; +use self::core_foundation::dictionary::CFDictionary; +use self::core_foundation::runloop::{CFRunLoop, kCFRunLoopCommonModes}; +use self::core_foundation::string::{CFString, CFStringRef}; + +use self::system_configuration::dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, + SCDynamicStoreCallBackContext}; + +use error_chain::ChainedError; + +use std::collections::HashMap; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; + +error_chain! { + errors { + SettingDnsFailed { description("Error while setting DNS servers") } + DynamicStoreInitError { description("Failed to initialize dynamic store") } + } +} + +const ALL_DNS_SETTINGS_PATH_PATTERN: &str = "(Setup|State):/Network/Service/.*/DNS"; + +type ServicePath = String; +type DnsServer = String; + +struct State { + desired_dns: Vec<DnsServer>, + backup: HashMap<ServicePath, Option<Vec<DnsServer>>>, +} + +pub struct DnsMonitor { + store: SCDynamicStore, + + /// The current DNS injection state. If this is `None` it means we are not injecting any DNS. + /// When it's `Some(state)` we are actively making sure `state.desired_dns` is configured + /// on all network interfaces. + state: Arc<Mutex<Option<State>>>, +} + +impl DnsMonitor { + /// Creates and returns a new `DnsMonitor`. This spawns a background thread that will monitor + /// DNS settings for all network interfaces. If any changes occur it will instantly reset + /// the DNS settings for that interface back to the last server list set to this instance + /// with `set_dns`. + pub fn new() -> Result<Self> { + let state = Arc::new(Mutex::new(None)); + Self::spawn(state.clone())?; + Ok(DnsMonitor { + store: SCDynamicStoreBuilder::new("mullvad-dns").build(), + state, + }) + } + + /// Spawns the background thread running the CoreFoundation main loop and monitors the system + /// for DNS changes. + fn spawn(state: Arc<Mutex<Option<State>>>) -> Result<()> { + let (result_tx, result_rx) = mpsc::channel(); + thread::spawn(move || match create_dynamic_store(state) { + Ok(store) => { + result_tx.send(Ok(())).unwrap(); + run_dynamic_store_runloop(store); + // TODO(linus): This is critical. Improve later by sending error signal to Daemon + error!("Core Foundation main loop exited! It should run forever"); + } + Err(e) => result_tx.send(Err(e)).unwrap(), + }); + result_rx.recv().unwrap() + } + + pub fn set_dns(&self, servers: Vec<DnsServer>) -> Result<()> { + let mut state_lock = self.state.lock().unwrap(); + *state_lock = Some(match state_lock.take() { + None => { + debug!("Setting DNS to [{}]", servers.join(", ")); + let backup = read_all_dns(&self.store); + for service_path in backup.keys() { + set_dns(&self.store, CFString::new(service_path), &servers)?; + } + State { + desired_dns: servers, + backup, + } + } + Some(state) => if servers != state.desired_dns { + debug!("Changing DNS to [{}]", servers.join(", ")); + for service_path in state.backup.keys() { + set_dns(&self.store, CFString::new(service_path), &servers)?; + } + State { + desired_dns: servers, + backup: state.backup, + } + } else { + debug!("No change, new DNS same as the one already set"); + state + }, + }); + Ok(()) + } + + /// Reset all DNS settings to the latest backed up values. + pub fn reset(&self) -> Result<()> { + let mut state_lock = self.state.lock().unwrap(); + if let Some(state) = state_lock.take() { + for (service_path, servers) in state.backup { + if let Some(servers) = servers { + set_dns(&self.store, CFString::new(&service_path), &servers)?; + } else { + debug!("Removing DNS for {}", service_path); + if !self.store.remove(CFString::new(&service_path)) { + bail!(ErrorKind::SettingDnsFailed); + } + } + } + } + Ok(()) + } +} + +/// Creates a `SCDynamicStore` that watches all network interfaces for changes to the DNS settings. +fn create_dynamic_store(state: Arc<Mutex<Option<State>>>) -> Result<SCDynamicStore> { + let callback_context = SCDynamicStoreCallBackContext { + callout: dns_change_callback, + info: state, + }; + + let store = SCDynamicStoreBuilder::new("mullvad-dns-monitor") + .callback_context(callback_context) + .build(); + + let watch_keys: CFArray<CFString> = CFArray::from_CFTypes(&[]); + let watch_patterns = CFArray::from_CFTypes(&[CFString::new(ALL_DNS_SETTINGS_PATH_PATTERN)]); + + 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 CFRunLoop"); + CFRunLoop::run_current(); +} + +/// This function is called by the Core Foundation event loop when there is a change to one or more +/// watched dynamic store values. In our case we watch all DNS settings. +fn dns_change_callback( + store: SCDynamicStore, + changed_keys: CFArray<CFString>, + state: &mut Arc<Mutex<Option<State>>>, +) { + let mut state_lock = state.lock().unwrap(); + match *state_lock { + None => { + trace!("Not injecting DNS at this time"); + } + Some(ref mut state) => for path_ptr in changed_keys.as_untyped().iter() { + let path = unsafe { CFString::wrap_under_get_rule(path_ptr as CFStringRef) }; + let should_set_dns = match read_dns(&store, path.clone()) { + None => { + debug!("Detected DNS removed for {}", path); + state.backup.insert(path.to_string(), None); + true + } + Some(servers) => if servers != state.desired_dns { + debug!( + "Detected DNS changed to [{}] for {}", + servers.join(", "), + path + ); + state.backup.insert(path.to_string(), Some(servers)); + true + } else { + false + }, + }; + if should_set_dns { + if let Err(error) = set_dns(&store, path.clone(), &state.desired_dns) { + let e = error.chain_err(|| format!("Failed changing DNS for {}", path)); + error!("{}", e.display_chain()); + } + } + }, + } +} + +/// Set the dynamic store entry at `path` to a dictionary with the given servers under the +/// "ServerAddresses" key. +fn set_dns(store: &SCDynamicStore, path: CFString, servers: &[DnsServer]) -> Result<()> { + debug!("Setting DNS to [{}] for {}", servers.join(", "), path); + let server_addresses_key = CFString::from_static_string("ServerAddresses"); + + let cf_string_servers: Vec<CFString> = servers.iter().map(|s| CFString::new(s)).collect(); + let server_addresses_value = CFArray::from_CFTypes(&cf_string_servers); + + let dns_dictionary = + CFDictionary::from_CFType_pairs(&[(server_addresses_key, server_addresses_value)]); + + if store.set(path, &dns_dictionary) { + Ok(()) + } else { + bail!(ErrorKind::SettingDnsFailed) + } +} + +/// Read all existing DNS settings and return them. +fn read_all_dns(store: &SCDynamicStore) -> HashMap<ServicePath, Option<Vec<DnsServer>>> { + let mut backup = HashMap::new(); + if let Some(paths) = store.get_keys(ALL_DNS_SETTINGS_PATH_PATTERN) { + for path_ptr in paths.as_untyped().iter() { + let path = unsafe { CFString::wrap_under_get_rule(path_ptr as CFStringRef) }; + backup.insert(path.to_string(), read_dns(store, path)); + } + } + backup +} + +/// Get DNS settings for a given dynamic store path. Returns `None` If the path does not exist +/// or does not contain the expected format. +fn read_dns(store: &SCDynamicStore, path: CFString) -> Option<Vec<DnsServer>> { + store + .get(path.clone()) + .and_then(|property_list| property_list.downcast::<_, CFDictionary>()) + .and_then(|dictionary| { + dictionary + .find2(&CFString::from_static_string("ServerAddresses")) + .map(|array_ptr| unsafe { + CFType::wrap_under_get_rule(array_ptr) + }) + }) + .and_then(|addresses: CFType| { + if addresses.instance_of::<_, CFArray>() { + let addresses_array = unsafe { + CFArray::wrap_under_get_rule(addresses.as_concrete_TypeRef() as CFArrayRef) + }; + parse_cf_string_array(addresses_array) + } else { + error!("DNS settings is not an array: {:?}", addresses); + None + } + }) +} + +/// Parses a CFArray into a Rust vector of Rust strings, if the array contains CFString instances, +/// otherwise `None` is returned. +fn parse_cf_string_array(array: CFArray) -> Option<Vec<String>> { + let mut strings = Vec::new(); + for string_ptr in array.iter() { + let cf_type = unsafe { CFType::wrap_under_get_rule(string_ptr) }; + if cf_type.instance_of::<_, CFString>() { + let address = unsafe { CFString::wrap_under_get_rule(string_ptr as CFStringRef) }; + strings.push(address.to_string()); + } else { + error!("DNS server entry is not a string: {:?}", cf_type); + return None; + }; + } + Some(strings) +} diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos/mod.rs index 40f841aa81..08e38a09c4 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos/mod.rs @@ -7,16 +7,26 @@ use std::net::Ipv4Addr; use talpid_types::net; +mod dns; + +use self::dns::DnsMonitor; + +error_chain! { + links { + PfCtl(self::pfctl::Error, self::pfctl::ErrorKind); + DnsMonitor(self::dns::Error, self::dns::ErrorKind); + } +} + // alias used to instantiate firewall implementation pub type ConcreteFirewall = PacketFilter; -pub use self::pfctl::{Error, ErrorKind, Result, ResultExt}; const ANCHOR_NAME: &'static str = "mullvad"; pub struct PacketFilter { pf: pfctl::PfCtl, pf_was_enabled: Option<bool>, - + dns_monitor: DnsMonitor, } impl Firewall<Error> for PacketFilter { @@ -24,6 +34,7 @@ impl Firewall<Error> for PacketFilter { Ok(PacketFilter { pf: pfctl::PfCtl::new()?, pf_was_enabled: None, + dns_monitor: DnsMonitor::new()?, }) } @@ -66,7 +77,7 @@ impl PacketFilter { let mut anchor_change = pfctl::AnchorChange::new(); anchor_change.set_filter_rules(new_filter_rules); anchor_change.set_redirect_rules(new_redirect_rules); - self.pf.set_rules(ANCHOR_NAME, anchor_change) + Ok(self.pf.set_rules(ANCHOR_NAME, anchor_change)?) } fn get_policy_specific_rules( @@ -119,7 +130,7 @@ impl PacketFilter { fn get_allow_relay_rule(relay_endpoint: net::Endpoint) -> Result<pfctl::FilterRule> { let pfctl_proto = as_pfctl_proto(relay_endpoint.protocol); - pfctl::FilterRuleBuilder::default() + Ok(pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Pass) .direction(pfctl::Direction::Out) .to(relay_endpoint.address) @@ -127,17 +138,17 @@ impl PacketFilter { .keep_state(pfctl::StatePolicy::Keep) .tcp_flags(Self::get_tcp_flags()) .quick(true) - .build() + .build()?) } fn get_allow_tunnel_rule(tunnel_interface: &str) -> Result<pfctl::FilterRule> { - pfctl::FilterRuleBuilder::default() + Ok(pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Pass) .interface(tunnel_interface) .keep_state(pfctl::StatePolicy::Keep) .tcp_flags(Self::get_tcp_flags()) .quick(true) - .build() + .build()?) } fn get_allow_loopback_rules() -> Result<Vec<pfctl::FilterRule>> { @@ -182,20 +193,21 @@ impl PacketFilter { fn remove_rules(&mut self) -> Result<()> { // remove_anchor() does not deactivate active rules - self.pf.flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Filter) + Ok(self.pf + .flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Filter)?) } fn enable(&mut self) -> Result<()> { if self.pf_was_enabled.is_none() { self.pf_was_enabled = Some(self.pf.is_enabled()?); } - self.pf.try_enable() + Ok(self.pf.try_enable()?) } fn restore_state(&mut self) -> Result<()> { match self.pf_was_enabled.take() { - Some(true) => self.pf.try_enable(), - Some(false) => self.pf.try_disable(), + Some(true) => Ok(self.pf.try_enable()?), + Some(false) => Ok(self.pf.try_disable()?), None => Ok(()), } } @@ -204,14 +216,16 @@ impl PacketFilter { self.pf .try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Filter)?; self.pf - .try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect) + .try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect)?; + Ok(()) } fn remove_anchor(&mut self) -> Result<()> { self.pf .try_remove_anchor(ANCHOR_NAME, pfctl::AnchorKind::Filter)?; self.pf - .try_remove_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect) + .try_remove_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect)?; + Ok(()) } } diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 6ac63e2493..4e6d1611de 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -1,16 +1,21 @@ use talpid_types::net::Endpoint; #[cfg(target_os = "macos")] -#[path = "macos.rs"] -mod imp; +pub mod macos; +#[cfg(target_os = "macos")] +use self::macos as imp; #[cfg(all(unix, not(target_os = "macos")))] -#[path = "unix.rs"] -mod imp; +pub mod unix; +#[cfg(all(unix, not(target_os = "macos")))] +use self::unix as imp; #[cfg(windows)] -#[path = "windows.rs"] -mod imp; +pub mod windows; +#[cfg(windows)] +use self::windows as imp; + + error_chain!{ errors { |
