diff options
| author | Emīls <emils@mullvad.net> | 2020-11-17 13:47:34 +0000 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2020-11-19 11:53:23 +0000 |
| commit | 5d513330ef9f1679ec228cbceef72a2f98ca1193 (patch) | |
| tree | da6c243396a78532b98fb267bb11636baf55b078 | |
| parent | ab29a68c768a0d1dcaafe622cc7b5585700b9845 (diff) | |
| download | mullvadvpn-5d513330ef9f1679ec228cbceef72a2f98ca1193.tar.xz mullvadvpn-5d513330ef9f1679ec228cbceef72a2f98ca1193.zip | |
Use refactored NM code for managing DNS
| -rw-r--r-- | talpid-core/src/dns/linux/mod.rs | 9 | ||||
| -rw-r--r-- | talpid-core/src/dns/linux/network_manager.rs | 493 |
2 files changed, 17 insertions, 485 deletions
diff --git a/talpid-core/src/dns/linux/mod.rs b/talpid-core/src/dns/linux/mod.rs index 48933269be..9eef807b71 100644 --- a/talpid-core/src/dns/linux/mod.rs +++ b/talpid-core/src/dns/linux/mod.rs @@ -7,7 +7,6 @@ use self::{ network_manager::NetworkManager, resolvconf::Resolvconf, static_resolv_conf::StaticResolvConf, systemd_resolved::SystemdResolved, }; -use crate::linux::network_manager::NetworkManager as NMDBus; use std::{env, fmt, net::IpAddr, path::Path}; @@ -65,14 +64,6 @@ impl super::DnsMonitorT for DnsMonitor { } Ok(()) } - - fn dbus_connection(&self) -> Option<&NMDBus> { - match &self.inner { - Some(DnsMonitorHolder::NetworkManager(nm)) => Some(&nm.connection), - // Some(DnsMonitorHolder::SystemdResolved(sdr)) => Some(&sdr.dbus_connection), - _ => None, - } - } } pub enum DnsMonitorHolder { diff --git a/talpid-core/src/dns/linux/network_manager.rs b/talpid-core/src/dns/linux/network_manager.rs index d127f22085..1112305468 100644 --- a/talpid-core/src/dns/linux/network_manager.rs +++ b/talpid-core/src/dns/linux/network_manager.rs @@ -1,272 +1,34 @@ -use dbus::{ - arg::{Append, RefArg, Variant}, - ffidisp::{stdintf::*, BusType, ConnPath, Connection}, - message::Message, - strings::Member, -}; -use lazy_static::lazy_static; -use std::{ - collections::HashMap, - fs::File, - io::{BufRead, BufReader}, - net::IpAddr, - path::Path, - time::{Duration, Instant}, -}; +pub use crate::linux::network_manager::Error; +use crate::linux::network_manager::{self, DeviceConfig, NetworkManager as DBus}; +use std::net::IpAddr; pub type Result<T> = std::result::Result<T, Error>; -#[derive(err_derive::Error, Debug)] -#[error(no_from)] -pub enum Error { - #[error(display = "NetworkManager not detected")] - NetworkManagerNotDetected(#[error(source)] dbus::Error), - - #[error(display = "NetworkManager is too old")] - TooOldNetworkManager(#[error(source)] dbus::Error), - - #[error(display = "NetworkManager is not managing DNS")] - NetworkManagerNotManagingDns, - - #[error(display = "Error while communicating over Dbus")] - Dbus(#[error(source)] dbus::Error), - - #[error(display = "Failed to construct DBus method call message")] - DbusMethodCall(String), - - #[error(display = "Failed to construct DBus member")] - DbusMemberConstruct(String), - - #[error(display = "Failed to match the returned D-Bus object with expected type")] - MatchDBusTypeError(#[error(source)] dbus::arg::TypeMismatchError), - - #[error(display = "DNS is managed by systemd-resolved - NM can't enforce DNS globally")] - SystemdResolved, - - #[error(display = "Failed to find obtain devices from network manager")] - ObtainDevices, - - #[error(display = "Failed to find link interface in network manager")] - LinkNotFound, - - #[error(display = "Device inactive: {}", _0)] - DeviceNotReady(u32), -} - -const NM_BUS: &str = "org.freedesktop.NetworkManager"; -const NM_TOP_OBJECT: &str = "org.freedesktop.NetworkManager"; -const NM_DNS_MANAGER: &str = "org.freedesktop.NetworkManager.DnsManager"; -const NM_DNS_MANAGER_PATH: &str = "/org/freedesktop/NetworkManager/DnsManager"; -const NM_OBJECT_PATH: &str = "/org/freedesktop/NetworkManager"; -const NM_DEVICE: &str = "org.freedesktop.NetworkManager.Device"; -const NM_IP4_CONFIG: &str = "org.freedesktop.NetworkManager.IP4Config"; -const NM_IP6_CONFIG: &str = "org.freedesktop.NetworkManager.IP6Config"; -const RPC_TIMEOUT_MS: i32 = 3000; -const DEVICE_READY_TIMEOUT: Duration = Duration::from_secs(15); -const GLOBAL_DNS_CONF_KEY: &str = "GlobalDnsConfiguration"; -const RC_MANAGEMENT_MODE_KEY: &str = "RcManager"; -const DNS_MODE_KEY: &str = "Mode"; -const DNS_FIRST_PRIORITY: i32 = -2147483647; - -const NM_DEVICE_STATE_IP_CHECK: u32 = 80; -const NM_DEVICE_STATE_SECONDARY: u32 = 90; -const NM_DEVICE_STATE_ACTIVATED: u32 = 100; - -lazy_static! { - static ref NM_DEVICE_STATE_CHANGED: Member<'static> = Member::new("StateChanged").unwrap(); -} pub struct NetworkManager { - pub dbus_connection: Connection, + pub connection: DBus, device: Option<String>, - settings_backup: Option<HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>>, + settings_backup: Option<DeviceConfig>, } impl NetworkManager { pub fn new() -> Result<Self> { - let dbus_connection = Connection::get_private(BusType::System).map_err(Error::Dbus)?; + let connection = DBus::new()?; + connection.ensure_resolv_conf_is_managed()?; + connection.ensure_network_manager_exists()?; let manager = NetworkManager { - dbus_connection, + connection, device: None, settings_backup: None, }; - manager.ensure_resolv_conf_is_managed()?; - manager.ensure_network_manager_exists()?; Ok(manager) } - fn ensure_network_manager_exists(&self) -> Result<()> { - let _: Box<dyn RefArg> = self - .as_manager() - .get(&NM_TOP_OBJECT, GLOBAL_DNS_CONF_KEY) - .map_err(Error::NetworkManagerNotDetected)?; - Ok(()) - } - - fn ensure_resolv_conf_is_managed(&self) -> Result<()> { - // check if NM is set to manage resolv.conf - let management_mode: String = self - .dbus_connection - .with_path(NM_BUS, NM_DNS_MANAGER_PATH, RPC_TIMEOUT_MS) - .get(NM_DNS_MANAGER, RC_MANAGEMENT_MODE_KEY) - .map_err(Error::TooOldNetworkManager)?; - if management_mode == "unmanaged" { - return Err(Error::NetworkManagerNotManagingDns); - } - - let dns_mode: String = self - .dbus_connection - .with_path(NM_BUS, NM_DNS_MANAGER_PATH, RPC_TIMEOUT_MS) - .get(NM_DNS_MANAGER, DNS_MODE_KEY) - .map_err(Error::Dbus)?; - - match dns_mode.as_ref() { - // If NetworkManager isn't managing DNS for us, it's useless. - "none" => return Err(Error::NetworkManagerNotManagingDns), - _ => (), - }; - - if !verify_etc_resolv_conf_contents() { - log::debug!("/etc/resolv.conf differs from reference resolv.conf, therefore NM is not managing DNS"); - return Err(Error::NetworkManagerNotManagingDns); - } - - Ok(()) - } - - fn as_manager(&self) -> ConnPath<'_, &Connection> { - self.dbus_connection - .with_path(NM_BUS, NM_OBJECT_PATH, RPC_TIMEOUT_MS) - } - pub fn set_dns(&mut self, interface_name: &str, servers: &[IpAddr]) -> Result<()> { - let device = self.fetch_device(interface_name)?; - self.wait_until_device_is_ready(&device)?; - - // Get the last applied connection - - let get_applied_connection = - Message::new_method_call(NM_BUS, &device, NM_DEVICE, "GetAppliedConnection") - .map_err(Error::DbusMethodCall)? - .append1(0u32); - let applied_connection = self - .dbus_connection - .send_with_reply_and_block(get_applied_connection, RPC_TIMEOUT_MS) - .map_err(Error::Dbus)?; - - let (mut settings, version_id): ( - HashMap<&str, HashMap<&str, Variant<Box<dyn RefArg>>>>, - u64, - ) = applied_connection - .read2() - .map_err(Error::MatchDBusTypeError)?; - - // Keep changed routes. - // These routes were modified outside NM, likely by RouteManager. - - if let Some(ipv4_settings) = settings.get_mut("ipv4") { - let device_ip4_config: dbus::Path<'_> = self - .dbus_connection - .with_path(NM_BUS, &device, RPC_TIMEOUT_MS) - .get(NM_DEVICE, "Ip4Config") - .map_err(Error::Dbus)?; - - let device_route_data: Vec<HashMap<String, Variant<Box<dyn RefArg>>>> = self - .dbus_connection - .with_path(NM_BUS, &device_ip4_config, RPC_TIMEOUT_MS) - .get(NM_IP4_CONFIG, "RouteData") - .map_err(Error::Dbus)?; - - ipv4_settings.insert("route-metric", Variant(Box::new(0u32))); - ipv4_settings.insert("route-data", Variant(Box::new(device_route_data))); - ipv4_settings.remove("routes"); - } - - if let Some(ipv6_settings) = settings.get_mut("ipv6") { - let device_ip6_config: dbus::Path<'_> = self - .dbus_connection - .with_path(NM_BUS, &device, RPC_TIMEOUT_MS) - .get(NM_DEVICE, "Ip6Config") - .map_err(Error::Dbus)?; - - let device_addresses6: Vec<(Vec<u8>, u32, Vec<u8>)> = self - .dbus_connection - .with_path(NM_BUS, &device_ip6_config, RPC_TIMEOUT_MS) - .get(NM_IP6_CONFIG, "Addresses") - .map_err(Error::Dbus)?; - let device_route6_data: Vec<HashMap<String, Variant<Box<dyn RefArg>>>> = self - .dbus_connection - .with_path(NM_BUS, &device_ip6_config, RPC_TIMEOUT_MS) - .get(NM_IP6_CONFIG, "RouteData") - .map_err(Error::Dbus)?; - - ipv6_settings.insert("route-metric", Variant(Box::new(0u32))); - ipv6_settings.remove("routes"); - ipv6_settings.insert("route-data", Variant(Box::new(device_route6_data))); - // if the link contains link local addresses, addresses shouldn't be reset - if ipv6_settings - .get("method") - .map(|method| { - // if IPv6 isn't enabled, IPv6 method will be set to "ignore", in which case we - // shouldn't reapply any config for ipv6 - method.as_str() != Some("link-local") && method.as_str() != Some("ignore") - }) - .unwrap_or(true) - { - ipv6_settings.insert("addresses", Variant(Box::new(device_addresses6))); - } - } - - let mut settings_backup = - HashMap::<String, HashMap<String, Variant<Box<dyn RefArg>>>>::new(); - for (top_key, map) in settings.iter() { - let mut inner_dict = HashMap::<String, Variant<Box<dyn RefArg>>>::new(); - for (key, variant) in map.iter() { - inner_dict.insert(key.to_string(), Variant(variant.0.box_clone())); - } - settings_backup.insert(top_key.to_string(), inner_dict); - } - - // Update the DNS config - - let v4_dns: Vec<u32> = servers - .iter() - .filter_map(|server| { - match server { - // Network-byte order - IpAddr::V4(server) => Some(u32::to_be(server.clone().into())), - IpAddr::V6(_) => None, - } - }) - .collect(); - if !v4_dns.is_empty() { - Self::update_dns_config(&mut settings, "ipv4", v4_dns); - } - - let v6_dns: Vec<Vec<u8>> = servers - .iter() - .filter_map(|server| match server { - IpAddr::V4(_) => None, - IpAddr::V6(server) => Some(server.octets().to_vec()), - }) - .collect(); - if !v6_dns.is_empty() { - Self::update_dns_config(&mut settings, "ipv6", v6_dns); - } - - if let Some(wg_config) = settings.get_mut("wireguard") { - wg_config.insert( - "fwmark", - Variant(Box::new(crate::linux::TUNNEL_FW_MARK) as Box<dyn RefArg>), - ); - } - - self.reapply_settings(&device, settings, version_id)?; - + let old_settings = self.connection.set_dns(interface_name, servers)?; + self.settings_backup = Some(old_settings); self.device = Some(interface_name.to_string()); - self.settings_backup = Some(settings_backup); - Ok(()) } @@ -276,240 +38,19 @@ impl NetworkManager { Some(device) => device, None => return Ok(()), }; - let device = match self.fetch_device(&device) { - Ok(device) => device, - Err(Error::LinkNotFound) => return Ok(()), + let device_path = match self.connection.fetch_device(&device) { + Ok(device_path) => device_path, + Err(Error::DeviceNotFound) => return Ok(()), Err(error) => return Err(error), }; - if device_is_ready(self.get_device_state(&device)?) { - self.reapply_settings(&device, settings_backup, 0u64)?; + if network_manager::device_is_ready(self.connection.get_device_state(&device_path)?) { + self.connection + .reapply_settings(&device_path, settings_backup, 0u64)?; } return Ok(()); } log::trace!("No DNS settings to reset"); Ok(()) } - - fn get_device_state(&self, device: &dbus::Path<'_>) -> Result<u32> { - self.dbus_connection - .with_path(NM_BUS, device, RPC_TIMEOUT_MS) - .get(NM_DEVICE, "State") - .map_err(Error::Dbus) - } - - fn reapply_settings<Settings: Append>( - &self, - device: &dbus::Path<'_>, - settings: Settings, - version_id: u64, - ) -> Result<()> { - let reapply = Message::new_method_call(NM_BUS, device, NM_DEVICE, "Reapply") - .map_err(Error::DbusMethodCall)? - .append3(settings, version_id, 0u32); - self.dbus_connection - .send_with_reply_and_block(reapply, RPC_TIMEOUT_MS) - .map_err(Error::Dbus)?; - Ok(()) - } - - fn update_dns_config<'a, T>( - settings: &mut HashMap<&str, HashMap<&str, Variant<Box<dyn RefArg + 'a>>>>, - ip_protocol: &'static str, - servers: T, - ) where - T: RefArg + 'a, - { - let settings = match settings.get_mut(ip_protocol) { - Some(ip_protocol) => ip_protocol, - None => { - settings.insert(ip_protocol, HashMap::new()); - settings.get_mut(ip_protocol).unwrap() - } - }; - - settings.insert("method", Variant(Box::new("manual".to_string()))); - settings.insert("dns-priority", Variant(Box::new(DNS_FIRST_PRIORITY))); - settings.insert("dns", Variant(Box::new(servers))); - settings.insert("dns-search", Variant(Box::new(vec!["~.".to_string()]))); - } - - fn fetch_device(&self, interface_name: &str) -> Result<dbus::Path<'static>> { - let devices: Box<dyn RefArg> = self - .as_manager() - .get(NM_TOP_OBJECT, "Devices") - .map_err(Error::Dbus)?; - let mut iter = devices.as_iter().ok_or(Error::ObtainDevices)?; - - while let Some(device) = iter.next() { - // Copy due to lifetime weirdness - let device = device.box_clone(); - let device = device - .as_any() - .downcast_ref::<dbus::Path<'_>>() - .ok_or(Error::ObtainDevices)?; - - let device_name: String = self - .dbus_connection - .with_path(NM_BUS, device, RPC_TIMEOUT_MS) - .get(NM_DEVICE, "Interface") - .map_err(Error::Dbus)?; - - if device_name != interface_name { - continue; - } - - return Ok(device.clone()); - } - Err(Error::LinkNotFound) - } - - fn wait_until_device_is_ready(&self, device: &dbus::Path<'_>) -> Result<()> { - let mut device_state = self.get_device_state(device)?; - - if !device_is_ready(device_state) { - let deadline = Instant::now() + DEVICE_READY_TIMEOUT; - let match_rule = &format!( - "destination='{}',path='{}',interface='{}',member='{}'", - NM_BUS, - device, - NM_DEVICE, - NM_DEVICE_STATE_CHANGED.to_string() - ); - self.dbus_connection - .add_match(match_rule) - .map_err(Error::Dbus)?; - - // a separate loopis used here because `connection.incoming(TIMEOUT)` will sleep - // for TIMEOUT after the last message was received - if the device is thrashing - // between states, we should probably give up rather than block indefinitely. - while Instant::now() < deadline && !device_is_ready(device_state) { - for message in self.dbus_connection.incoming(RPC_TIMEOUT_MS as u32) { - if message.member().as_ref() != Some(&NM_DEVICE_STATE_CHANGED) { - continue; - } - let (new_state, _old_state, _reason): (u32, u32, u32) = message - .read3() - .map_err(Error::MatchDBusTypeError) - .map_err(|error| { - let _ = self.dbus_connection.remove_match(match_rule); - error - })?; - - device_state = new_state; - log::trace!("New tunnel device state: {}", device_state); - if device_is_ready(device_state) { - break; - } - } - } - - if let Err(error) = self.dbus_connection.remove_match(match_rule) { - log::warn!("Failed to remove signal listener: {}", error); - } - if !device_is_ready(device_state) { - return Err(Error::DeviceNotReady(device_state)); - } - } - Ok(()) - } -} - -/// Given a DBus connection, verify that NM is up and running and capable of managing DNS via -/// systemd-resolved. This includes verifying that NM is managing DNS via systemd-resolved and is -/// controlling /etc/resolv.conf -pub fn is_nm_managing_via_resolved(connection: &Connection) -> bool { - let check_nm = || -> Result<bool> { - let dns_mode: String = connection - .with_path(NM_BUS, NM_DNS_MANAGER_PATH, RPC_TIMEOUT_MS) - .get(NM_DNS_MANAGER, DNS_MODE_KEY) - .map_err(Error::Dbus)?; - if &dns_mode != "systemd-resolved" { - return Ok(false); - } - - - let rc_management_mode: String = connection - .with_path(NM_BUS, NM_DNS_MANAGER_PATH, RPC_TIMEOUT_MS) - .get(NM_DNS_MANAGER, RC_MANAGEMENT_MODE_KEY) - .map_err(Error::TooOldNetworkManager)?; - - match rc_management_mode.as_str() { - // /etc/resolv.conf is managed via executing a command, can't verify that works, have - // to assume it does - "resolvconf" | "netconfig" => Ok(true), - - "symlink" | "none" | "file" => { - let reload = - Message::new_method_call(NM_BUS, NM_OBJECT_PATH, NM_TOP_OBJECT, "Reload") - .map_err(Error::DbusMethodCall)? - .append1(0x02u32); - let _ = connection - .send_with_reply_and_block(reload, RPC_TIMEOUT_MS) - .map_err(Error::Dbus)?; - let result = verify_etc_resolv_conf_contents(); - Ok(result) - } - - // NM doesn't manage DNS at all - "unmanaged" => Ok(false), - unknown_rc_mode => { - log::error!("Unknown resolvconf management mode - {}", unknown_rc_mode); - Ok(false) - } - } - }; - match check_nm() { - Ok(result) => result, - Err(err) => { - log::error!( - "Failed to check if NM is managing DNS via systemd-resolved: {}", - err - ); - false - } - } -} - -// Verify that the contents of /etc/resolv.conf match what NM expectes them to be. -fn verify_etc_resolv_conf_contents() -> bool { - let expected_resolv_conf = "/var/run/NetworkManager/resolv.conf"; - let actual_resolv_conf = "/etc/resolv.conf"; - eq_file_content(&expected_resolv_conf, &actual_resolv_conf) -} - -fn device_is_ready(device_state: u32) -> bool { - /// Any state above `NM_DEVICE_STATE_IP_CONFIG` is considered to be an OK state to change the - /// DNS config. For the enums, see https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMDeviceState - const READY_STATES: [u32; 3] = [ - NM_DEVICE_STATE_IP_CHECK, - NM_DEVICE_STATE_SECONDARY, - NM_DEVICE_STATE_ACTIVATED, - ]; - READY_STATES.contains(&device_state) -} - -fn eq_file_content<P: AsRef<Path>>(a: &P, b: &P) -> bool { - let file_a = match File::open(a).map(BufReader::new) { - Ok(file) => file, - Err(e) => { - log::debug!("Failed top open file {}: {}", a.as_ref().display(), e); - return false; - } - }; - let file_b = match File::open(b).map(BufReader::new) { - Ok(file) => file, - Err(e) => { - log::debug!("Failed top open file {}: {}", b.as_ref().display(), e); - return false; - } - }; - - !file_a - .lines() - .zip(file_b.lines()) - .any(|(a, b)| match (a, b) { - (Ok(a), Ok(b)) => a != b, - _ => false, - }) } |
