summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2026-04-10 11:02:20 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2026-04-11 13:32:26 +0200
commit0aac70899b34b5cdf85629a36b55c5fa794fea3c (patch)
treec5af3f449759af9e9a29bf2a1a119ea38f1a745a
parenta99cd1f73beb1a7835564dd2db86468c86c77807 (diff)
downloadmullvadvpn-zbus.tar.xz
mullvadvpn-zbus.zip
WIP Port `network_manager` module to zbuszbus
Note that NetworkManager has supported WireGuard since 2019, so we don't bother porting the code checking for the freshness of NetworkManager on the system. Only port the WireGuard configuration for now. DNS will be done at a later time.
-rw-r--r--Cargo.toml2
-rw-r--r--talpid-core/src/tunnel_state_machine/mod.rs4
-rw-r--r--talpid-dbus/Cargo.toml2
-rw-r--r--talpid-dbus/src/lib.rs3
-rw-r--r--talpid-dbus/src/network_manager.rs756
-rw-r--r--talpid-dbus/src/network_manager/dbus_rs.rs753
-rw-r--r--talpid-dbus/src/network_manager/zbus.rs298
-rw-r--r--talpid-dns/Cargo.toml2
-rw-r--r--talpid-dns/src/linux/network_manager.rs6
-rw-r--r--talpid-platform-metadata/src/linux.rs2
-rw-r--r--talpid-wireguard/Cargo.toml2
-rw-r--r--talpid-wireguard/src/lib.rs12
-rw-r--r--talpid-wireguard/src/wireguard_kernel/nm_tunnel.rs153
13 files changed, 1155 insertions, 840 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 35e825dfb4..95b94ca1a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -109,7 +109,7 @@ sha2 = "0.10"
shadowsocks = "1.23.2"
socket2 = "0.5.7"
strum = { version = "0.27" }
-talpid-dbus = { path = "./talpid-dbus" }
+talpid-dbus = { path = "./talpid-dbus", default-features = false }
thiserror = "2.0"
tipsy = "0.7.0"
tokio = { version = "1.44" }
diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs
index fde54ecf08..1576d0a32c 100644
--- a/talpid-core/src/tunnel_state_machine/mod.rs
+++ b/talpid-core/src/tunnel_state_machine/mod.rs
@@ -672,7 +672,7 @@ impl SharedTunnelStateValues {
#[cfg(target_os = "linux")]
pub fn disable_connectivity_check(&mut self) {
if self.connectivity_check_was_enabled.is_none() {
- if let Ok(nm) = talpid_dbus::network_manager::NetworkManager::new() {
+ if let Ok(nm) = talpid_dbus::network_manager::dbus_rs::NetworkManager::new() {
self.connectivity_check_was_enabled = nm.disable_connectivity_check();
}
} else {
@@ -684,7 +684,7 @@ impl SharedTunnelStateValues {
#[cfg(target_os = "linux")]
pub fn reset_connectivity_check(&mut self) {
if self.connectivity_check_was_enabled.take() == Some(true) {
- if let Ok(nm) = talpid_dbus::network_manager::NetworkManager::new() {
+ if let Ok(nm) = talpid_dbus::network_manager::dbus_rs::NetworkManager::new() {
nm.enable_connectivity_check();
}
} else {
diff --git a/talpid-dbus/Cargo.toml b/talpid-dbus/Cargo.toml
index d91d4ac04a..df351d3b27 100644
--- a/talpid-dbus/Cargo.toml
+++ b/talpid-dbus/Cargo.toml
@@ -16,7 +16,7 @@ zbus = { version = "5.12", optional = true }
zvariant = { version = "5.8.0", optional = true }
[features]
-default = ["zbus"]
+default = ["libdbus"]
libdbus = ["dep:dbus"]
zbus = ["dep:zbus", "dep:zvariant"] # experimental: Prefer zbus over libdbus
diff --git a/talpid-dbus/src/lib.rs b/talpid-dbus/src/lib.rs
index 260c6b0bc4..9dee681a7b 100644
--- a/talpid-dbus/src/lib.rs
+++ b/talpid-dbus/src/lib.rs
@@ -1,13 +1,14 @@
//! DBus system connection
#![cfg(target_os = "linux")]
-#[cfg(feature = "libdbus")] // TODO: implement network_manager using zbus
pub mod network_manager;
pub mod systemd;
pub mod systemd_resolved;
#[cfg(feature = "libdbus")]
pub use dbus;
+#[cfg(feature = "zbus")]
+pub use zbus;
#[cfg(feature = "libdbus")]
use std::sync::{Arc, LazyLock, Mutex};
diff --git a/talpid-dbus/src/network_manager.rs b/talpid-dbus/src/network_manager.rs
index 623aab3326..d87f4e5c13 100644
--- a/talpid-dbus/src/network_manager.rs
+++ b/talpid-dbus/src/network_manager.rs
@@ -1,754 +1,6 @@
//! NetworkManager is the one-stop-shop of network configuration on Linux.
-use super::systemd_resolved;
-pub use dbus::arg::{RefArg, Variant};
-use dbus::{
- arg,
- blocking::{Proxy, SyncConnection, stdintf::org_freedesktop_dbus::Properties},
- message::MatchRule,
-};
-use std::{
- collections::HashMap,
- fs::File,
- io::{BufRead, BufReader},
- net::IpAddr,
- path::Path,
- sync::{
- Arc,
- atomic::{AtomicU32, Ordering},
- },
- 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_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 MAXIMUM_SUPPORTED_MAJOR_VERSION: u32 = 1;
-const MAXIMUM_SUPPORTED_MINOR_VERSION: u32 = 26;
-
-const NM_DEVICE_STATE_CHANGED: &str = "StateChanged";
-
-pub type Result<T> = std::result::Result<T, Error>;
-type NetworkSettings<'a> = HashMap<String, HashMap<String, Variant<Box<dyn RefArg + 'a>>>>;
-
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
- #[error("Error while communicating over Dbus")]
- Dbus(#[from] dbus::Error),
-
- #[error("Failed to match the returned D-Bus object with expected type")]
- MatchDBusTypeError(#[from] dbus::arg::TypeMismatchError),
-
- #[error(
- "NM is configured to manage DNS via systemd-resolved but systemd-resolved is not managing /etc/resolv.conf: {0}"
- )]
- SystemdResolvedNotManagingResolvconf(systemd_resolved::Error),
-
- #[error("Configuration has no device associated to it")]
- NoDevice,
-
- #[error("NetworkManager is too old: {0}.{1}")]
- NMTooOld(u32, u32),
-
- #[error("NetworkManager is too new to manage DNS: {0}.{1}")]
- NMTooNewFroDns(u32, u32),
-
- #[error("Failed to parse NetworkManager version string: {0}")]
- ParseNmVersionError(String),
-
- #[error("Device inactive: {0}")]
- DeviceNotReady(u32),
-
- #[error("Device not found")]
- DeviceNotFound,
-
- #[error("NetworkManager not detected")]
- NetworkManagerNotDetected,
-
- #[error("NetworkManager is using dnsmasq to manage DNS")]
- UsingDnsmasq,
-
- #[error("NetworkManager is too old: {0}")]
- TooOldNetworkManager(String),
-
- #[error("NetworkManager is not managing DNS")]
- NetworkManagerNotManagingDns,
-
- #[error("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: crate::get_connection()?,
- })
- }
-
- pub fn create_wg_tunnel(&self, config: &DeviceConfig) -> Result<WireguardTunnel> {
- self.nm_supports_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: &DeviceConfig) -> Result<WireguardTunnel> {
- let config_path: dbus::Path<'static> = match self.add_connection_2(config) {
- Ok((path, _result)) => path,
- Err(Error::Dbus(dbus_error)) if dbus_error.name() == Some(DBUS_UNKNOWN_METHOD) => {
- self.add_connection_unsaved(config)?.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,
- })
- }
-
- pub fn nm_supports_wireguard(&self) -> Result<()> {
- let (major, minor) = self.version()?;
- Self::ensure_nm_is_new_enough_for_wireguard(major, minor)?;
- Self::ensure_nm_is_old_enough_for_dns(major, minor)
- }
-
- pub fn nm_version_dns_works(&self) -> Result<()> {
- let (major, minor) = self.version()?;
- Self::ensure_nm_is_old_enough_for_dns(major, minor)
- }
-
- pub fn version_string(&self) -> Result<String> {
- let manager = self.nm_manager();
- manager.get(NM_MANAGER, "Version").map_err(Error::Dbus)
- }
-
- fn ensure_nm_is_new_enough_for_wireguard(major: u32, minor: u32) -> Result<()> {
- if major < MINIMUM_SUPPORTED_MAJOR_VERSION
- || (minor < MINIMUM_SUPPORTED_MINOR_VERSION && major == MINIMUM_SUPPORTED_MAJOR_VERSION)
- {
- Err(Error::NMTooOld(major, minor))
- } else {
- Ok(())
- }
- }
-
- fn ensure_nm_is_old_enough_for_dns(major_version: u32, minor_version: u32) -> Result<()> {
- if major_version > MAXIMUM_SUPPORTED_MAJOR_VERSION
- || (minor_version > MAXIMUM_SUPPORTED_MINOR_VERSION
- && major_version >= MAXIMUM_SUPPORTED_MAJOR_VERSION)
- {
- Err(Error::NMTooNewFroDns(major_version, minor_version))
- } else {
- Ok(())
- }
- }
-
- fn version(&self) -> Result<(u32, u32)> {
- let version = self.version_string()?;
- Self::parse_nm_version(&version).ok_or(Error::ParseNmVersionError(version))
- }
-
- fn parse_nm_version(version: &str) -> Option<(u32, u32)> {
- let mut parts = version.split('.').map(|part| part.parse().ok());
-
- let major_version: u32 = parts.next()??;
- let minor_version: u32 = parts.next()??;
- Some((major_version, minor_version))
- }
-
- 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)
- }
-
- 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)
- }
-
- 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_can_be_used_to_manage_dns(&self) -> Result<()> {
- self.ensure_resolv_conf_is_managed()?;
- self.ensure_network_manager_exists()?;
- self.nm_version_dns_works()?;
- Ok(())
- }
- 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);
- }
-
- 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)
- .map_err(Error::Dbus)?;
-
- match dns_mode.as_ref() {
- // NM can't setup config for multiple interfaces with dnsmasq
- "dnsmasq" => return Err(Error::UsingDnsmasq),
- // 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) -> Proxy<'_, &SyncConnection> {
- Proxy::new(NM_BUS, NM_MANAGER_PATH, RPC_TIMEOUT, &*self.connection)
- }
-
- fn as_dns_manager(&'_ self) -> Proxy<'_, &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, &'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): (NetworkSettings<'_>, 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.clone(), Variant(variant.0.box_clone()));
- }
- settings_backup.insert(top_key.clone(), 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).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.contains_key("fwmark")
- {
- log::error!("WireGuard config doesn't contain the firewall mark");
- }
-
- 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 NetworkSettings<'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 iter = devices
- .as_iter()
- .ok_or(Error::ObtainDevices)?
- .map(|device| device.box_clone());
-
- for device_item in iter {
- // 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)
- }
-
- pub 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
- }
-}
-
-#[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, &'a SyncConnection> {
- Proxy::new(NM_BUS, &self.device_path, RPC_TIMEOUT, connection)
- }
-
- fn config_proxy<'a>(&'a self, connection: &'a SyncConnection) -> Proxy<'a, &'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 expects 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 to 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 to 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,
- })
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_valid_versions() {
- NetworkManager::ensure_nm_is_new_enough_for_wireguard(1, 16).unwrap();
- NetworkManager::ensure_nm_is_old_enough_for_dns(1, 26).unwrap();
- assert!(NetworkManager::ensure_nm_is_new_enough_for_wireguard(1, 14).is_err());
- assert!(NetworkManager::ensure_nm_is_old_enough_for_dns(1, 28).is_err());
- }
-}
+#[cfg(feature = "libdbus")]
+pub mod dbus_rs;
+#[cfg(feature = "zbus")]
+pub mod zbus;
diff --git a/talpid-dbus/src/network_manager/dbus_rs.rs b/talpid-dbus/src/network_manager/dbus_rs.rs
new file mode 100644
index 0000000000..b15dd7b45e
--- /dev/null
+++ b/talpid-dbus/src/network_manager/dbus_rs.rs
@@ -0,0 +1,753 @@
+use crate::systemd_resolved;
+pub use dbus::arg::{RefArg, Variant};
+use dbus::{
+ arg,
+ blocking::{Proxy, SyncConnection, stdintf::org_freedesktop_dbus::Properties},
+ message::MatchRule,
+};
+use std::{
+ collections::HashMap,
+ fs::File,
+ io::{BufRead, BufReader},
+ net::IpAddr,
+ path::Path,
+ sync::{
+ Arc,
+ atomic::{AtomicU32, Ordering},
+ },
+ 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_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 MAXIMUM_SUPPORTED_MAJOR_VERSION: u32 = 1;
+const MAXIMUM_SUPPORTED_MINOR_VERSION: u32 = 26;
+
+const NM_DEVICE_STATE_CHANGED: &str = "StateChanged";
+
+pub type Result<T> = std::result::Result<T, Error>;
+type NetworkSettings<'a> = HashMap<String, HashMap<String, Variant<Box<dyn RefArg + 'a>>>>;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Error while communicating over Dbus")]
+ Dbus(#[from] dbus::Error),
+
+ #[error("Failed to match the returned D-Bus object with expected type")]
+ MatchDBusTypeError(#[from] dbus::arg::TypeMismatchError),
+
+ #[error(
+ "NM is configured to manage DNS via systemd-resolved but systemd-resolved is not managing /etc/resolv.conf: {0}"
+ )]
+ SystemdResolvedNotManagingResolvconf(systemd_resolved::Error),
+
+ #[error("Configuration has no device associated to it")]
+ NoDevice,
+
+ #[error("NetworkManager is too old: {0}.{1}")]
+ NMTooOld(u32, u32),
+
+ #[error("NetworkManager is too new to manage DNS: {0}.{1}")]
+ NMTooNewFroDns(u32, u32),
+
+ #[error("Failed to parse NetworkManager version string: {0}")]
+ ParseNmVersionError(String),
+
+ #[error("Device inactive: {0}")]
+ DeviceNotReady(u32),
+
+ #[error("Device not found")]
+ DeviceNotFound,
+
+ #[error("NetworkManager not detected")]
+ NetworkManagerNotDetected,
+
+ #[error("NetworkManager is using dnsmasq to manage DNS")]
+ UsingDnsmasq,
+
+ #[error("NetworkManager is too old: {0}")]
+ TooOldNetworkManager(String),
+
+ #[error("NetworkManager is not managing DNS")]
+ NetworkManagerNotManagingDns,
+
+ #[error("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: crate::get_connection()?,
+ })
+ }
+
+ pub fn create_wg_tunnel(&self, config: &DeviceConfig) -> Result<WireguardTunnel> {
+ self.nm_supports_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: &DeviceConfig) -> Result<WireguardTunnel> {
+ let config_path: dbus::Path<'static> = match self.add_connection_2(config) {
+ Ok((path, _result)) => path,
+ Err(Error::Dbus(dbus_error)) if dbus_error.name() == Some(DBUS_UNKNOWN_METHOD) => {
+ self.add_connection_unsaved(config)?.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,
+ })
+ }
+
+ pub fn nm_supports_wireguard(&self) -> Result<()> {
+ let (major, minor) = self.version()?;
+ Self::ensure_nm_is_new_enough_for_wireguard(major, minor)?;
+ Self::ensure_nm_is_old_enough_for_dns(major, minor)
+ }
+
+ pub fn nm_version_dns_works(&self) -> Result<()> {
+ let (major, minor) = self.version()?;
+ Self::ensure_nm_is_old_enough_for_dns(major, minor)
+ }
+
+ pub fn version_string(&self) -> Result<String> {
+ let manager = self.nm_manager();
+ manager.get(NM_MANAGER, "Version").map_err(Error::Dbus)
+ }
+
+ fn ensure_nm_is_new_enough_for_wireguard(major: u32, minor: u32) -> Result<()> {
+ if major < MINIMUM_SUPPORTED_MAJOR_VERSION
+ || (minor < MINIMUM_SUPPORTED_MINOR_VERSION && major == MINIMUM_SUPPORTED_MAJOR_VERSION)
+ {
+ Err(Error::NMTooOld(major, minor))
+ } else {
+ Ok(())
+ }
+ }
+
+ fn ensure_nm_is_old_enough_for_dns(major_version: u32, minor_version: u32) -> Result<()> {
+ if major_version > MAXIMUM_SUPPORTED_MAJOR_VERSION
+ || (minor_version > MAXIMUM_SUPPORTED_MINOR_VERSION
+ && major_version >= MAXIMUM_SUPPORTED_MAJOR_VERSION)
+ {
+ Err(Error::NMTooNewFroDns(major_version, minor_version))
+ } else {
+ Ok(())
+ }
+ }
+
+ fn version(&self) -> Result<(u32, u32)> {
+ let version = self.version_string()?;
+ Self::parse_nm_version(&version).ok_or(Error::ParseNmVersionError(version))
+ }
+
+ fn parse_nm_version(version: &str) -> Option<(u32, u32)> {
+ let mut parts = version.split('.').map(|part| part.parse().ok());
+
+ let major_version: u32 = parts.next()??;
+ let minor_version: u32 = parts.next()??;
+ Some((major_version, minor_version))
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ 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_can_be_used_to_manage_dns(&self) -> Result<()> {
+ self.ensure_resolv_conf_is_managed()?;
+ self.ensure_network_manager_exists()?;
+ self.nm_version_dns_works()?;
+ Ok(())
+ }
+ 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);
+ }
+
+ 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)
+ .map_err(Error::Dbus)?;
+
+ match dns_mode.as_ref() {
+ // NM can't setup config for multiple interfaces with dnsmasq
+ "dnsmasq" => return Err(Error::UsingDnsmasq),
+ // 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) -> Proxy<'_, &SyncConnection> {
+ Proxy::new(NM_BUS, NM_MANAGER_PATH, RPC_TIMEOUT, &*self.connection)
+ }
+
+ fn as_dns_manager(&'_ self) -> Proxy<'_, &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, &'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): (NetworkSettings<'_>, 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.clone(), Variant(variant.0.box_clone()));
+ }
+ settings_backup.insert(top_key.clone(), 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).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.contains_key("fwmark")
+ {
+ log::error!("WireGuard config doesn't contain the firewall mark");
+ }
+
+ 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 NetworkSettings<'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 iter = devices
+ .as_iter()
+ .ok_or(Error::ObtainDevices)?
+ .map(|device| device.box_clone());
+
+ for device_item in iter {
+ // 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)
+ }
+
+ pub 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
+ }
+}
+
+#[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, &'a SyncConnection> {
+ Proxy::new(NM_BUS, &self.device_path, RPC_TIMEOUT, connection)
+ }
+
+ fn config_proxy<'a>(&'a self, connection: &'a SyncConnection) -> Proxy<'a, &'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 expects 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 to 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 to 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,
+ })
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_valid_versions() {
+ NetworkManager::ensure_nm_is_new_enough_for_wireguard(1, 16).unwrap();
+ NetworkManager::ensure_nm_is_old_enough_for_dns(1, 26).unwrap();
+ assert!(NetworkManager::ensure_nm_is_new_enough_for_wireguard(1, 14).is_err());
+ assert!(NetworkManager::ensure_nm_is_old_enough_for_dns(1, 28).is_err());
+ }
+}
diff --git a/talpid-dbus/src/network_manager/zbus.rs b/talpid-dbus/src/network_manager/zbus.rs
new file mode 100644
index 0000000000..6bea3ce4eb
--- /dev/null
+++ b/talpid-dbus/src/network_manager/zbus.rs
@@ -0,0 +1,298 @@
+use std::collections::HashMap;
+
+use zbus::blocking::Connection;
+use zvariant::{OwnedObjectPath, OwnedValue};
+
+// TODO: Inline
+type O = OwnedObjectPath;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Failed to create a DBus connection")]
+ Connect(#[source] zbus::Error),
+ // A Proxy is a helper to interact with an interface on a remote object.
+ #[error("Failed to create proxy for object {0}")]
+ Proxy(#[source] zbus::Error),
+ // Failed to call some method on a proxy.
+ #[error("Failed to call method on proxy {0}")]
+ Call(#[source] zbus::Error),
+}
+
+#[derive(Clone)]
+pub struct NetworkManager {
+ pub dbus_connection: Connection,
+}
+
+impl NetworkManager {
+ pub fn new() -> Result<Self, Error> {
+ let dbus_connection = crate::get_connection_zbus().map_err(Error::Connect)?;
+ let network_manager = NetworkManager { dbus_connection };
+ // TODO: Assert a recent-enough version of network manager is installed. Not even sure if
+ // necessary.
+ network_manager.ensure_can_be_used_to_manage_dns()?;
+
+ Ok(network_manager)
+ }
+
+ /// Check if NetworkManager is capable of managing DNS.
+ pub fn ensure_can_be_used_to_manage_dns(&self) -> Result<bool, Error> {
+ Ok(true) // TODO: Add DNS capabilities.
+ }
+
+ pub fn create_wg_tunnel(&self, wg_config: DeviceConfig) -> Result<WireguardTunnel, Error> {
+ WireguardTunnel::new(self.clone(), wg_config)
+ }
+
+ fn activate_connection<'a>(
+ &self,
+ connection: OwnedObjectPath,
+ ) -> std::result::Result<ConnectionActiveProxyBlocking<'a>, Error> {
+ let device = OwnedObjectPath::try_from("/").unwrap(); // TODO: unwrap
+ let specified_object = OwnedObjectPath::try_from("/").unwrap(); // TODO: unwrap
+ as_nm(&self.dbus_connection)?
+ .activate_connection(connection, device, specified_object)
+ .map_err(Error::Call)
+ }
+
+ fn add_connection_2(
+ &self,
+ settings: DeviceConfig,
+ ) -> Result<(OwnedObjectPath, DeviceConfig), Error> {
+ // Blocks auto-connect on the new profile.
+ const NM_ADD_CONNECTION_VOLATILE: u32 = 0x2;
+ let flags = NM_ADD_CONNECTION_VOLATILE;
+ let args = HashMap::default();
+ as_settings(&self.dbus_connection)?
+ .add_connection_2(settings, flags, args)
+ .map_err(Error::Call)
+ }
+}
+
+fn as_settings<'a>(conn: &Connection) -> Result<NetworkManagerSettingsProxyBlocking<'a>, Error> {
+ NetworkManagerSettingsProxyBlocking::new(conn).map_err(Error::Proxy)
+}
+
+fn as_nm<'a>(conn: &Connection) -> Result<NetworkManagerProxyBlocking<'a>, Error> {
+ NetworkManagerProxyBlocking::new(conn).map_err(Error::Proxy)
+}
+
+/// NetworkManager device configurations. A map from device to configuration options.
+///
+/// # DBUS
+///
+/// This corresponds to the DBUS type signature `a{sa{sv}}`
+///
+/// ### Breakdown:
+///
+/// - `s` : `string`
+/// - `v` : `variant`
+/// - `a{}` : `map`
+/// - `a{sv}` : `map<string, variant>`
+/// - `a{sa{sv}}`: `map<string, map<string, variant>`
+pub type DeviceConfig = HashMap<String, HashMap<String, OwnedValue>>;
+
+#[derive(Debug)]
+/// TODO: Document
+pub struct WireguardTunnel {
+ /// TODO: Document
+ active: ConnectionActiveProxyBlocking<'static>,
+ /// TODO: Document
+ nm: NetworkManagerProxyBlocking<'static>,
+ /// TODO: Document
+ device: DeviceProxyBlocking<'static>,
+}
+
+impl WireguardTunnel {
+ pub fn new(nm: NetworkManager, wg_config: DeviceConfig) -> Result<Self, Error> {
+ let connection = nm
+ .add_connection_2(wg_config)
+ .inspect_err(|err| {
+ log::error!(
+ "Failed to create a new interface via AddConnection2: {}",
+ err
+ );
+ })?
+ .0;
+
+ let active = nm.activate_connection(connection)?;
+ let devices = active.devices().map_err(Error::Proxy)?;
+ // TODO: Why do we only consider the first device?
+ let device = devices.into_iter().next().unwrap();
+
+ let nm = as_nm(&nm.dbus_connection)?;
+
+ Ok(WireguardTunnel { active, nm, device })
+ }
+
+ pub fn remove(self) -> Result<(), Error> {
+ let device_object = OwnedObjectPath::from(self.device.into_inner().path().clone());
+ let deactivation_result = self
+ .nm
+ .deactivate_connection(device_object)
+ .map_err(Error::Call);
+
+ let config_result = self
+ .active
+ .connection()
+ .map_err(Error::Proxy)?
+ .delete()
+ .map_err(Error::Call);
+
+ deactivation_result?;
+ config_result?;
+ Ok(())
+ }
+
+ pub fn get_interface_name(&self) -> Result<String, Error> {
+ self.device.interface().map_err(Error::Call)
+ }
+}
+
+/// <https://people.freedesktop.org/~lkundrak/nm-docs/gdbus-org.freedesktop.NetworkManager>
+#[zbus::proxy(
+ interface = "org.freedesktop.NetworkManager",
+ default_service = "org.freedesktop.NetworkManager",
+ default_path = "/org/freedesktop/NetworkManager"
+)]
+trait NetworkManager {
+ /// org.freedesktop.NetworkManager.Settings.ActivateConnection
+ ///
+ /// # Input
+ /// - `connection`: The connection to activate.
+ ///
+ /// If "/" is given, a valid device path must be given, and NetworkManager picks the best connection to activate for
+ /// the given device. VPN connections must always pass a valid connection path.
+ ///
+ /// - `device`: The object path of device to be activated for physical connections.
+ ///
+ /// This parameter is ignored for VPN connections, because the specific_object (if provided) specifies the device
+ /// to use.
+ ///
+ /// - `specific_object`: The path of a connection-type-specific object this activation should use.
+ ///
+ /// For VPN connections, pass the object path of an ActiveConnection object that should serve as the "base"
+ /// connection (to which the VPN connections lifetime will be tied), or pass "/" and NM will automatically use the
+ /// current default device.
+ ///
+ /// # Result
+ ///
+ /// Object path of the active connection object representing this active connection.
+ ///
+ /// # Documenation
+ ///
+ /// https://people.freedesktop.org/~lkundrak/nm-docs/gdbus-org.freedesktop.NetworkManager.html#gdbus-method-org-freedesktop-NetworkManager.ActivateConnection
+ #[zbus(object = "ConnectionActive")]
+ fn activate_connection(&self, connection: O, device: O, specified_object: O);
+
+ fn deactivate_connection(&self, active: OwnedObjectPath) -> Result<(), zbus::Error>;
+
+ // methods
+ //
+ // GetDevices (OUT ao devices);
+ // GetAllDevices (OUT ao devices);
+ // GetDeviceByIpIface (IN s iface,
+ // OUT o device);
+ // ActivateConnection (IN o connection,
+ // IN o device,
+ // IN o specific_object,
+ // OUT o active_connection);
+ // AddAndActivateConnection (IN a{sa{sv}} connection,
+ // IN o device,
+ // IN o specific_object,
+ // OUT o path,
+ // OUT o active_connection);
+ // DeactivateConnection (IN o active_connection);
+ // Sleep (IN b sleep);
+ // Enable (IN b enable);
+ // GetPermissions (OUT a{ss} permissions);
+ // SetLogging (IN s level,
+ // IN s domains);
+ // GetLogging (OUT s level,
+ // OUT s domains);
+ // CheckConnectivity (OUT u connectivity);
+ // state (OUT u state);
+ //
+ // properties
+ //
+ // Devices readable ao
+ // AllDevices readable ao
+ // NetworkingEnabled readable b
+ // WirelessEnabled readwrite b
+ // WirelessHardwareEnabled readable b
+ // WwanEnabled readwrite b
+ // WwanHardwareEnabled readable b
+ // WimaxEnabled readwrite b
+ // WimaxHardwareEnabled readable b
+ // ActiveConnections readable ao
+ // PrimaryConnection readable o
+ // PrimaryConnectionType readable s
+ // Metered readable u
+ // ActivatingConnection readable o
+ // Startup readable b
+ // Version readable s
+ // State readable u
+ // Connectivity readable u
+ // GlobalDnsConfiguration readwrite a{sv}
+}
+
+/// https://networkmanager.pages.freedesktop.org/NetworkManager/NetworkManager/gdbus-org.freedesktop.NetworkManager.Settings
+#[zbus::proxy(
+ interface = "org.freedesktop.NetworkManager.Settings",
+ default_service = "org.freedesktop.NetworkManager.Settings",
+ default_path = "/org/freedesktop/NetworkManager/Settings"
+)]
+trait NetworkManagerSettings {
+ /// org.freedesktop.NetworkManager.Settings.AddConnection2 (since 1.20)
+ ///
+ /// # Result
+ /// A tuple of
+ /// - Object path of the new connection that was just added.
+ /// - "Output argument, currently no additional results are returned" (lol).
+ ///
+ /// # Documenation
+ ///
+ /// https://networkmanager.pages.freedesktop.org/NetworkManager/NetworkManager/gdbus-org.freedesktop.NetworkManager.Settings.html#gdbus-method-org-freedesktop-NetworkManager-Settings.AddConnection2
+ fn add_connection_2(
+ &self,
+ settings: DeviceConfig,
+ flags: u32,
+ args: HashMap<String, OwnedValue>,
+ ) -> Result<(OwnedObjectPath, DeviceConfig), zbus::Error>;
+}
+
+#[zbus::proxy(
+ interface = "org.freedesktop.NetworkManager",
+ default_service = "org.freedesktop.NetworkManager.Connection.Active", // TODO: Check if makes sense.
+ default_path = "/org/freedesktop/NetworkManager/Connection/Active"
+)]
+trait ConnectionActive {
+ #[zbus(object = "Connection")]
+ fn connection(&self);
+
+ /// Array of object paths representing devices which are part of this active connection.
+ ///
+ /// https://www.networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Connection.Active.html#gdbus-property-org-freedesktop-NetworkManager-Connection-Active.Devices
+ #[zbus(object = "Device", object_vec)]
+ fn devices(&self);
+}
+
+/// <https://www.networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Settings.Connection>
+#[zbus::proxy(
+ interface = "org.freedesktop.NetworkManager",
+ default_service = "org.freedesktop.NetworkManager.Settings.Connection", // TODO: Check if makes sense.
+ default_path = "/org/freedesktop/NetworkManager/Settings/Connection"
+)]
+trait Connection {
+ fn delete(&self) -> Result<(), zbus::Error>;
+}
+
+/// https://people.freedesktop.org/~lkundrak/nm-docs/gdbus-org.freedesktop.NetworkManager.Device
+#[zbus::proxy(
+ interface = "org.freedesktop.NetworkManager",
+ default_service = "org.freedesktop.NetworkManager.Device", // TODO: Check if makes sense.
+ default_path = "/org/freedesktop/NetworkManager/Device"
+)]
+trait Device {
+ #[zbus(property)]
+ fn interface(&self) -> Result<String, zbus::Error>;
+}
diff --git a/talpid-dns/Cargo.toml b/talpid-dns/Cargo.toml
index 386a32fc46..472aef7c2a 100644
--- a/talpid-dns/Cargo.toml
+++ b/talpid-dns/Cargo.toml
@@ -17,7 +17,7 @@ inotify = "0.10"
nix = { workspace = true, features = ["net"] }
parking_lot = "0.12.0"
resolv-conf = "0.7"
-talpid-dbus = { workspace = true }
+talpid-dbus = { workspace = true, features = ["libdbus"] }
talpid-routing = { path = "../talpid-routing" }
tokio = { workspace = true, features = ["macros"] }
triggered = "0.1.1"
diff --git a/talpid-dns/src/linux/network_manager.rs b/talpid-dns/src/linux/network_manager.rs
index 14e3fe7178..e1d06295a6 100644
--- a/talpid-dns/src/linux/network_manager.rs
+++ b/talpid-dns/src/linux/network_manager.rs
@@ -1,6 +1,8 @@
use std::net::IpAddr;
-pub use talpid_dbus::network_manager::Error;
-use talpid_dbus::network_manager::{self, DeviceConfig, NetworkManager as DBus};
+pub use talpid_dbus::network_manager::dbus_rs::Error;
+use talpid_dbus::network_manager::dbus_rs::{
+ self as network_manager, DeviceConfig, NetworkManager as DBus,
+};
pub type Result<T> = std::result::Result<T, Error>;
diff --git a/talpid-platform-metadata/src/linux.rs b/talpid-platform-metadata/src/linux.rs
index be3fafa6f3..3fea20ead7 100644
--- a/talpid-platform-metadata/src/linux.rs
+++ b/talpid-platform-metadata/src/linux.rs
@@ -97,7 +97,7 @@ fn kernel_version() -> Option<(String, String)> {
/// > 1.26.0
#[cfg(feature = "network-manager")]
fn nm_version() -> Option<(String, String)> {
- let nm = talpid_dbus::network_manager::NetworkManager::new().ok()?;
+ let nm = talpid_dbus::network_manager::dbus_rs::NetworkManager::new().ok()?;
Some(("nm".to_string(), nm.version_string().ok()?))
}
diff --git a/talpid-wireguard/Cargo.toml b/talpid-wireguard/Cargo.toml
index 97c5bcfc77..096c7f6d96 100644
--- a/talpid-wireguard/Cargo.toml
+++ b/talpid-wireguard/Cargo.toml
@@ -47,7 +47,7 @@ netlink-packet-core = { workspace = true }
netlink-packet-route = { workspace = true }
netlink-proto = { workspace = true }
rtnetlink = { workspace = true }
-talpid-dbus = { workspace = true }
+talpid-dbus = { workspace = true, features = ["zbus"] }
zerocopy = { workspace = true, features = ["derive"] }
[target.'cfg(unix)'.dependencies]
diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs
index 141146f531..e956c0b16e 100644
--- a/talpid-wireguard/src/lib.rs
+++ b/talpid-wireguard/src/lib.rs
@@ -1180,18 +1180,14 @@ pub enum TunnelError {
#[cfg(target_os = "linux")]
fn will_nm_manage_dns() -> bool {
- use talpid_dbus::network_manager::NetworkManager;
+ use talpid_dbus::{network_manager::zbus::NetworkManager, systemd_resolved::SystemdResolved};
- if talpid_dbus::systemd_resolved::SystemdResolved::new().is_ok() {
+ if SystemdResolved::new().is_ok() {
return false;
}
-
NetworkManager::new()
- .and_then(|nm| {
- nm.ensure_can_be_used_to_manage_dns()?;
- Ok(true)
- })
- .unwrap_or(false)
+ .and_then(|nm| nm.ensure_can_be_used_to_manage_dns())
+ .is_ok_and(|p| p)
}
// Set the MTU to the lowest possible whilst still allowing for IPv6 to help with wireless
diff --git a/talpid-wireguard/src/wireguard_kernel/nm_tunnel.rs b/talpid-wireguard/src/wireguard_kernel/nm_tunnel.rs
index c35503fa82..f57c82c009 100644
--- a/talpid-wireguard/src/wireguard_kernel/nm_tunnel.rs
+++ b/talpid-wireguard/src/wireguard_kernel/nm_tunnel.rs
@@ -6,27 +6,25 @@ use super::{
};
use futures::Future;
use std::{collections::HashMap, pin::Pin};
-use talpid_dbus::{
- dbus,
- network_manager::{
- DeviceConfig, Error as NetworkManagerError, NetworkManager, Variant, VariantMap,
- WireguardTunnel,
- },
+
+use talpid_dbus::network_manager::zbus::{
+ self as network_manager, DeviceConfig, NetworkManager, WireguardTunnel,
};
+use talpid_dbus::zbus;
+
use talpid_net::unix::iface_index;
use talpid_tunnel_config_client::DaitaSettings;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Error while communicating over Dbus")]
- Dbus(#[from] dbus::Error),
+ Dbus(#[from] zbus::Error),
#[error("NetworkManager error")]
- NetworkManager(#[from] NetworkManagerError),
+ NetworkManager(#[from] network_manager::Error),
}
pub struct NetworkManagerTunnel {
- network_manager: NetworkManager,
tunnel: Option<WireguardTunnel>,
netlink_connections: Handle,
interface_name: String,
@@ -40,22 +38,19 @@ impl NetworkManagerTunnel {
let network_manager = NetworkManager::new()
.map_err(Error::NetworkManager)
.map_err(WgKernelError::NetworkManager)?;
- let config_map = convert_config_to_dbus(config);
+ let wg_config = convert_config_to_dbus(config);
let tunnel = network_manager
- .create_wg_tunnel(&config_map)
+ .create_wg_tunnel(wg_config)
.map_err(|err| WgKernelError::NetworkManager(err.into()))?;
- let interface_name = match network_manager.get_interface_name(&tunnel) {
- Ok(name) => name,
- Err(error) => {
- log::error!("Failed to fetch interface name from NM: {}", error);
- MULLVAD_INTERFACE_NAME.to_string()
- }
- };
+ let interface_name = tunnel
+ .get_interface_name()
+ .inspect_err(|error| log::error!("Failed to fetch interface name from NM: {}", error))
+ .unwrap_or(MULLVAD_INTERFACE_NAME.to_string());
+
let netlink_connections = tokio_handle.block_on(Handle::connect())?;
Ok(NetworkManagerTunnel {
- network_manager,
tunnel: Some(tunnel),
netlink_connections,
interface_name,
@@ -70,13 +65,11 @@ impl Tunnel for NetworkManagerTunnel {
}
fn stop(mut self: Box<Self>) -> std::result::Result<(), TunnelError> {
- if let Some(tunnel) = self.tunnel.take() {
- if let Err(err) = self.network_manager.remove_tunnel(tunnel) {
- log::error!("Failed to remove WireGuard tunnel via NM: {}", err);
- Err(TunnelError::StopWireguardError(Box::new(err)))
- } else {
- Ok(())
- }
+ if let Some(tunnel) = self.tunnel.take()
+ && let Err(err) = tunnel.remove()
+ {
+ log::error!("Failed to remove WireGuard tunnel via NM: {}", err);
+ Err(TunnelError::StopWireguardError(Box::new(err)))
} else {
Ok(())
}
@@ -120,92 +113,112 @@ impl Tunnel for NetworkManagerTunnel {
}
fn convert_config_to_dbus(config: &Config) -> 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();
+ use zbus::zvariant::{OwnedValue, Value};
+ // let mut ipv6_config = HashMap::new();
+ // let mut ipv4_config = HashMap::new();
+ let mut wireguard_config: HashMap<String, OwnedValue> = HashMap::new();
+ let mut connection_config: HashMap<String, OwnedValue> = HashMap::new();
let mut peer_configs = vec![];
- wireguard_config.insert("mtu".into(), Variant(Box::new(config.mtu as u32)));
+ wireguard_config.insert("mtu".into(), OwnedValue::from(config.mtu));
if let Some(fwmark) = config.fwmark {
- wireguard_config.insert("fwmark".into(), Variant(Box::new(fwmark)));
+ wireguard_config.insert("fwmark".into(), OwnedValue::from(fwmark));
}
- wireguard_config.insert("peer-routes".into(), Variant(Box::new(false)));
+ wireguard_config.insert("peer-routes".into(), OwnedValue::from(false));
+
wireguard_config.insert(
"private-key".into(),
- Variant(Box::new(config.tunnel.private_key.to_base64())),
+ Value::new(config.tunnel.private_key.to_base64())
+ .try_to_owned()
+ .unwrap(),
);
- wireguard_config.insert("private-key-flags".into(), Variant(Box::new(0x0u32)));
+ wireguard_config.insert("private-key-flags".into(), OwnedValue::from(0x0u32));
for peer in config.peers() {
- let mut peer_config: VariantMap = HashMap::new();
+ let mut peer_config: HashMap<String, OwnedValue> = 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(
+ "allowed-ips".into(),
+ Value::new(allowed_ips).try_to_owned().unwrap(),
+ );
peer_config.insert(
"endpoint".into(),
- Variant(Box::new(peer.endpoint.to_string())),
+ Value::new(peer.endpoint.to_string())
+ .try_to_owned()
+ .unwrap(),
);
peer_config.insert(
"public-key".into(),
- Variant(Box::new(peer.public_key.to_base64())),
+ Value::new(peer.public_key.to_base64())
+ .try_to_owned()
+ .unwrap(),
);
peer_configs.push(peer_config);
}
- wireguard_config.insert("peers".into(), Variant(Box::new(peer_configs)));
+ wireguard_config.insert(
+ "peers".into(),
+ Value::new(peer_configs).try_to_owned().unwrap(),
+ );
- connection_config.insert("type".into(), Variant(Box::new("wireguard".to_string())));
+ connection_config.insert(
+ "type".into(),
+ Value::new("wireguard").try_to_owned().unwrap(),
+ );
connection_config.insert(
"id".into(),
- Variant(Box::new(MULLVAD_INTERFACE_NAME.to_string())),
+ Value::from(MULLVAD_INTERFACE_NAME).try_to_owned().unwrap(),
);
connection_config.insert(
"interface-name".into(),
- Variant(Box::new(MULLVAD_INTERFACE_NAME.to_string())),
+ Value::from(MULLVAD_INTERFACE_NAME).try_to_owned().unwrap(),
);
- connection_config.insert("autoconnect".into(), Variant(Box::new(true)));
+ connection_config.insert("autoconnect".into(), OwnedValue::from(true));
- let ipv4_addrs: Vec<_> = config
- .tunnel
- .addresses
- .iter()
- .filter(|ip| ip.is_ipv4())
- .map(NetworkManager::convert_address_to_dbus)
- .collect();
+ /*
+ let ipv4_addrs: Vec<_> = config
+ .tunnel
+ .addresses
+ .iter()
+ .filter(|ip| ip.is_ipv4())
+ .map(NetworkManager::convert_address_to_dbus)
+ .collect();
- let ipv6_addrs: Vec<_> = config
- .tunnel
- .addresses
- .iter()
- .filter(|ip| ip.is_ipv6())
- .map(NetworkManager::convert_address_to_dbus)
- .collect();
+ let ipv6_addrs: Vec<_> = config
+ .tunnel
+ .addresses
+ .iter()
+ .filter(|ip| ip.is_ipv6())
+ .map(NetworkManager::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)));
+ ipv4_config.insert("address-data".into(), OwnedValue::from(ipv4_addrs));
+ ipv4_config.insert("ignore-auto-routes".into(), OwnedValue::from(true));
+ ipv4_config.insert("ignore-auto-dns".into(), OwnedValue::from(true));
+ ipv4_config.insert("may-fail".into(), OwnedValue::from(true));
+ ipv4_config.insert("method".into(), OwnedValue::from("manual".to_string()));
+ ipv4_config.insert("never-default".into(), OwnedValue::from(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)));
+ ipv6_config.insert("method".into(), OwnedValue::from("manual".to_string()));
+ ipv6_config.insert("address-data".into(), OwnedValue::from(ipv6_addrs));
+ ipv6_config.insert("ignore-auto-routes".into(), OwnedValue::from(true));
+ ipv6_config.insert("ignore-auto-dns".into(), OwnedValue::from(true));
+ ipv6_config.insert("may-fail".into(), OwnedValue::from(true));
}
- let mut settings = HashMap::new();
settings.insert("ipv4".into(), ipv4_config);
if !ipv6_config.is_empty() {
settings.insert("ipv6".into(), ipv6_config);
}
+ */
+ let mut settings = HashMap::new();
+
settings.insert("wireguard".into(), wireguard_config);
settings.insert("connection".into(), connection_config);