summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2020-11-12 16:39:50 +0000
committerEmīls <emils@mullvad.net>2020-11-19 11:53:23 +0000
commit2fd8b360822373be336dd8d896f921a752e458fb (patch)
treed61e7ce74a894501f6455e0f21d8ab118281a303
parenta51e006ddfcc9a49ad4a4bf448a58bf2d4f5bf6a (diff)
downloadmullvadvpn-2fd8b360822373be336dd8d896f921a752e458fb.tar.xz
mullvadvpn-2fd8b360822373be336dd8d896f921a752e458fb.zip
Consolidate NetworkManager code
-rw-r--r--talpid-core/src/linux.rs74
-rw-r--r--talpid-core/src/linux/mod.rs31
-rw-r--r--talpid-core/src/linux/network_manager.rs848
3 files changed, 879 insertions, 74 deletions
diff --git a/talpid-core/src/linux.rs b/talpid-core/src/linux.rs
deleted file mode 100644
index c91e228369..0000000000
--- a/talpid-core/src/linux.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-use std::{
- ffi::{self, CString},
- io,
-};
-
-/// Converts an interface name into the corresponding index.
-pub fn iface_index(name: &str) -> Result<libc::c_uint, IfaceIndexLookupError> {
- let c_name = CString::new(name)
- .map_err(|e| IfaceIndexLookupError::InvalidInterfaceName(name.to_owned(), e))?;
- let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
- if index == 0 {
- Err(IfaceIndexLookupError::InterfaceLookupError(
- name.to_owned(),
- io::Error::last_os_error(),
- ))
- } else {
- Ok(index)
- }
-}
-
-#[derive(Debug, err_derive::Error)]
-pub enum IfaceIndexLookupError {
- #[error(display = "Invalid network interface name: {}", _0)]
- InvalidInterfaceName(String, #[error(source)] ffi::NulError),
- #[error(display = "Failed to get index for interface {}", _0)]
- InterfaceLookupError(String, #[error(source)] io::Error),
-}
-
-// b"mole" is [ 0x6d, 0x6f 0x6c, 0x65 ]
-pub const TUNNEL_FW_MARK: u32 = 0x6d6f6c65;
-
-pub mod network_manager {
- use dbus::ffidisp::{stdintf::*, Connection};
-
- const NM_BUS: &str = "org.freedesktop.NetworkManager";
- const NM_TOP_OBJECT: &str = "org.freedesktop.NetworkManager";
- const NM_OBJECT_PATH: &str = "/org/freedesktop/NetworkManager";
- const CONNECTIVITY_CHECK_KEY: &str = "ConnectivityCheckEnabled";
- const RPC_TIMEOUT_MS: i32 = 1500;
-
- /// Ensures NetworkManager's connectivity check is disabled and returns the connectivity check
- /// previous state. Returns true only if the connectivity check was enabled and is now
- /// disabled. Disabling the connectivity check should be done before a firewall is applied
- /// due to the fact that blocking DNS requests can make it hang:
- /// https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/404
- pub fn nm_disable_connectivity_check(connection: &Connection) -> Option<bool> {
- let nm_manager = connection.with_path(NM_BUS, NM_OBJECT_PATH, RPC_TIMEOUT_MS);
- match nm_manager.get(NM_TOP_OBJECT, CONNECTIVITY_CHECK_KEY) {
- Ok(true) => {
- if let Err(err) = nm_manager.set(NM_TOP_OBJECT, CONNECTIVITY_CHECK_KEY, false) {
- log::error!(
- "Failed to disable NetworkManager connectivity check: {}",
- err
- );
- Some(false)
- } else {
- Some(true)
- }
- }
- Ok(false) => Some(false),
- Err(_) => None,
- }
- }
-
- /// Enabled NetworkManager's connectivity check. Fails silently.
- pub fn nm_enable_connectivity_check(connection: &Connection) {
- if let Err(err) = connection
- .with_path(NM_BUS, NM_OBJECT_PATH, RPC_TIMEOUT_MS)
- .set(NM_TOP_OBJECT, CONNECTIVITY_CHECK_KEY, true)
- {
- log::error!("Failed to reset NetworkManager connectivity check: {}", err);
- }
- }
-}
diff --git a/talpid-core/src/linux/mod.rs b/talpid-core/src/linux/mod.rs
new file mode 100644
index 0000000000..2ecf1935de
--- /dev/null
+++ b/talpid-core/src/linux/mod.rs
@@ -0,0 +1,31 @@
+use std::{
+ ffi::{self, CString},
+ io,
+};
+
+pub mod network_manager;
+/// Converts an interface name into the corresponding index.
+pub fn iface_index(name: &str) -> Result<libc::c_uint, IfaceIndexLookupError> {
+ let c_name = CString::new(name)
+ .map_err(|e| IfaceIndexLookupError::InvalidInterfaceName(name.to_owned(), e))?;
+ let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
+ if index == 0 {
+ Err(IfaceIndexLookupError::InterfaceLookupError(
+ name.to_owned(),
+ io::Error::last_os_error(),
+ ))
+ } else {
+ Ok(index)
+ }
+}
+
+#[derive(Debug, err_derive::Error)]
+pub enum IfaceIndexLookupError {
+ #[error(display = "Invalid network interface name: {}", _0)]
+ InvalidInterfaceName(String, #[error(source)] ffi::NulError),
+ #[error(display = "Failed to get index for interface {}", _0)]
+ InterfaceLookupError(String, #[error(source)] io::Error),
+}
+
+// b"mole" is [ 0x6d, 0x6f 0x6c, 0x65 ]
+pub const TUNNEL_FW_MARK: u32 = 0x6d6f6c65;
diff --git a/talpid-core/src/linux/network_manager.rs b/talpid-core/src/linux/network_manager.rs
new file mode 100644
index 0000000000..373104a0ea
--- /dev/null
+++ b/talpid-core/src/linux/network_manager.rs
@@ -0,0 +1,848 @@
+//! NetworkManager is the one-stop-shop of network configuration on Linux.
+use crate::tunnel::wireguard::{
+ config::Config as WireguardConfig, wireguard_kernel::MULLVAD_INTERFACE_NAME,
+};
+use dbus::{
+ arg::{self, RefArg, Variant},
+ blocking::{stdintf::org_freedesktop_dbus::Properties, Proxy, SyncConnection},
+ message::MatchRule,
+};
+use std::{
+ collections::HashMap,
+ fs::File,
+ io::{BufRead, BufReader},
+ net::IpAddr,
+ path::Path,
+ sync::{
+ atomic::{AtomicU32, Ordering},
+ Arc,
+ },
+ time::{Duration, Instant},
+};
+
+const NM_BUS: &str = "org.freedesktop.NetworkManager";
+const NM_MANAGER: &str = "org.freedesktop.NetworkManager";
+const NM_MANAGER_PATH: &str = "/org/freedesktop/NetworkManager";
+const CONNECTIVITY_CHECK_KEY: &str = "ConnectivityCheckEnabled";
+
+const NM_DNS_MANAGER: &str = "org.freedesktop.NetworkManager.DnsManager";
+const NM_DNS_MANAGER_PATH: &str = "/org/freedesktop/NetworkManager/DnsManager";
+const NM_DEVICE: &str = "org.freedesktop.NetworkManager.Device";
+const NM_DEVICE_STATISTICS: &str = "org.freedesktop.NetworkManager.Device.Statistics";
+
+const NM_IP4_CONFIG: &str = "org.freedesktop.NetworkManager.IP4Config";
+const NM_IP6_CONFIG: &str = "org.freedesktop.NetworkManager.IP6Config";
+const DEVICE_READY_TIMEOUT: Duration = Duration::from_secs(15);
+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;
+
+const NM_SETTINGS_INTERFACE: &str = "org.freedesktop.NetworkManager.Settings";
+const NM_SETTINGS_CONNECTION_INTERFACE: &str = "org.freedesktop.NetworkManager.Settings.Connection";
+const NM_SETTINGS_PATH: &str = "/org/freedesktop/NetworkManager/Settings";
+const NM_CONNECTION_ACTIVE: &str = "org.freedesktop.NetworkManager.Connection.Active";
+
+const NM_ADD_CONNECTION_VOLATILE: u32 = 0x2;
+
+const RPC_TIMEOUT: std::time::Duration = Duration::from_secs(3);
+
+const DBUS_UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
+
+const MINIMUM_SUPPORTED_MAJOR_VERSION: u32 = 1;
+const MINIMUM_SUPPORTED_MINOR_VERSION: u32 = 16;
+
+
+const NM_DEVICE_STATE_CHANGED: &'static str = "StateChanged";
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(err_derive::Error, Debug)]
+pub enum Error {
+ #[error(display = "Error while communicating over Dbus")]
+ Dbus(#[error(source)] dbus::Error),
+
+ #[error(display = "Failed to match the returned D-Bus object with expected type")]
+ MatchDBusTypeError(#[error(source)] dbus::arg::TypeMismatchError),
+
+ #[error(display = "Configuration has no device associated to it")]
+ NoDevice,
+
+ #[error(display = "NetworkManager is too old - {}", _0)]
+ NMTooOld(String),
+
+ #[error(display = "Device inactive: {}", _0)]
+ DeviceNotReady(u32),
+
+ #[error(display = "Device not found")]
+ DeviceNotFound,
+
+ #[error(display = "NetworkManager not detected")]
+ NetworkManagerNotDetected,
+
+ #[error(display = "NetworkManager is too old: {}", 0)]
+ TooOldNetworkManager(String),
+
+ #[error(display = "NetworkManager is not managing DNS")]
+ NetworkManagerNotManagingDns,
+
+ #[error(display = "Failed to get devices from NetworkManager object")]
+ ObtainDevices,
+}
+
+
+pub type VariantRefArg = Variant<Box<dyn RefArg>>;
+pub type VariantMap = HashMap<String, VariantRefArg>;
+// settings are a{sa{sv}}
+pub type DeviceConfig = HashMap<String, VariantMap>;
+
+/// Implements functionality to control NetworkManager over DBus.
+pub struct NetworkManager {
+ connection: Arc<SyncConnection>,
+}
+
+impl NetworkManager {
+ pub fn new() -> Result<Self> {
+ Ok(Self {
+ connection: Arc::new(SyncConnection::new_system()?),
+ })
+ }
+
+ pub fn create_wg_tunnel(&self, config: &WireguardConfig) -> Result<WireguardTunnel> {
+ self.ensure_nm_is_new_enough_for_wireguard()?;
+ let tunnel = self.create_wg_tunnel_inner(config)?;
+ if let Err(err) = self.wait_until_device_is_ready(&tunnel.device_path) {
+ if let Err(removal_error) = self.remove_tunnel(tunnel) {
+ log::error!(
+ "Failed to remove WireGuard tunnel after it not becoming ready fast enough: {}",
+ removal_error
+ );
+ }
+ return Err(err);
+ }
+
+ Ok(tunnel)
+ }
+
+ pub fn get_interface_name(&self, tunnel: &WireguardTunnel) -> Result<String> {
+ tunnel
+ .device_proxy(&*self.connection)
+ .get(NM_DEVICE, "Interface")
+ .map_err(Error::Dbus)
+ }
+
+ pub fn get_device_state(&self, device: &dbus::Path<'_>) -> Result<u32> {
+ self.as_path(device)
+ .get(NM_DEVICE, "State")
+ .map_err(Error::Dbus)
+ }
+
+ fn create_wg_tunnel_inner(&self, config: &WireguardConfig) -> Result<WireguardTunnel> {
+ let settings_map = Self::convert_config_to_dbus(config);
+
+ let config_path: dbus::Path<'static> = match self.add_connection_2(&settings_map) {
+ Ok((path, _result)) => path,
+ Err(Error::Dbus(dbus_error)) if dbus_error.name() == Some(DBUS_UNKNOWN_METHOD) => {
+ self.add_connection_unsaved(&settings_map)?.0
+ }
+ Err(err) => {
+ log::error!(
+ "Failed to create a new interface via AddConnection2 - {}",
+ err
+ );
+ return Err(err);
+ }
+ };
+
+ let manager = self.nm_manager();
+ let (connection_path,): (dbus::Path<'static>,) = manager
+ .method_call(
+ NM_MANAGER,
+ "ActivateConnection",
+ (
+ &config_path,
+ &dbus::Path::new("/").unwrap(),
+ &dbus::Path::new("/").unwrap(),
+ ),
+ )
+ .map_err(Error::Dbus)?;
+
+ let connection = Proxy::new(NM_BUS, &connection_path, RPC_TIMEOUT, &*self.connection);
+ let device_paths: Vec<dbus::Path<'static>> = connection
+ .get(NM_CONNECTION_ACTIVE, "Devices")
+ .map_err(Error::Dbus)?;
+ let device_path = device_paths.into_iter().next().ok_or(Error::NoDevice)?;
+
+
+ Ok(WireguardTunnel {
+ config_path,
+ connection_path,
+ device_path,
+ })
+ }
+
+ fn ensure_nm_is_new_enough_for_wireguard(&self) -> Result<()> {
+ let manager = self.nm_manager();
+ let version_string: String = manager.get(NM_MANAGER, "Version").map_err(Error::Dbus)?;
+ let version_too_old = || Error::NMTooOld(version_string.clone());
+ let mut parts = version_string
+ .split(".")
+ .map(|part| part.parse().map_err(|_| version_too_old()));
+
+ let major_version: u32 = parts.next().ok_or_else(|| version_too_old())??;
+ let minor_version: u32 = parts.next().ok_or_else(|| version_too_old())??;
+
+ if major_version < MINIMUM_SUPPORTED_MAJOR_VERSION
+ || minor_version < MINIMUM_SUPPORTED_MINOR_VERSION
+ {
+ Err(version_too_old())
+ } else {
+ Ok(())
+ }
+ }
+
+ fn add_connection_2(
+ &self,
+ settings_map: &DeviceConfig,
+ ) -> Result<(dbus::Path<'static>, DeviceConfig)> {
+ let args: VariantMap = HashMap::new();
+
+ Proxy::new(NM_BUS, NM_SETTINGS_PATH, RPC_TIMEOUT, &*self.connection)
+ .method_call(
+ NM_SETTINGS_INTERFACE,
+ "AddConnection2",
+ (settings_map, NM_ADD_CONNECTION_VOLATILE, args),
+ )
+ .map_err(Error::Dbus)
+ }
+
+ fn add_connection_unsaved(
+ &self,
+ settings_map: &DeviceConfig,
+ ) -> Result<(dbus::Path<'static>,)> {
+ Proxy::new(NM_BUS, NM_SETTINGS_PATH, RPC_TIMEOUT, &*self.connection)
+ .method_call(
+ NM_SETTINGS_INTERFACE,
+ "AddConnectionUnsaved",
+ (settings_map,),
+ )
+ .map_err(Error::Dbus)
+ }
+
+ pub fn set_stats_refresh_rate(
+ &self,
+ tunnel: &WireguardTunnel,
+ refresh_rate: u32,
+ ) -> Result<()> {
+ tunnel
+ .device_proxy(&*self.connection)
+ .set(NM_DEVICE_STATISTICS, "RefreshRateMs", refresh_rate)
+ .map_err(Error::Dbus)
+ }
+
+ pub fn get_tunnel_stats(&self, tunnel: &WireguardTunnel) -> Result<(u64, u64)> {
+ let device = tunnel.device_proxy(&*self.connection);
+ let tx_bytes = device.get(NM_DEVICE_STATISTICS, "TxBytes")?;
+ let rx_bytes = device.get(NM_DEVICE_STATISTICS, "RxBytes")?;
+
+ Ok((tx_bytes, rx_bytes))
+ }
+
+ fn wait_until_device_is_ready(&self, device: &dbus::Path<'_>) -> Result<()> {
+ let device_state = self.get_device_state(device)?;
+
+ if !device_is_ready(device_state) {
+ let deadline = Instant::now() + DEVICE_READY_TIMEOUT;
+
+ let mut match_rule = MatchRule::new_signal(NM_DEVICE, NM_DEVICE_STATE_CHANGED);
+
+ match_rule.path = Some(device.clone().into_static());
+ let device_state = Arc::new(AtomicU32::new(device_state));
+
+ {
+ let shared_device_state = device_state.clone();
+ let device_matcher = self
+ .connection
+ .add_match(
+ match_rule,
+ move |state_change: DeviceStateChange, _connection, _message| {
+ log::debug!("Received new tunnel state change: {:?}", state_change);
+ let new_state = state_change.new_state;
+ shared_device_state.store(new_state, Ordering::Release);
+ true
+ },
+ )
+ .map_err(Error::Dbus)?;
+ while Instant::now() < deadline
+ && !device_is_ready(device_state.load(Ordering::Acquire))
+ {
+ if let Err(err) = self.connection.process(RPC_TIMEOUT) {
+ log::error!(
+ "DBus connection failed while waiting for device to be ready: {}",
+ err
+ );
+ }
+ }
+
+ if let Err(err) = self.connection.remove_match(device_matcher) {
+ log::error!("Failed to remove match from DBus connection: {}", err);
+ }
+ }
+
+ let final_device_state = device_state.load(Ordering::Acquire);
+ if !device_is_ready(final_device_state) {
+ return Err(Error::DeviceNotReady(final_device_state));
+ }
+ }
+ Ok(())
+ }
+
+ pub fn remove_tunnel(&self, tunnel: WireguardTunnel) -> Result<()> {
+ let deactivation_result: Result<()> = self
+ .nm_manager()
+ .method_call(
+ NM_MANAGER,
+ "DeactivateConnection",
+ (&tunnel.connection_path,),
+ )
+ .map_err(Error::Dbus);
+
+ let config_result: Result<()> = tunnel
+ .config_proxy(&*self.connection)
+ .method_call(NM_SETTINGS_CONNECTION_INTERFACE, "Delete", ())
+ .map_err(Error::Dbus);
+ deactivation_result?;
+ config_result?;
+ Ok(())
+ }
+
+ /// Ensures NetworkManager's connectivity check is disabled and returns the connectivity check
+ /// previous state. Returns true only if the connectivity check was enabled and is now
+ /// disabled. Disabling the connectivity check should be done before a firewall is applied
+ /// due to the fact that blocking DNS requests can make it hang:
+ /// https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/404
+ pub fn disable_connectivity_check(&self) -> Option<bool> {
+ let nm_manager = self.nm_manager();
+ match nm_manager.get(NM_MANAGER, CONNECTIVITY_CHECK_KEY) {
+ Ok(true) => {
+ if let Err(err) = nm_manager.set(NM_MANAGER, CONNECTIVITY_CHECK_KEY, false) {
+ log::error!(
+ "Failed to disable NetworkManager connectivity check: {}",
+ err
+ );
+ Some(false)
+ } else {
+ Some(true)
+ }
+ }
+ Ok(false) => Some(false),
+ Err(_) => None,
+ }
+ }
+
+ /// Enabled NetworkManager's connectivity check. Fails silently.
+ pub fn enable_connectivity_check(&self) {
+ if let Err(err) = self
+ .nm_manager()
+ .set(NM_MANAGER, CONNECTIVITY_CHECK_KEY, true)
+ {
+ log::error!("Failed to reset NetworkManager connectivity check: {}", err);
+ }
+ }
+
+ fn nm_manager(&self) -> Proxy<'_, &SyncConnection> {
+ Proxy::new(NM_BUS, NM_MANAGER_PATH, RPC_TIMEOUT, &*self.connection)
+ }
+
+ fn convert_config_to_dbus(config: &WireguardConfig) -> DeviceConfig {
+ let mut ipv6_config: VariantMap = HashMap::new();
+ let mut ipv4_config: VariantMap = HashMap::new();
+ let mut wireguard_config: VariantMap = HashMap::new();
+ let mut connection_config: VariantMap = HashMap::new();
+ let mut peer_configs = vec![];
+
+ wireguard_config.insert("mtu".into(), Variant(Box::new(config.mtu as u32)));
+ wireguard_config.insert("fwmark".into(), Variant(Box::new(config.fwmark as u32)));
+ wireguard_config.insert("peer-routes".into(), Variant(Box::new(false)));
+ wireguard_config.insert(
+ "private-key".into(),
+ Variant(Box::new(config.tunnel.private_key.to_base64())),
+ );
+ wireguard_config.insert("private-key-flags".into(), Variant(Box::new(0x0u32)));
+
+ for peer in config.peers.iter() {
+ let mut peer_config: VariantMap = HashMap::new();
+ let allowed_ips = peer
+ .allowed_ips
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<_>>();
+
+
+ peer_config.insert("allowed-ips".into(), Variant(Box::new(allowed_ips)));
+ peer_config.insert(
+ "endpoint".into(),
+ Variant(Box::new(peer.endpoint.to_string())),
+ );
+ peer_config.insert(
+ "public-key".into(),
+ Variant(Box::new(peer.public_key.to_base64())),
+ );
+
+ peer_configs.push(peer_config);
+ }
+ wireguard_config.insert("peers".into(), Variant(Box::new(peer_configs)));
+
+ connection_config.insert("type".into(), Variant(Box::new("wireguard".to_string())));
+ connection_config.insert(
+ "id".into(),
+ Variant(Box::new(MULLVAD_INTERFACE_NAME.to_string())),
+ );
+ connection_config.insert(
+ "interface-name".into(),
+ Variant(Box::new(MULLVAD_INTERFACE_NAME.to_string())),
+ );
+ connection_config.insert("autoconnect".into(), Variant(Box::new(true)));
+
+
+ let ipv4_addrs: Vec<_> = config
+ .tunnel
+ .addresses
+ .iter()
+ .filter(|ip| ip.is_ipv4())
+ .map(Self::convert_address_to_dbus)
+ .collect();
+
+ let ipv6_addrs: Vec<_> = config
+ .tunnel
+ .addresses
+ .iter()
+ .filter(|ip| ip.is_ipv6())
+ .map(Self::convert_address_to_dbus)
+ .collect();
+
+ ipv4_config.insert("address-data".into(), Variant(Box::new(ipv4_addrs)));
+ ipv4_config.insert("ignore-auto-routes".into(), Variant(Box::new(true)));
+ ipv4_config.insert("ignore-auto-dns".into(), Variant(Box::new(true)));
+ ipv4_config.insert("may-fail".into(), Variant(Box::new(true)));
+ ipv4_config.insert("method".into(), Variant(Box::new("manual".to_string())));
+ ipv4_config.insert("never-default".into(), Variant(Box::new(true)));
+
+ if !ipv6_addrs.is_empty() {
+ ipv6_config.insert("method".into(), Variant(Box::new("manual".to_string())));
+ ipv6_config.insert("address-data".into(), Variant(Box::new(ipv6_addrs)));
+ ipv6_config.insert("ignore-auto-routes".into(), Variant(Box::new(true)));
+ ipv6_config.insert("ignore-auto-dns".into(), Variant(Box::new(true)));
+ ipv6_config.insert("may-fail".into(), Variant(Box::new(true)));
+ }
+
+
+ let mut settings = HashMap::new();
+ settings.insert("ipv4".into(), ipv4_config);
+ if !ipv6_config.is_empty() {
+ settings.insert("ipv6".into(), ipv6_config);
+ }
+ settings.insert("wireguard".into(), wireguard_config);
+ settings.insert("connection".into(), connection_config);
+
+ settings
+ }
+
+ fn convert_address_to_dbus(address: &IpAddr) -> VariantMap {
+ let mut map: VariantMap = HashMap::new();
+ map.insert(
+ "address".to_string(),
+ Variant(Box::new(address.to_string())),
+ );
+ let prefix: u32 = if address.is_ipv4() { 32 } else { 128 };
+ map.insert("prefix".to_string(), Variant(Box::new(prefix)));
+
+ map
+ }
+
+ pub fn ensure_network_manager_exists(&self) -> Result<()> {
+ match self
+ .as_manager()
+ .get::<Box<dyn RefArg>>(&NM_MANAGER, "Version")
+ {
+ Ok(_) => Ok(()),
+ Err(err) => {
+ log::error!("Failed to read version of NetworkManager {}", err);
+ Err(Error::NetworkManagerNotDetected)
+ }
+ }
+ }
+
+ pub fn ensure_resolv_conf_is_managed(&self) -> Result<()> {
+ // check if NM is set to manage resolv.conf
+ let management_mode: String = self
+ .as_dns_manager()
+ .get(NM_DNS_MANAGER, RC_MANAGEMENT_MODE_KEY)?;
+ if management_mode == "unmanaged" {
+ return Err(Error::NetworkManagerNotManagingDns);
+ }
+
+ let dns_mode: String = self
+ .as_dns_manager()
+ .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<'a>(&'a self) -> Proxy<'a, &SyncConnection> {
+ Proxy::new(NM_BUS, NM_MANAGER_PATH, RPC_TIMEOUT, &*self.connection)
+ }
+
+ fn as_dns_manager<'a>(&'a self) -> Proxy<'a, &SyncConnection> {
+ Proxy::new(NM_BUS, NM_DNS_MANAGER_PATH, RPC_TIMEOUT, &*self.connection)
+ }
+
+ fn as_path<'a>(&'a self, device: &'a dbus::Path<'a>) -> Proxy<'a, &SyncConnection> {
+ Proxy::new(NM_BUS, device, RPC_TIMEOUT, &*self.connection)
+ }
+
+ pub fn set_dns(&mut self, interface_name: &str, servers: &[IpAddr]) -> Result<DeviceConfig> {
+ let device_path = self.fetch_device(interface_name)?;
+ self.wait_until_device_is_ready(&device_path)?;
+
+
+ let device = self.as_path(&device_path);
+ // Get the last applied connection
+ let (mut settings, version_id): (
+ HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>,
+ u64,
+ ) = device.method_call(NM_DEVICE, "GetAppliedConnection", (0u32,))?;
+
+ // 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<'_> =
+ device.get(NM_DEVICE, "Ip4Config").map_err(Error::Dbus)?;
+
+ let device_routes: Vec<Vec<u32>> = self
+ .as_path(&device_ip4_config)
+ .get(NM_IP4_CONFIG, "Routes")
+ .map_err(Error::Dbus)?;
+
+ let device_route_data: Vec<HashMap<String, Variant<Box<dyn RefArg>>>> = self
+ .as_path(&device_ip4_config)
+ .get(NM_IP4_CONFIG, "RouteData")
+ .map_err(Error::Dbus)?;
+
+ ipv4_settings.insert("route-metric".to_string(), Variant(Box::new(0u32)));
+ ipv4_settings.insert("routes".to_string(), Variant(Box::new(device_routes)));
+ ipv4_settings.insert(
+ "route-data".to_string(),
+ Variant(Box::new(device_route_data)),
+ );
+ }
+
+ if let Some(ipv6_settings) = settings.get_mut("ipv6") {
+ let device_ip6_config: dbus::Path<'_> =
+ device.get(NM_DEVICE, "Ip6Config").map_err(Error::Dbus)?;
+
+ let device_addresses6: Vec<(Vec<u8>, u32, Vec<u8>)> = self
+ .as_path(&device_ip6_config)
+ .get(NM_IP6_CONFIG, "Addresses")
+ .map_err(Error::Dbus)?;
+
+ let device_routes6: Vec<(Vec<u8>, u32, Vec<u8>, u32)> = self
+ .as_path(&device_ip6_config)
+ .get(NM_IP6_CONFIG, "Routes")
+ .map_err(Error::Dbus)?;
+
+ let device_route6_data: Vec<HashMap<String, Variant<Box<dyn RefArg>>>> = self
+ .as_path(&device_ip6_config)
+ .get(NM_IP6_CONFIG, "RouteData")
+ .map_err(Error::Dbus)?;
+
+ ipv6_settings.insert("route-metric".to_string(), Variant(Box::new(0u32)));
+ ipv6_settings.insert("routes".to_string(), Variant(Box::new(device_routes6)));
+ ipv6_settings.insert(
+ "route-data".to_string(),
+ 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".to_string(),
+ 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".to_string(),
+ Variant(Box::new(crate::linux::TUNNEL_FW_MARK) as Box<dyn RefArg>),
+ );
+ }
+
+ self.reapply_settings(&device_path, settings, version_id)?;
+ Ok(settings_backup)
+ }
+
+ pub fn reapply_settings<Settings: arg::Append>(
+ &self,
+ device: &dbus::Path<'_>,
+ settings: Settings,
+ version_id: u64,
+ ) -> Result<()> {
+ self.as_path(device)
+ .method_call(NM_DEVICE, "Reapply", (settings, version_id, 0u32))?;
+ Ok(())
+ }
+
+ fn update_dns_config<'a, T>(
+ settings: &mut HashMap<String, HashMap<String, 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.to_string(), HashMap::new());
+ settings.get_mut(ip_protocol).unwrap()
+ }
+ };
+
+ settings.insert(
+ "method".to_string(),
+ Variant(Box::new("manual".to_string())),
+ );
+ settings.insert(
+ "dns-priority".to_string(),
+ Variant(Box::new(DNS_FIRST_PRIORITY)),
+ );
+ settings.insert("dns".to_string(), Variant(Box::new(servers)));
+ settings.insert(
+ "dns-search".to_string(),
+ Variant(Box::new(vec!["~.".to_string()])),
+ );
+ }
+
+ pub fn fetch_device(&self, interface_name: &str) -> Result<dbus::Path<'static>> {
+ let devices: Box<dyn RefArg> = self
+ .as_manager()
+ .get(NM_MANAGER, "Devices")
+ .map_err(Error::Dbus)?;
+ let mut iter = devices
+ .as_iter()
+ .ok_or(Error::ObtainDevices)?
+ .map(|device| device.box_clone());
+
+ while let Some(device_item) = iter.next() {
+ // Copy due to lifetime weirdness
+ let device_path = device_item
+ .as_any()
+ .downcast_ref::<dbus::Path<'_>>()
+ .ok_or(Error::ObtainDevices)?;
+
+ let device_name: String = self
+ .as_path(&device_path)
+ .get(NM_DEVICE, "Interface")
+ .map_err(Error::Dbus)?;
+
+ if device_name != interface_name {
+ continue;
+ }
+
+ return Ok(device_path.clone());
+ }
+ Err(Error::DeviceNotFound)
+ }
+}
+
+#[derive(Debug)]
+struct DeviceStateChange {
+ new_state: u32,
+ old_state: u32,
+ reason: u32,
+}
+
+impl arg::ReadAll for DeviceStateChange {
+ fn read(i: &mut arg::Iter<'_>) -> std::result::Result<Self, arg::TypeMismatchError> {
+ Ok(DeviceStateChange {
+ new_state: i.read()?,
+ old_state: i.read()?,
+ reason: i.read()?,
+ })
+ }
+}
+
+impl dbus::message::SignalArgs for DeviceStateChange {
+ const NAME: &'static str = NM_DEVICE_STATE_CHANGED;
+ const INTERFACE: &'static str = NM_DEVICE;
+}
+
+#[derive(Debug)]
+pub struct WireguardTunnel {
+ config_path: dbus::Path<'static>,
+ connection_path: dbus::Path<'static>,
+ device_path: dbus::Path<'static>,
+}
+
+impl WireguardTunnel {
+ fn device_proxy<'a>(&'a self, connection: &'a SyncConnection) -> Proxy<'a, &SyncConnection> {
+ Proxy::new(NM_BUS, &self.device_path, RPC_TIMEOUT, connection)
+ }
+
+ fn config_proxy<'a>(&'a self, connection: &'a SyncConnection) -> Proxy<'a, &SyncConnection> {
+ Proxy::new(NM_BUS, &self.config_path, RPC_TIMEOUT, connection)
+ }
+}
+
+
+pub 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)
+}
+
+// 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 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,
+ })
+}
+
+/// 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: &SyncConnection) -> bool {
+ let check_nm = || -> std::result::Result<bool, dbus::Error> {
+ let dns_manager = Proxy::new(NM_BUS, NM_DNS_MANAGER_PATH, RPC_TIMEOUT, connection);
+ let dns_mode: String = dns_manager.get(NM_DNS_MANAGER, DNS_MODE_KEY)?;
+ if &dns_mode != "systemd-resolved" {
+ return Ok(false);
+ }
+
+
+ let rc_management_mode: String = dns_manager.get(NM_DNS_MANAGER, RC_MANAGEMENT_MODE_KEY)?;
+
+ 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" => {
+ Proxy::new(NM_BUS, NM_MANAGER_PATH, RPC_TIMEOUT, connection).method_call(
+ NM_MANAGER,
+ "Reload",
+ (0x02u32,),
+ )?;
+ 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
+ }
+ }
+}