summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--talpid-core/Cargo.toml2
-rw-r--r--talpid-core/src/firewall/macos/dns.rs269
-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.rs17
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 {