diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2017-12-04 11:15:38 +0100 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2017-12-04 11:15:38 +0100 |
| commit | 26d9f85b9c779ca18f935df9006ca6fad037aeb7 (patch) | |
| tree | 421e8a350ec907bb7dbb0c4ddf31b06a9e21d238 | |
| parent | 08530bb297f5f1faff0ca5f6b68d33af03b217ea (diff) | |
| parent | 98ad6988a8ad239af6ce6639269801d125156088 (diff) | |
| download | mullvadvpn-26d9f85b9c779ca18f935df9006ca6fad037aeb7.tar.xz mullvadvpn-26d9f85b9c779ca18f935df9006ca6fad037aeb7.zip | |
Merge branch 'set-dns-on-macos'
| -rw-r--r-- | CHANGELOG.md | 5 | ||||
| -rw-r--r-- | Cargo.lock | 55 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rwxr-xr-x | format.sh | 3 | ||||
| -rw-r--r-- | mullvad-daemon/src/main.rs | 4 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 4 | ||||
| -rw-r--r-- | talpid-core/src/firewall/macos/dns.rs | 316 | ||||
| -rw-r--r-- | talpid-core/src/firewall/macos/mod.rs (renamed from talpid-core/src/firewall/macos.rs) | 165 | ||||
| -rw-r--r-- | talpid-core/src/firewall/mod.rs | 20 | ||||
| -rw-r--r-- | talpid-core/src/firewall/unix.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/firewall/windows.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 3 |
12 files changed, 446 insertions, 136 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index df5110dcfb..5b7d167dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Changed - Renamed daemon binary from `mullvadd` to `mullvad-daemon`. +### Security +- DNS leak found when using redirect firewall rules and a custom DNS forwarder. Replaced all of that + with strict DNS blocking firewall rules and SystemConfiguration integration where DNS settings are + injected to the operating system settings and constantly monitored for external changes. + ## [2017.1-beta6] - 2017-11-09 ### Added diff --git a/Cargo.lock b/Cargo.lock index bb9982afc7..8dd38c5b99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,15 @@ dependencies = [ ] [[package]] +name = "core-foundation" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "core-foundation-sys" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -199,6 +208,14 @@ dependencies = [ ] [[package]] +name = "core-foundation-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "crypt32-sys" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1142,18 +1159,6 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "socket-relay" -version = "0.1.0" -dependencies = [ - "env_logger 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "stable_deref_trait" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1182,6 +1187,25 @@ dependencies = [ ] [[package]] +name = "system-configuration" +version = "0.1.0" +source = "git+https://github.com/mullvad/system-configuration-rs#cfda5c348acbbc9ca9c9f621c29d2661604b4af9" +dependencies = [ + "core-foundation 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "system-configuration-sys 0.1.0 (git+https://github.com/mullvad/system-configuration-rs)", +] + +[[package]] +name = "system-configuration-sys" +version = "0.1.0" +source = "git+https://github.com/mullvad/system-configuration-rs#cfda5c348acbbc9ca9c9f621c29d2661604b4af9" +dependencies = [ + "bindgen 0.31.3 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "take" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1190,6 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "talpid-core" version = "0.1.0" dependencies = [ + "core-foundation 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "duct 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 7.1.1 (git+https://github.com/paritytech/jsonrpc?tag=v7.1.1)", @@ -1200,7 +1225,7 @@ dependencies = [ "openvpn-plugin 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "pfctl 0.1.0 (git+https://github.com/mullvad/pfctl-rs?rev=dae436f6ee4e3529fc995c5192b314f1cc8dccec)", "shell-escape 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "socket-relay 0.1.0", + "system-configuration 0.1.0 (git+https://github.com/mullvad/system-configuration-rs)", "talpid-ipc 0.1.0", "talpid-types 0.1.0", "tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1507,7 +1532,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum clap 2.27.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b8c532887f1a292d17de05ae858a8fe50a301e196f9ef0ddb7ccd0d1d00f180" "checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" "checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" +"checksum core-foundation 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8047f547cd6856d45b1cdd75ef8d2f21f3d0e4bf1dab0a0041b0ae9a5dda9c0e" "checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d" +"checksum core-foundation-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "152195421a2e6497a8179195672e9d4ee8e45ed8c465b626f1606d27a08ebcd5" "checksum crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e34988f7e069e0b2f3bfc064295161e489b2d4e04a2e4248fb94360cdf00b4ec" "checksum ctrlc 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "df391ea008fca636e41e40863a0b39a850e2ab26b0cdeed0c3657fd05a66d44c" "checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" @@ -1614,6 +1641,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum system-configuration 0.1.0 (git+https://github.com/mullvad/system-configuration-rs)" = "<none>" +"checksum system-configuration-sys 0.1.0 (git+https://github.com/mullvad/system-configuration-rs)" = "<none>" "checksum take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" diff --git a/Cargo.toml b/Cargo.toml index 0082f61236..1bed44a013 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,4 @@ members = [ "talpid-openvpn-plugin", "talpid-core", "talpid-ipc", - "socket-relay", ] @@ -5,7 +5,7 @@ set -u -VERSION="0.2.15" +VERSION="0.2.16" CMD="rustfmt" INSTALL_CMD="cargo install --vers $VERSION --force rustfmt-nightly" @@ -14,6 +14,7 @@ function correct_rustfmt() { echo "$CMD is not installed" >&2 return 1 fi + export DYLD_LIBRARY_PATH=$(rustc --print sysroot)/lib local installed_version=$($CMD --version | cut -d'-' -f1) if [[ "$installed_version" != "$VERSION" ]]; then echo "Wrong version of $CMD installed. Expected $VERSION, got $installed_version" >&2 diff --git a/mullvad-daemon/src/main.rs b/mullvad-daemon/src/main.rs index 2adcb0a9b7..c1c7aa4980 100644 --- a/mullvad-daemon/src/main.rs +++ b/mullvad-daemon/src/main.rs @@ -562,8 +562,8 @@ impl Daemon { Ok(()) } (TargetState::Unsecured, TunnelState::NotRunning) => self.reset_security_policy(), - (TargetState::Unsecured, TunnelState::Connecting) | - (TargetState::Unsecured, TunnelState::Connected) => self.kill_tunnel(), + (TargetState::Unsecured, TunnelState::Connecting) + | (TargetState::Unsecured, TunnelState::Connected) => self.kill_tunnel(), (..) => Ok(()), } } diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index a7dfc01aa9..32e36c9ed2 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -24,6 +24,6 @@ libc = "0.2.20" [target.'cfg(target_os = "macos")'.dependencies] pfctl = { git = "https://github.com/mullvad/pfctl-rs", rev = "dae436f6ee4e3529fc995c5192b314f1cc8dccec" } -socket-relay = { path = "../socket-relay" } +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..abf12353ab --- /dev/null +++ b/talpid-core/src/firewall/macos/dns.rs @@ -0,0 +1,316 @@ +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 STATE_PATH_PATTERN: &str = "State:/Network/Service/.*/DNS"; +const SETUP_PATH_PATTERN: &str = "Setup:/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(STATE_PATH_PATTERN), + CFString::new(SETUP_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>>>, +) { + if let Err(e) = dns_change_callback_internal(store, changed_keys, state) { + error!("{}", e.display_chain()); + } +} + +fn dns_change_callback_internal( + store: SCDynamicStore, + changed_keys: CFArray<CFString>, + state: &mut Arc<Mutex<Option<State>>>, +) -> Result<()> { + 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 { + set_dns(&store, path.clone(), &state.desired_dns) + .chain_err(|| format!("Failed changing DNS for {}", path))?; + // If we changed a state DNS, also set the corresponding setup DNS. + if let Some(setup_path_str) = state_to_setup_path(&path.to_string()) { + let setup_path = CFString::new(&setup_path_str); + if !state.backup.contains_key(&setup_path_str) { + state + .backup + .insert(setup_path_str, read_dns(&store, setup_path.clone())); + } + set_dns(&store, setup_path.clone(), &state.desired_dns) + .chain_err(|| format!("Failed changing DNS for {}", setup_path))?; + } + } + }, + } + Ok(()) +} + +/// 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(); + // Backup all "state" DNS, and all corresponding "setup" DNS even if they don't exist + if let Some(paths) = store.get_keys(STATE_PATH_PATTERN) { + for path_ptr in paths.as_untyped().iter() { + let state_path = unsafe { CFString::wrap_under_get_rule(path_ptr as CFStringRef) }; + let state_path_str = state_path.to_string(); + let setup_path_str = state_to_setup_path(&state_path_str).unwrap(); + let setup_path = CFString::new(&setup_path_str); + backup.insert(state_path_str, read_dns(store, state_path)); + backup.insert(setup_path_str, read_dns(store, setup_path)); + } + } + // Backup all "setup" DNS not already covered + if let Some(paths) = store.get_keys(SETUP_PATH_PATTERN) { + for path_ptr in paths.as_untyped().iter() { + let setup_path = unsafe { CFString::wrap_under_get_rule(path_ptr as CFStringRef) }; + let setup_path_str = setup_path.to_string(); + if !backup.contains_key(&setup_path_str) { + backup.insert(setup_path_str, read_dns(store, setup_path)); + } + } + } + backup +} + +fn state_to_setup_path(state_path: &str) -> Option<String> { + if state_path.starts_with("State:/") { + Some(state_path.replacen("State:/", "Setup:/", 1)) + } else { + None + } +} + +/// 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 fdef40ed9e..9065a607d6 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos/mod.rs @@ -1,27 +1,34 @@ -extern crate socket_relay; +extern crate pfctl; extern crate tokio_core; use super::{Firewall, SecurityPolicy}; -use pfctl; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::mpsc; -use std::thread; +use std::net::Ipv4Addr; -use self::socket_relay::udp::{Relay, RelayCloseHandle}; use talpid_types::net; -use tunnel::TunnelMetadata; -// alias used to instantiate firewall implementation +mod dns; + +use self::dns::DnsMonitor; + +error_chain! { + links { + PfCtl(self::pfctl::Error, self::pfctl::ErrorKind) #[doc = "PF error"]; + DnsMonitor(self::dns::Error, self::dns::ErrorKind) #[doc = "DNS error"]; + } +} + +/// alias used to instantiate firewall implementation pub type ConcreteFirewall = PacketFilter; -pub use pfctl::{Error, ErrorKind, Result, ResultExt}; const ANCHOR_NAME: &'static str = "mullvad"; +/// The macOS firewall implementation. Acting as converter between the `Firewall` trait API +/// and actual PF firewall rules and other protective measures to keep the `SecurityPolicy`. pub struct PacketFilter { pf: pfctl::PfCtl, pf_was_enabled: Option<bool>, - dns_proxy_close_handle: Option<RelayCloseHandle>, + dns_monitor: DnsMonitor, } impl Firewall<Error> for PacketFilter { @@ -29,7 +36,7 @@ impl Firewall<Error> for PacketFilter { Ok(PacketFilter { pf: pfctl::PfCtl::new()?, pf_was_enabled: None, - dns_proxy_close_handle: None, + dns_monitor: DnsMonitor::new()?, }) } @@ -40,11 +47,11 @@ impl Firewall<Error> for PacketFilter { } fn reset_policy(&mut self) -> Result<()> { - self.stop_dns_proxy(); vec![ self.remove_rules(), self.remove_anchor(), self.restore_state(), + self.restore_dns(), ].into_iter() .collect::<Result<Vec<_>>>() .map(|_| ()) @@ -54,15 +61,12 @@ impl Firewall<Error> for PacketFilter { impl PacketFilter { fn set_rules(&mut self, policy: SecurityPolicy) -> Result<()> { let mut new_filter_rules = vec![]; - let mut new_redirect_rules = vec![]; new_filter_rules.append(&mut Self::get_allow_loopback_rules()?); new_filter_rules.append(&mut Self::get_allow_dhcp_rules()?); - let (mut policy_filter_rules, mut policy_redirect_rules) = - self.get_policy_specific_rules(policy)?; + let mut policy_filter_rules = self.get_policy_specific_rules(policy)?; new_filter_rules.append(&mut policy_filter_rules); - new_redirect_rules.append(&mut policy_redirect_rules); let drop_all_rule = pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Drop) @@ -72,64 +76,59 @@ 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( &mut self, policy: SecurityPolicy, - ) -> Result<(Vec<pfctl::FilterRule>, Vec<pfctl::RedirectRule>)> { + ) -> Result<Vec<pfctl::FilterRule>> { match policy { SecurityPolicy::Connecting(relay_endpoint) => { - self.stop_dns_proxy(); - Ok((vec![Self::get_allow_relay_rule(relay_endpoint)?], vec![])) + Ok(vec![Self::get_allow_relay_rule(relay_endpoint)?]) } SecurityPolicy::Connected(relay_endpoint, tunnel) => { - let dns_proxy_listen_addr = self.start_dns_proxy(&tunnel)?; + self.dns_monitor.set_dns(vec![tunnel.gateway.to_string()])?; - let allow_dns_to_relay_rule = pfctl::FilterRuleBuilder::default() + let allow_tcp_dns_to_relay_rule = pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Pass) .direction(pfctl::Direction::Out) .quick(true) .interface(&tunnel.interface) - .proto(pfctl::Proto::Udp) + .proto(pfctl::Proto::Tcp) .to(pfctl::Endpoint::new(tunnel.gateway, 53)) .build()?; - let reroute_dns_rule = pfctl::FilterRuleBuilder::default() + let allow_udp_dns_to_relay_rule = pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Pass) .direction(pfctl::Direction::Out) .quick(true) - .route(pfctl::Route::route_to(pfctl::Interface::from("lo0"))) + .interface(&tunnel.interface) .proto(pfctl::Proto::Udp) - .to(pfctl::Port::from(53)) + .to(pfctl::Endpoint::new(tunnel.gateway, 53)) .build()?; - let block_all_other_dns_rule = pfctl::FilterRuleBuilder::default() + let block_tcp_dns_rule = pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Drop) .direction(pfctl::Direction::Out) .quick(true) .proto(pfctl::Proto::Tcp) .to(pfctl::Port::from(53)) .build()?; - - let dns_redirect_rule = pfctl::RedirectRuleBuilder::default() - .action(pfctl::RedirectRuleAction::Redirect) - .interface("lo0") + let block_udp_dns_rule = pfctl::FilterRuleBuilder::default() + .action(pfctl::FilterRuleAction::Drop) + .direction(pfctl::Direction::Out) + .quick(true) .proto(pfctl::Proto::Udp) .to(pfctl::Port::from(53)) - .redirect_to(dns_proxy_listen_addr) .build()?; - Ok(( - vec![ - allow_dns_to_relay_rule, - reroute_dns_rule, - block_all_other_dns_rule, - Self::get_allow_relay_rule(relay_endpoint)?, - Self::get_allow_tunnel_rule(tunnel.interface.as_str())?, - ], - vec![dns_redirect_rule], - )) + Ok(vec![ + allow_tcp_dns_to_relay_rule, + allow_udp_dns_to_relay_rule, + block_tcp_dns_rule, + block_udp_dns_rule, + Self::get_allow_relay_rule(relay_endpoint)?, + Self::get_allow_tunnel_rule(tunnel.interface.as_str())?, + ]) } } } @@ -137,7 +136,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) @@ -145,17 +144,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>> { @@ -200,49 +199,43 @@ 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(()), } } + fn restore_dns(&self) -> Result<()> { + Ok(self.dns_monitor.reset()?) + } + fn add_anchor(&mut self) -> Result<()> { 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) - } - - fn start_dns_proxy(&mut self, tunnel: &TunnelMetadata) -> Result<SocketAddr> { - self.stop_dns_proxy(); - let (listen_addr, close_handle) = spawn_dns_proxy(tunnel.ip, tunnel.gateway)?; - self.dns_proxy_close_handle = Some(close_handle); - Ok(listen_addr) - } - - fn stop_dns_proxy(&mut self) { - if let Some(close_handle) = self.dns_proxy_close_handle.take() { - close_handle.close(); - } + .try_remove_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect)?; + Ok(()) } } @@ -252,43 +245,3 @@ fn as_pfctl_proto(protocol: net::TransportProtocol) -> pfctl::Proto { net::TransportProtocol::Tcp => pfctl::Proto::Tcp, } } - -fn spawn_dns_proxy( - tunnel_ip: Ipv4Addr, - tunnel_gateway: Ipv4Addr, -) -> Result<(SocketAddr, RelayCloseHandle)> { - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - match spawn_dns_proxy_helper(tunnel_ip, tunnel_gateway) { - Ok((mut core, relay)) => { - tx.send(Ok((relay.listen_addr(), relay.close_handle()))) - .unwrap(); - match core.run(relay) { - Err(e) => error!("DNS proxy died with an error: {}", e), - Ok(_) => info!("DNS proxy exiting"), - } - } - Err(e) => { - tx.send(Err(e)).unwrap(); - } - } - }); - rx.recv().unwrap() -} - -fn spawn_dns_proxy_helper( - tunnel_ip: Ipv4Addr, - tunnel_gateway: Ipv4Addr, -) -> Result<(tokio_core::reactor::Core, Relay)> { - let core = tokio_core::reactor::Core::new().chain_err(|| "Unable to init Tokio event loop")?; - - let relay = Relay::new( - "127.0.0.1:0".parse().unwrap(), - IpAddr::V4(tunnel_ip), - SocketAddr::from((tunnel_gateway, 53)), - core.handle(), - ).chain_err(|| "Unable to create DNS proxy socket relay")?; - info!("DNS proxy listening on {}", relay.listen_addr()); - - Ok((core, relay)) -} diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 6ac63e2493..17ba65b88d 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -1,16 +1,24 @@ use talpid_types::net::Endpoint; +/// macOS implementation of the firewall/security policy enforcer. #[cfg(target_os = "macos")] -#[path = "macos.rs"] -mod imp; +pub mod macos; +#[cfg(target_os = "macos")] +use self::macos as imp; +/// Linux implementation of the firewall/security policy enforcer. +#[cfg(all(unix, not(target_os = "macos")))] +pub mod unix; #[cfg(all(unix, not(target_os = "macos")))] -#[path = "unix.rs"] -mod imp; +use self::unix as imp; +/// Windows implementation of the firewall/security policy enforcer. +#[cfg(windows)] +pub mod windows; #[cfg(windows)] -#[path = "windows.rs"] -mod imp; +use self::windows as imp; + + error_chain!{ errors { diff --git a/talpid-core/src/firewall/unix.rs b/talpid-core/src/firewall/unix.rs index 7550c3c051..b39013e3c0 100644 --- a/talpid-core/src/firewall/unix.rs +++ b/talpid-core/src/firewall/unix.rs @@ -1,10 +1,11 @@ use super::{Firewall, SecurityPolicy}; -// alias used to instantiate firewall implementation +/// alias used to instantiate firewall implementation pub type ConcreteFirewall = Netfilter; error_chain!{} +/// The Linux implementation for the `Firewall` trait. pub struct Netfilter; impl Firewall<Error> for Netfilter { fn new() -> Result<Self> { diff --git a/talpid-core/src/firewall/windows.rs b/talpid-core/src/firewall/windows.rs index 3405ba12c0..bae506fdc5 100644 --- a/talpid-core/src/firewall/windows.rs +++ b/talpid-core/src/firewall/windows.rs @@ -1,10 +1,11 @@ use super::{Firewall, SecurityPolicy}; -// alias used to instantiate firewall implementation +/// alias used to instantiate firewall implementation pub type ConcreteFirewall = WindowsFirewall; error_chain!{} +/// The Windows implementation for the `Firewall` trait. pub struct WindowsFirewall; impl Firewall<Error> for WindowsFirewall { fn new() -> Result<Self> { diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 1af5587def..fc85584267 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -29,9 +29,6 @@ extern crate openvpn_plugin; extern crate talpid_ipc; extern crate talpid_types; -#[cfg(target_os = "macos")] -extern crate pfctl; - /// Working with processes. pub mod process; |
