diff options
| author | Emīls <emils@mullvad.net> | 2021-02-19 00:44:14 +0000 |
|---|---|---|
| committer | Emīls Piņķis <emils@mullvad.net> | 2021-03-02 11:45:54 +0000 |
| commit | 5d8fe0f8e75fd0b942285d3782c358a37d3dc34c (patch) | |
| tree | 1286fa532eab12089a1bbcec092b8e65e69a7988 | |
| parent | e84c98a0d51b630d37c9ba41bf825dbe3c6e2328 (diff) | |
| download | mullvadvpn-5d8fe0f8e75fd0b942285d3782c358a37d3dc34c.tar.xz mullvadvpn-5d8fe0f8e75fd0b942285d3782c358a37d3dc34c.zip | |
Watch DNS config changes in systemd-resolved
Certain NM versions will overwrite systemd-resovled config when they're
reapplying existing config, this can be invoked `nmcli general reload
dns-full` or by receiving a new DHCP lease. NM will just wipe the config
for interfaces it doesn't have the config for in systemd-resolved, and
since our daemon prefers systemd-resolved over NM, our config would be
wiped. To fix this, I've changed the systemd-resolved DNS code to listen
to changes to the global DNS config and reapply the tunnel interface DNS
config if it's changed in any way.
To better seperate the conecrns, the systemd-resolved DBus specific code
was moved to the `talpid-dbus` crate, and the DNS code that manages the
state and applies changes remains in `talpid-core`.
One other solution that was considered was to just prefer NM over
systemd-resolved, and we already kind of could do that, but the coming
NM versions (1.28 and up) seem to not be able to manage DNS via it's own
/etc/resolv.conf.
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | talpid-core/src/dns/linux/mod.rs | 8 | ||||
| -rw-r--r-- | talpid-core/src/dns/linux/systemd_resolved.rs | 344 | ||||
| -rw-r--r-- | talpid-dbus/Cargo.toml | 2 | ||||
| -rw-r--r-- | talpid-dbus/src/lib.rs | 1 | ||||
| -rw-r--r-- | talpid-dbus/src/network_manager.rs | 14 | ||||
| -rw-r--r-- | talpid-dbus/src/systemd_resolved.rs | 396 |
8 files changed, 494 insertions, 276 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cf29f853..7e6b021638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,9 @@ Line wrap the file at 100 chars. Th #### Linux - Stop using NM for managing DNS if it's newer than 1.26. +#### Linux +- Fix DNS issues where NM would overwrite Mullvad tunnel's DNS config in systemd-resolved. + ## [2021.2] - 2021-02-18 This release is for desktop only. diff --git a/Cargo.lock b/Cargo.lock index 17328f4e9a..93fe8d713f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2617,7 +2617,9 @@ dependencies = [ "dbus", "err-derive 0.3.0", "lazy_static", + "libc", "log 0.4.14", + "talpid-types", ] [[package]] diff --git a/talpid-core/src/dns/linux/mod.rs b/talpid-core/src/dns/linux/mod.rs index 9eef807b71..96f8402da7 100644 --- a/talpid-core/src/dns/linux/mod.rs +++ b/talpid-core/src/dns/linux/mod.rs @@ -1,7 +1,7 @@ mod network_manager; mod resolvconf; mod static_resolv_conf; -mod systemd_resolved; +pub(self) mod systemd_resolved; use self::{ network_manager::NetworkManager, resolvconf::Resolvconf, static_resolv_conf::StaticResolvConf, @@ -106,9 +106,11 @@ impl DnsMonitorHolder { .map(DnsMonitorHolder::SystemdResolved) .or_else(|err| { match err { - systemd_resolved::Error::NoSystemdResolved(_) => (), + systemd_resolved::Error::SystemdResolvedError( + systemd_resolved::SystemdDbusError::NoSystemdResolved(_), + ) => (), other_error => { - log::debug!("systemd-resolved is not being used because {}", other_error) + log::debug!("NetworkManager is not being used because {}", other_error) } } NetworkManager::new().map(DnsMonitorHolder::NetworkManager) diff --git a/talpid-core/src/dns/linux/systemd_resolved.rs b/talpid-core/src/dns/linux/systemd_resolved.rs index 73b8756a37..906efeb6be 100644 --- a/talpid-core/src/dns/linux/systemd_resolved.rs +++ b/talpid-core/src/dns/linux/systemd_resolved.rs @@ -1,311 +1,109 @@ -use super::RESOLV_CONF_PATH; -use crate::linux::iface_index; -use lazy_static::lazy_static; -use libc::{AF_INET, AF_INET6}; -use std::{fs, io, net::IpAddr, path::Path, sync::Arc, time::Duration}; -use talpid_dbus::dbus::{ - self, - arg::RefArg, - blocking::{stdintf::org_freedesktop_dbus::Properties, Proxy, SyncConnection}, +use crate::linux::{iface_index, IfaceIndexLookupError}; +use std::{ + net::IpAddr, + sync::{Arc, Mutex}, + thread, }; -use talpid_types::ErrorExt as _; +use talpid_dbus::systemd_resolved::{DnsState, SystemdResolved as DbusInterface}; + +pub(crate) use talpid_dbus::systemd_resolved::Error as SystemdDbusError; +use talpid_types::ErrorExt; pub type Result<T> = std::result::Result<T, Error>; #[derive(err_derive::Error, Debug)] -#[error(no_from)] pub enum Error { - #[error(display = "Failed to initialize a connection to D-Bus")] - ConnectDBus(#[error(source)] dbus::Error), - - #[error(display = "Failed to read /etc/resolv.conf: _0")] - ReadResolvConfError(#[error(source)] io::Error), - - #[error(display = "/etc/resolv.conf contents do not match systemd-resolved resolv.conf")] - ResolvConfDiffers, - - #[error(display = "/etc/resolv.conf is not a symlink to Systemd resolved")] - NotSymlinkedToResolvConf, - - #[error(display = "Static stub file does not point to localhost")] - StaticStubNotPointingToLocalhost, - - #[error(display = "Systemd resolved not detected")] - NoSystemdResolved(#[error(source)] dbus::Error), - - #[error(display = "Invalid network interface name")] - InvalidInterfaceName(#[error(source)] crate::linux::IfaceIndexLookupError), - - #[error(display = "Failed to find link interface in resolved manager")] - GetLinkError(#[error(source)] Box<Error>), - - #[error(display = "Failed to configure DNS domains")] - SetDomainsError(#[error(source)] dbus::Error), + #[error(display = "systemd-resolved operation failed")] + SystemdResolvedError(#[error(source)] SystemdDbusError), - #[error(display = "Failed to revert DNS settings of interface: {}", _0)] - RevertDnsError(String, #[error(source)] dbus::Error), - - #[error(display = "Failed to perform RPC call on D-Bus")] - DBusRpcError(#[error(source)] dbus::Error), -} - -lazy_static! { - static ref RESOLVED_STUB_PATHS: Vec<&'static Path> = vec![ - Path::new("/run/systemd/resolve/stub-resolv.conf"), - Path::new("/run/systemd/resolve/resolv.conf"), - Path::new("/var/run/systemd/resolve/stub-resolv.conf"), - Path::new("/var/run/systemd/resolve/resolv.conf"), - ]; + #[error(display = "Failed to resolve interface index with error {}", _0)] + InterfaceNameError(#[error(source)] IfaceIndexLookupError), } -const STATIC_STUB_PATH: &str = "/usr/lib/systemd/resolv.conf"; - -const RESOLVED_BUS: &str = "org.freedesktop.resolve1"; -const RPC_TIMEOUT: Duration = Duration::from_secs(1); - -const LINK_INTERFACE: &str = "org.freedesktop.resolve1.Link"; -const MANAGER_INTERFACE: &str = "org.freedesktop.resolve1.Manager"; -const GET_LINK_METHOD: &str = "GetLink"; -const SET_DNS_METHOD: &str = "SetDNS"; -const SET_DOMAINS_METHOD: &str = "SetDomains"; -const REVERT_METHOD: &str = "Revert"; - pub struct SystemdResolved { - pub dbus_connection: Arc<SyncConnection>, - interface_link: Option<(String, dbus::Path<'static>)>, + pub dbus_interface: DbusInterface, + state: Option<Arc<Mutex<Option<DnsState>>>>, + watcher_thread: Option<thread::JoinHandle<()>>, } + impl SystemdResolved { pub fn new() -> Result<Self> { - let dbus_connection = talpid_dbus::get_connection().map_err(Error::ConnectDBus)?; + let dbus_interface = DbusInterface::new()?; let systemd_resolved = SystemdResolved { - dbus_connection, - interface_link: None, + dbus_interface, + state: None, + watcher_thread: None, }; - systemd_resolved.ensure_resolved_exists()?; - Self::ensure_resolv_conf_is_resolved_symlink()?; Ok(systemd_resolved) } - fn ensure_resolved_exists(&self) -> Result<()> { - let _: Box<dyn RefArg> = self - .as_manager_object() - .get(&MANAGER_INTERFACE, "DNS") - .map_err(Error::NoSystemdResolved)?; - - Ok(()) - } - - fn ensure_resolv_conf_is_resolved_symlink() -> Result<()> { - match fs::read_link(RESOLV_CONF_PATH) { - Ok(link_target) => { - // if /etc/resolv.conf is not symlinked to the stub resolve.conf file , managing DNS - // through systemd-resolved will not ensure that our resolver is given priority - - // sometimes this will mean adding 1 and 2 seconds of latency to DNS - // queries, other times our resolver won't be considered at all. In - // this case, it's better to fall back to cruder management methods. - if Self::path_is_resolvconf_stub(&link_target) - || Self::resolv_conf_is_static_stub(&link_target)? - || Self::ensure_resolvconf_contents().is_ok() - { - Ok(()) - } else { - Err(Error::NotSymlinkedToResolvConf) - } - } - // etc/resolv.conf is not a symlink - Err(err) if err.kind() == io::ErrorKind::InvalidInput => { - Self::ensure_resolvconf_contents() - } - Err(err) => { - log::trace!("Failed to read /etc/resolv.conf symlink - {}", err); - Err(Error::NotSymlinkedToResolvConf) - } - } - } - - fn ensure_resolvconf_contents() -> Result<()> { - let resolv_conf = - fs::read_to_string(RESOLV_CONF_PATH).map_err(Error::ReadResolvConfError)?; - if RESOLVED_STUB_PATHS - .iter() - .filter_map(|path| fs::read_to_string(path).ok()) - .any(|link_contents| link_contents == resolv_conf) - { - Ok(()) - } else { - Err(Error::ResolvConfDiffers) - } - } - - fn path_is_resolvconf_stub(link_path: &Path) -> bool { - // if link path is relative to /etc/resolv.conf, resolve the path and compare it. - if link_path.is_relative() { - match Path::new("/etc/").join(link_path).canonicalize() { - Ok(link_destination) => RESOLVED_STUB_PATHS.contains(&link_destination.as_ref()), - Err(e) => { - log::error!( - "Failed to canonicalize resolv conf path {} - {}", - link_path.display(), - e - ); - false - } - } - } else { - RESOLVED_STUB_PATHS.contains(&link_path) - } - } - - /// Checks if path is pointing to the systemd-resolved _static_ resolv.conf file. If it's not, - /// it returns false, otherwise it checks whether the static stub file points to the local - /// resolver. If not, the file has been _meddled_ with, so we can't trust it. - fn resolv_conf_is_static_stub(link_path: &Path) -> Result<bool> { - if link_path == &STATIC_STUB_PATH.as_ref() { - let points_to_localhost = fs::read_to_string(link_path) - .map(|contents| { - let parts = contents.trim().split(' '); - parts - .map(str::parse::<IpAddr>) - .map(|maybe_ip| maybe_ip.map(|addr| addr.is_loopback()).unwrap_or(false)) - .any(|is_loopback| is_loopback) - }) - .unwrap_or(false); - - if points_to_localhost { - Ok(true) - } else { - Err(Error::StaticStubNotPointingToLocalhost) - } - } else { - Ok(false) - } - } + pub fn set_dns(&mut self, interface_name: &str, servers: &[IpAddr]) -> Result<()> { + let iface_index = iface_index(interface_name)?; + let dns_state = self.dbus_interface.set_dns(iface_index, servers)?; + let cloned_dns_state = self.set_dns_state(dns_state); + let weak_dns_state = Arc::downgrade(&cloned_dns_state); + let dns_state_should_continue = weak_dns_state.clone(); + let dbus_interface = self.dbus_interface.clone(); + let mut applied_servers: Vec<_> = servers.iter().cloned().collect(); + applied_servers.sort(); + let applied_servers = Arc::new(applied_servers); - fn as_manager_object(&self) -> Proxy<'_, &SyncConnection> { - Proxy::new( - RESOLVED_BUS, - "/org/freedesktop/resolve1", - RPC_TIMEOUT, - &self.dbus_connection, - ) - } + self.watcher_thread = Some(std::thread::spawn(move || { + let result = dbus_interface.clone().watch_dns_changes( + move |new_servers| { + (|| { + let dns_state_lock = weak_dns_state.upgrade()?; + let dns_state = dns_state_lock.lock().ok()?; + let dns_state_ref: &DnsState = &*dns_state.as_ref()?; - fn as_link_object<'a>( - &'a self, - link_object_path: dbus::Path<'a>, - ) -> Proxy<'a, &'a SyncConnection> { - Proxy::new( - RESOLVED_BUS, - link_object_path, - RPC_TIMEOUT, - &self.dbus_connection, - ) - } - - pub fn set_dns(&mut self, interface_name: &str, servers: &[IpAddr]) -> Result<()> { - let link_object_path = self - .fetch_link(interface_name) - .map_err(|e| Error::GetLinkError(Box::new(e)))?; - if let Err(e) = self.reset() { - log::debug!( - "Failed to reset previous DNS settings - {}", - e.display_chain() + let mut current_servers: Vec<IpAddr> = new_servers + .into_iter() + .filter(|server| server.iface_index == iface_index as i32) + .map(|server| server.address) + .collect(); + current_servers.sort(); + if current_servers != *dns_state_ref.set_servers { + log::debug!("DNS config for tunnel interface changed, currently applied servers - {:?}", current_servers); + if let Err(err) = dbus_interface.set_dns(iface_index, &applied_servers) { + log::error!("Failed to re-apply DNS config - {}", err); + } + } + Some(()) + })(); + }, + || dns_state_should_continue.upgrade().is_some(), ); - } - - self.set_link_dns(&link_object_path, servers)?; - self.interface_link = Some((interface_name.to_string(), link_object_path)); - + if let Err(err) = result { + log::error!("Failed to watch DNS config updates: {}", err); + } + })); Ok(()) } - fn fetch_link(&self, interface_name: &str) -> Result<dbus::Path<'static>> { - let interface_index = iface_index(interface_name).map_err(Error::InvalidInterfaceName)?; - - self.as_manager_object() - .method_call( - MANAGER_INTERFACE, - GET_LINK_METHOD, - (interface_index as i32,), - ) - .map_err(Error::DBusRpcError) - .map(|result: (dbus::Path<'static>,)| result.0) - } - - fn set_link_dns<'a, 'b: 'a>( - &'a self, - link_object_path: &'b dbus::Path<'static>, - servers: &[IpAddr], - ) -> Result<()> { - let servers = servers - .iter() - .map(|addr| (ip_version(addr), ip_to_bytes(addr))) - .collect::<Vec<_>>(); - self.as_link_object(link_object_path.clone()) - .method_call(LINK_INTERFACE, SET_DNS_METHOD, (servers,)) - .map_err(Error::DBusRpcError)?; - - // set the search domain to catch all DNS requests, forces the link to be the prefered - // resolver, otherwise systemd-resolved will use other interfaces to do DNS lookups - let dns_domains: &[_] = &[(&".", true)]; - - Proxy::new( - RESOLVED_BUS, - link_object_path, - RPC_TIMEOUT, - &*self.dbus_connection, - ) - .method_call(LINK_INTERFACE, SET_DOMAINS_METHOD, (dns_domains,)) - .map_err(Error::SetDomainsError) + fn set_dns_state(&mut self, dns_state: DnsState) -> Arc<Mutex<Option<DnsState>>> { + let new_state = Arc::new(Mutex::new(Some(dns_state))); + self.state = Some(new_state.clone()); + new_state } pub fn reset(&mut self) -> Result<()> { - if let Some((interface_name, link_object_path)) = self.interface_link.take() { - self.revert_link(link_object_path, &interface_name) - .map_err(|e| Error::RevertDnsError(interface_name.to_owned(), e)) + if let Some(state_lock) = self.state.take() { + if let Some(dns_state) = state_lock.lock().expect("DNS state lock poisoned").take() { + if let Err(err) = self.dbus_interface.revert_link(dns_state) { + log::error!("Failed to revert DNS config - {}", err.display_chain()); + } + } } else { log::trace!("No DNS settings to reset"); - Ok(()) } - } - - fn revert_link( - &mut self, - link_object_path: dbus::Path<'static>, - interface_name: &str, - ) -> std::result::Result<(), dbus::Error> { - let link = self.as_link_object(link_object_path); - - if let Err(error) = link.method_call::<(), _, _, _>(LINK_INTERFACE, REVERT_METHOD, ()) { - if error.name() == Some("org.freedesktop.DBus.Error.UnknownObject") { - log::trace!( - "Not resetting DNS of interface {} because it no longer exists", - interface_name - ); - Ok(()) - } else { - Err(error) - } - } else { - Ok(()) + if let Some(join_handle) = self.watcher_thread.take() { + let _ = join_handle.join(); } - } -} -fn ip_version(address: &IpAddr) -> i32 { - match address { - IpAddr::V4(_) => AF_INET, - IpAddr::V6(_) => AF_INET6, - } -} - -fn ip_to_bytes(address: &IpAddr) -> Vec<u8> { - match address { - IpAddr::V4(v4_address) => v4_address.octets().to_vec(), - IpAddr::V6(v6_address) => v6_address.octets().to_vec(), + Ok(()) } } diff --git a/talpid-dbus/Cargo.toml b/talpid-dbus/Cargo.toml index 94d67d61fe..5685daf922 100644 --- a/talpid-dbus/Cargo.toml +++ b/talpid-dbus/Cargo.toml @@ -10,3 +10,5 @@ dbus = "0.9" err-derive = "0.3.0" lazy_static = "1.0" log = "0.4" +libc = "0.2" +talpid-types = { path = "../talpid-types" } diff --git a/talpid-dbus/src/lib.rs b/talpid-dbus/src/lib.rs index d8515ce305..50f3bafa1f 100644 --- a/talpid-dbus/src/lib.rs +++ b/talpid-dbus/src/lib.rs @@ -4,6 +4,7 @@ pub use dbus; use dbus::blocking::SyncConnection; use std::sync::{Arc, Mutex}; pub mod network_manager; +pub mod systemd_resolved; lazy_static::lazy_static! { static ref DBUS_CONNECTION: Mutex<Option<Arc<SyncConnection>>> = Mutex::new(None); diff --git a/talpid-dbus/src/network_manager.rs b/talpid-dbus/src/network_manager.rs index b714996dad..160b2f3e5f 100644 --- a/talpid-dbus/src/network_manager.rs +++ b/talpid-dbus/src/network_manager.rs @@ -1,4 +1,5 @@ //! NetworkManager is the one-stop-shop of network configuration on Linux. +use super::systemd_resolved; pub use dbus::arg::{RefArg, Variant}; use dbus::{ arg, @@ -68,6 +69,12 @@ pub enum Error { #[error(display = "Failed to match the returned D-Bus object with expected type")] MatchDBusTypeError(#[error(source)] dbus::arg::TypeMismatchError), + #[error( + display = "NM is configured to manage DNS via systemd-resolved but systemd-resolved is not managing /etc/resolv.conf: {}", + _0 + )] + SystemdResolvedNotManagingResolvconf(systemd_resolved::Error), + #[error(display = "Configuration has no device associated to it")] NoDevice, @@ -416,6 +423,13 @@ impl NetworkManager { return Err(Error::NetworkManagerNotManagingDns); } + if management_mode == "systemd-resolved" { + return match systemd_resolved::SystemdResolved::new() { + Ok(_) => Ok(()), + Err(err) => Err(Error::SystemdResolvedNotManagingResolvconf(err)), + }; + } + let dns_mode: String = self .as_dns_manager() .get(NM_DNS_MANAGER, DNS_MODE_KEY) diff --git a/talpid-dbus/src/systemd_resolved.rs b/talpid-dbus/src/systemd_resolved.rs new file mode 100644 index 0000000000..e4e96c3b2f --- /dev/null +++ b/talpid-dbus/src/systemd_resolved.rs @@ -0,0 +1,396 @@ +use dbus::{ + self, + arg::{self, RefArg}, + blocking::{ + stdintf::org_freedesktop_dbus::{Properties, PropertiesPropertiesChanged}, + Proxy, SyncConnection, + }, + message::{MatchRule, SignalArgs}, +}; +use lazy_static::lazy_static; +use libc::{AF_INET, AF_INET6}; +use std::{fs, io, net::IpAddr, path::Path, sync::Arc, time::Duration}; + +pub type Result<T> = std::result::Result<T, Error>; + + +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +pub enum Error { + #[error(display = "Failed to initialize a connection to D-Bus")] + ConnectDBus(#[error(source)] dbus::Error), + + #[error(display = "Failed to read /etc/resolv.conf: _0")] + ReadResolvConfError(#[error(source)] io::Error), + + #[error(display = "/etc/resolv.conf contents do not match systemd-resolved resolv.conf")] + ResolvConfDiffers, + + #[error(display = "/etc/resolv.conf is not a symlink to Systemd resolved")] + NotSymlinkedToResolvConf, + + #[error(display = "Static stub file does not point to localhost")] + StaticStubNotPointingToLocalhost, + + #[error(display = "Systemd resolved not detected")] + NoSystemdResolved(#[error(source)] dbus::Error), + + #[error(display = "Failed to find link interface in resolved manager")] + GetLinkError(#[error(source)] Box<Error>), + + #[error(display = "Failed to configure DNS domains")] + SetDomainsError(#[error(source)] dbus::Error), + + #[error(display = "Failed to revert DNS settings of interface: {}", _0)] + RevertDnsError(String, #[error(source)] dbus::Error), + + #[error(display = "Failed to perform RPC call on D-Bus")] + DBusRpcError(#[error(source)] dbus::Error), + + #[error(display = "Failed to add a match to listen for DNS config updates")] + DnsUpdateMatchError(#[error(source)] dbus::Error), + + #[error(display = "Failed to remove a match for DNS config updates")] + DnsUpdateRemoveMatchError(#[error(source)] dbus::Error), +} + +lazy_static! { + static ref RESOLVED_STUB_PATHS: Vec<&'static Path> = vec![ + Path::new("/run/systemd/resolve/stub-resolv.conf"), + Path::new("/run/systemd/resolve/resolv.conf"), + Path::new("/var/run/systemd/resolve/stub-resolv.conf"), + Path::new("/var/run/systemd/resolve/resolv.conf"), + ]; +} + +const RESOLV_CONF_PATH: &str = "/etc/resolv.conf"; +const STATIC_STUB_PATH: &str = "/usr/lib/systemd/resolv.conf"; + +const RESOLVED_BUS: &str = "org.freedesktop.resolve1"; +const RESOLVED_MANAGER_PATH: &str = "/org/freedesktop/resolve1"; + +const RPC_TIMEOUT: Duration = Duration::from_secs(1); + +const LINK_INTERFACE: &str = "org.freedesktop.resolve1.Link"; +const MANAGER_INTERFACE: &str = "org.freedesktop.resolve1.Manager"; +const DNS_SERVERS: &str = "DNS"; +const GET_LINK_METHOD: &str = "GetLink"; +const SET_DNS_METHOD: &str = "SetDNS"; +const SET_DOMAINS_METHOD: &str = "SetDomains"; +const REVERT_METHOD: &str = "Revert"; + +#[derive(Clone)] +pub struct SystemdResolved { + pub dbus_connection: Arc<SyncConnection>, +} + +pub struct DnsState { + pub interface_path: dbus::Path<'static>, + pub interface_index: u32, + pub set_servers: Vec<IpAddr>, +} + +impl SystemdResolved { + pub fn new() -> Result<Self> { + let dbus_connection = crate::get_connection().map_err(Error::ConnectDBus)?; + + let systemd_resolved = SystemdResolved { dbus_connection }; + + systemd_resolved.ensure_resolved_exists()?; + Self::ensure_resolv_conf_is_resolved_symlink()?; + Ok(systemd_resolved) + } + + pub fn ensure_resolved_exists(&self) -> Result<()> { + let _: Box<dyn RefArg> = self + .as_manager_object() + .get(&MANAGER_INTERFACE, "DNS") + .map_err(Error::NoSystemdResolved)?; + + Ok(()) + } + + pub fn ensure_resolv_conf_is_resolved_symlink() -> Result<()> { + match fs::read_link(RESOLV_CONF_PATH) { + Ok(link_target) => { + // if /etc/resolv.conf is not symlinked to the stub resolve.conf file , managing DNS + // through systemd-resolved will not ensure that our resolver is given priority - + // sometimes this will mean adding 1 and 2 seconds of latency to DNS + // queries, other times our resolver won't be considered at all. In + // this case, it's better to fall back to cruder management methods. + if Self::path_is_resolvconf_stub(&link_target) + || Self::resolv_conf_is_static_stub(&link_target)? + || Self::ensure_resolvconf_contents().is_ok() + { + Ok(()) + } else { + Err(Error::NotSymlinkedToResolvConf) + } + } + // etc/resolv.conf is not a symlink + Err(err) if err.kind() == io::ErrorKind::InvalidInput => { + Self::ensure_resolvconf_contents() + } + Err(err) => { + log::trace!("Failed to read /etc/resolv.conf symlink - {}", err); + Err(Error::NotSymlinkedToResolvConf) + } + } + } + + fn ensure_resolvconf_contents() -> Result<()> { + let resolv_conf = + fs::read_to_string(RESOLV_CONF_PATH).map_err(Error::ReadResolvConfError)?; + if RESOLVED_STUB_PATHS + .iter() + .filter_map(|path| fs::read_to_string(path).ok()) + .any(|link_contents| link_contents == resolv_conf) + { + Ok(()) + } else { + Err(Error::ResolvConfDiffers) + } + } + + fn path_is_resolvconf_stub(link_path: &Path) -> bool { + // if link path is relative to /etc/resolv.conf, resolve the path and compare it. + if link_path.is_relative() { + match Path::new("/etc/").join(link_path).canonicalize() { + Ok(link_destination) => RESOLVED_STUB_PATHS.contains(&link_destination.as_ref()), + Err(e) => { + log::error!( + "Failed to canonicalize resolv conf path {} - {}", + link_path.display(), + e + ); + false + } + } + } else { + RESOLVED_STUB_PATHS.contains(&link_path) + } + } + + /// Checks if path is pointing to the systemd-resolved _static_ resolv.conf file. If it's not, + /// it returns false, otherwise it checks whether the static stub file points to the local + /// resolver. If not, the file has been _meddled_ with, so we can't trust it. + fn resolv_conf_is_static_stub(link_path: &Path) -> Result<bool> { + if link_path == &STATIC_STUB_PATH.as_ref() { + let points_to_localhost = fs::read_to_string(link_path) + .map(|contents| { + let parts = contents.trim().split(' '); + parts + .map(str::parse::<IpAddr>) + .map(|maybe_ip| maybe_ip.map(|addr| addr.is_loopback()).unwrap_or(false)) + .any(|is_loopback| is_loopback) + }) + .unwrap_or(false); + + if points_to_localhost { + Ok(true) + } else { + Err(Error::StaticStubNotPointingToLocalhost) + } + } else { + Ok(false) + } + } + + + fn as_manager_object(&self) -> Proxy<'_, &SyncConnection> { + Proxy::new( + RESOLVED_BUS, + "/org/freedesktop/resolve1", + RPC_TIMEOUT, + &self.dbus_connection, + ) + } + + fn as_link_object<'a>( + &'a self, + link_object_path: dbus::Path<'a>, + ) -> Proxy<'a, &'a SyncConnection> { + Proxy::new( + RESOLVED_BUS, + link_object_path, + RPC_TIMEOUT, + &self.dbus_connection, + ) + } + + pub fn set_dns(&self, interface_index: u32, servers: &[IpAddr]) -> Result<DnsState> { + let link_object_path = self + .fetch_link(interface_index) + .map_err(|e| Error::GetLinkError(Box::new(e)))?; + + let mut set_servers = servers.to_vec(); + set_servers.sort(); + self.set_link_dns(&link_object_path, servers)?; + Ok(DnsState { + interface_path: link_object_path, + interface_index, + set_servers, + }) + } + + fn fetch_link(&self, interface_index: u32) -> Result<dbus::Path<'static>> { + self.as_manager_object() + .method_call( + MANAGER_INTERFACE, + GET_LINK_METHOD, + (interface_index as i32,), + ) + .map_err(Error::DBusRpcError) + .map(|result: (dbus::Path<'static>,)| result.0) + } + + fn set_link_dns<'a, 'b: 'a>( + &'a self, + link_object_path: &'b dbus::Path<'static>, + servers: &[IpAddr], + ) -> Result<()> { + let servers = servers + .iter() + .map(|addr| (ip_version(addr), ip_to_bytes(addr))) + .collect::<Vec<_>>(); + self.as_link_object(link_object_path.clone()) + .method_call(LINK_INTERFACE, SET_DNS_METHOD, (servers,)) + .map_err(Error::DBusRpcError)?; + + // set the search domain to catch all DNS requests, forces the link to be the prefered + // resolver, otherwise systemd-resolved will use other interfaces to do DNS lookups + let dns_domains: &[_] = &[(&".", true)]; + + Proxy::new( + RESOLVED_BUS, + link_object_path, + RPC_TIMEOUT, + &*self.dbus_connection, + ) + .method_call(LINK_INTERFACE, SET_DOMAINS_METHOD, (dns_domains,)) + .map_err(Error::SetDomainsError) + } + + pub fn revert_link(&mut self, dns_state: DnsState) -> std::result::Result<(), dbus::Error> { + let link = self.as_link_object(dns_state.interface_path); + + if let Err(error) = link.method_call::<(), _, _, _>(LINK_INTERFACE, REVERT_METHOD, ()) { + if error.name() == Some("org.freedesktop.DBus.Error.UnknownObject") { + log::trace!( + "Not resetting DNS of interface {} because it no longer exists", + dns_state.interface_index + ); + Ok(()) + } else { + Err(error) + } + } else { + Ok(()) + } + } + + pub fn watch_dns_changes<F: FnMut(Vec<DnsServer>) + Send + Sync + 'static, S: Fn() -> bool>( + &mut self, + mut callback: F, + should_continue: S, + ) -> Result<()> { + let mut match_rule = + MatchRule::new_signal(PropertiesPropertiesChanged::INTERFACE, DNS_SERVERS); + match_rule.member = None; + match_rule.path = Some(RESOLVED_MANAGER_PATH.into()); + let dns_matcher = self + .dbus_connection + .add_match( + match_rule, + move |mut prop_changed: PropertiesPropertiesChanged, _connection, _message| { + if let Some(dns_change) = prop_changed + .changed_properties + .get_mut(DNS_SERVERS) + .and_then(|dns_change| { + dns_change.as_iter().and_then(|mut iter| iter.next()) + }) + { + match DnsServer::server_list_from_refarg(dns_change) { + Some(new_server_list) => { + callback(new_server_list); + } + None => { + log::error!("Failed to deserialize message {:?}", dns_change); + } + } + }; + true + }, + ) + .map_err(Error::DnsUpdateMatchError)?; + + while should_continue() { + if let Err(err) = self.dbus_connection.process(RPC_TIMEOUT) { + log::error!("Failed to process DBus messages: {}", err); + } + } + + self.dbus_connection + .remove_match(dns_matcher) + .map_err(Error::DnsUpdateRemoveMatchError) + } +} + +#[derive(Debug)] +pub struct DnsServer { + pub iface_index: i32, + pub address_family: i32, + pub address: IpAddr, +} + +impl DnsServer { + fn from_refarg(refarg: &dyn RefArg) -> Option<Self> { + let mut iter = refarg.as_iter()?; + let iface_index = *arg::cast(&*iter.next()?.box_clone())?; + let address_family = *arg::cast(&*iter.next()?.box_clone())?; + + let ip_bytes = iter.next()?.box_clone(); + let ip_bytes: &Vec<u8> = arg::cast(&ip_bytes)?; + let address = ip_from_bytes(&ip_bytes)?; + Some(Self { + iface_index, + address_family, + address, + }) + } + + fn server_list_from_refarg(refarg: &dyn RefArg) -> Option<Vec<Self>> { + let iter = refarg.as_iter()?; + Some(iter.filter_map(DnsServer::from_refarg).collect()) + } +} + +fn ip_version(address: &IpAddr) -> i32 { + match address { + IpAddr::V4(_) => AF_INET, + IpAddr::V6(_) => AF_INET6, + } +} + +fn ip_to_bytes(address: &IpAddr) -> Vec<u8> { + match address { + IpAddr::V4(v4_address) => v4_address.octets().to_vec(), + IpAddr::V6(v6_address) => v6_address.octets().to_vec(), + } +} + +fn ip_from_bytes(bytes: &[u8]) -> Option<IpAddr> { + match bytes.len() { + 4 => { + let mut ipv4_bytes = [0u8; 4]; + &mut ipv4_bytes.copy_from_slice(bytes); + Some(IpAddr::from(ipv4_bytes)) + } + 16 => { + let mut ipv6_bytes = [0u8; 16]; + &mut ipv6_bytes.copy_from_slice(bytes); + Some(IpAddr::from(ipv6_bytes)) + } + _ => None, + } +} |
