summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls Piņķis <emils@mullvad.net>2021-03-02 11:46:13 +0000
committerEmīls Piņķis <emils@mullvad.net>2021-03-02 11:46:13 +0000
commit583760ef9ef40e4828fffba5df9e6e85f30e3e17 (patch)
tree1286fa532eab12089a1bbcec092b8e65e69a7988
parente84c98a0d51b630d37c9ba41bf825dbe3c6e2328 (diff)
parent5d8fe0f8e75fd0b942285d3782c358a37d3dc34c (diff)
downloadmullvadvpn-583760ef9ef40e4828fffba5df9e6e85f30e3e17.tar.xz
mullvadvpn-583760ef9ef40e4828fffba5df9e6e85f30e3e17.zip
Merge branch 'linux-reapply-sd-config'
-rw-r--r--CHANGELOG.md3
-rw-r--r--Cargo.lock2
-rw-r--r--talpid-core/src/dns/linux/mod.rs8
-rw-r--r--talpid-core/src/dns/linux/systemd_resolved.rs344
-rw-r--r--talpid-dbus/Cargo.toml2
-rw-r--r--talpid-dbus/src/lib.rs1
-rw-r--r--talpid-dbus/src/network_manager.rs14
-rw-r--r--talpid-dbus/src/systemd_resolved.rs396
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,
+ }
+}