summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2020-07-30 12:54:48 +0200
committerDavid Lönnhager <david.l@mullvad.net>2020-08-10 13:36:13 +0200
commit0692f84d3f20db4211e1a2c97aa9d6fbbe4874fa (patch)
tree88edf3f5e489ef17ece098b12847794a4af17b59
parentd20d34aaf8381f9b636a07b496ae37f467734472 (diff)
downloadmullvadvpn-0692f84d3f20db4211e1a2c97aa9d6fbbe4874fa.tar.xz
mullvadvpn-0692f84d3f20db4211e1a2c97aa9d6fbbe4874fa.zip
Apply DNS config only to the tunnel interface with NetworkManager
-rw-r--r--talpid-core/src/dns/linux/mod.rs4
-rw-r--r--talpid-core/src/dns/linux/network_manager.rs227
2 files changed, 182 insertions, 49 deletions
diff --git a/talpid-core/src/dns/linux/mod.rs b/talpid-core/src/dns/linux/mod.rs
index f2c7af8a0f..b0293590bc 100644
--- a/talpid-core/src/dns/linux/mod.rs
+++ b/talpid-core/src/dns/linux/mod.rs
@@ -120,7 +120,9 @@ impl DnsMonitorHolder {
SystemdResolved(ref mut systemd_resolved) => {
systemd_resolved.set_dns(interface, &servers)?
}
- NetworkManager(ref mut network_manager) => network_manager.set_dns(servers)?,
+ NetworkManager(ref mut network_manager) => {
+ network_manager.set_dns(interface, servers)?
+ }
}
Ok(())
}
diff --git a/talpid-core/src/dns/linux/network_manager.rs b/talpid-core/src/dns/linux/network_manager.rs
index 09cbd1f165..60833207ef 100644
--- a/talpid-core/src/dns/linux/network_manager.rs
+++ b/talpid-core/src/dns/linux/network_manager.rs
@@ -1,8 +1,9 @@
use dbus::{
arg::{RefArg, Variant},
stdintf::*,
- BusType,
+ BusType, Member, Message,
};
+use lazy_static::lazy_static;
use std::{
collections::HashMap,
fs::File,
@@ -28,8 +29,26 @@ pub enum Error {
#[error(display = "Error while communicating over Dbus")]
Dbus(#[error(source)] dbus::Error),
+ #[error(display = "Failed to construct DBus method call message")]
+ DbusMethodCall(String),
+
+ #[error(display = "Failed to construct DBus member")]
+ DbusMemberConstruct(String),
+
+ #[error(display = "Failed to match the returned D-Bus object with expected type")]
+ MatchDBusTypeError(#[error(source)] dbus::arg::TypeMismatchError),
+
#[error(display = "DNS is managed by systemd-resolved - NM can't enforce DNS globally")]
SystemdResolved,
+
+ #[error(display = "Failed to find obtain devices from network manager")]
+ ObtainDevices,
+
+ #[error(display = "Failed to find link interface in network manager")]
+ LinkNotFound,
+
+ #[error(display = "Device inactive: {}", _0)]
+ DeviceNotReady(u32),
}
const NM_BUS: &str = "org.freedesktop.NetworkManager";
@@ -37,10 +56,18 @@ const NM_TOP_OBJECT: &str = "org.freedesktop.NetworkManager";
const NM_DNS_MANAGER: &str = "org.freedesktop.NetworkManager.DnsManager";
const NM_DNS_MANAGER_PATH: &str = "/org/freedesktop/NetworkManager/DnsManager";
const NM_OBJECT_PATH: &str = "/org/freedesktop/NetworkManager";
+const NM_DEVICE: &str = "org.freedesktop.NetworkManager.Device";
const RPC_TIMEOUT_MS: i32 = 3000;
const GLOBAL_DNS_CONF_KEY: &str = "GlobalDnsConfiguration";
const RC_MANAGEMENT_MODE_KEY: &str = "RcManager";
const DNS_MODE_KEY: &str = "Mode";
+const DNS_FIRST_PRIORITY: i32 = -2147483647;
+
+const NM_DEVICE_STATE_ACTIVATED: u32 = 100;
+
+lazy_static! {
+ static ref NM_DEVICE_STATE_CHANGED: Member<'static> = Member::new("StateChanged").unwrap();
+}
pub struct NetworkManager {
dbus_connection: dbus::Connection,
@@ -83,8 +110,7 @@ impl NetworkManager {
.map_err(Error::Dbus)?;
match dns_mode.as_ref() {
- // NetworkManager can only set DNS globally if it's not managing DNS through
- // systemd-resolved.
+ // Managed by systemd-resolved
"systemd-resolved" => return Err(Error::SystemdResolved),
// If NetworkManager isn't managing DNS for us, it's useless.
"none" => return Err(Error::NetworkManagerNotManagingDns),
@@ -107,62 +133,167 @@ impl NetworkManager {
.with_path(NM_BUS, NM_OBJECT_PATH, RPC_TIMEOUT_MS)
}
- pub fn set_dns(&mut self, servers: &[IpAddr]) -> Result<()> {
- self.set_global_dns(create_global_settings(servers))
- }
+ pub fn set_dns(&mut self, interface_name: &str, servers: &[IpAddr]) -> Result<()> {
+ let device = self.fetch_device(interface_name)?;
+
+ // Get the last applied connection
+
+ let get_applied_connection =
+ Message::new_method_call(NM_BUS, &device, NM_DEVICE, "GetAppliedConnection")
+ .map_err(Error::DbusMethodCall)?
+ .append1(0u32);
+ let applied_connection = self
+ .dbus_connection
+ .send_with_reply_and_block(get_applied_connection, RPC_TIMEOUT_MS)
+ .map_err(Error::Dbus)?;
+
+ let (mut settings, version_id): (
+ HashMap<&str, HashMap<&str, Variant<Box<dyn RefArg>>>>,
+ u64,
+ ) = applied_connection
+ .read2()
+ .map_err(Error::MatchDBusTypeError)?;
+
+ // 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);
+ }
+
+ // Re-apply changes
- fn set_global_dns(&mut self, config: GlobalDnsConfig) -> Result<()> {
- self.as_manager()
- .set(NM_TOP_OBJECT, GLOBAL_DNS_CONF_KEY, config)
- .map_err(Error::Dbus)
+ let reapply = Message::new_method_call(NM_BUS, &device, NM_DEVICE, "Reapply")
+ .map_err(Error::DbusMethodCall)?
+ .append3(settings, version_id, 0u32);
+ self.dbus_connection
+ .send_with_reply_and_block(reapply, RPC_TIMEOUT_MS)
+ .map_err(Error::Dbus)?;
+
+ Ok(())
}
pub fn reset(&mut self) -> Result<()> {
- self.set_global_dns(create_empty_global_settings())
+ Ok(())
}
-}
-type GlobalDnsConfig = HashMap<&'static str, Variant<Box<dyn RefArg>>>;
+ fn update_dns_config<'a, T>(
+ settings: &mut HashMap<&str, HashMap<&str, Variant<Box<dyn RefArg + 'a>>>>,
+ ip_protocol: &'static str,
+ servers: T,
+ ) where
+ T: RefArg + 'a,
+ {
+ let settings = match settings.get_mut(ip_protocol) {
+ Some(ip_protocol) => ip_protocol,
+ None => {
+ settings.insert(ip_protocol, HashMap::new());
+ settings.get_mut(ip_protocol).unwrap()
+ }
+ };
-// The NetworkManager GlobalDnsConfiguration schema looks something like this
-// {
-// "searches": ["example.com", "search-domain.com"],
-// "options": "this field is currently unused",
-// "domains": {
-// "*": {
-// "servers": [ "1.1.1.1" ]
-// }
-// "example.com": {
-// "servers": [ "8.8.8.8", "8.8.4.4" ]
-// }
-// }
-// }
-fn create_global_settings(server_list: &[IpAddr]) -> GlobalDnsConfig {
- let mut global_settings = HashMap::new();
- let mut domain_settings = HashMap::new();
- let mut specific_domain_config = HashMap::new();
+ settings.insert("method", Variant(Box::new("manual".to_string())));
+ settings.insert("dns-priority", Variant(Box::new(DNS_FIRST_PRIORITY)));
+ settings.insert("dns", Variant(Box::new(servers)));
+ }
- let dns_server_list = as_variant(
- server_list
- .iter()
- .map(ToString::to_string)
- .collect::<Vec<_>>(),
- );
- specific_domain_config.insert("servers".to_owned(), dns_server_list);
- domain_settings.insert("*".to_owned(), as_variant(specific_domain_config));
- global_settings.insert("domains", as_variant(domain_settings));
- global_settings.insert("searches", as_variant(vec![] as Vec<String>));
- global_settings.insert("options", as_variant(vec![] as Vec<String>));
+ fn fetch_device(&self, interface_name: &str) -> Result<dbus::Path<'_>> {
+ let devices: Box<dyn RefArg> = self
+ .as_manager()
+ .get(NM_TOP_OBJECT, "Devices")
+ .map_err(Error::Dbus)?;
+ let mut iter = devices.as_iter().ok_or(Error::ObtainDevices)?;
- global_settings
-}
+ while let Some(device) = iter.next() {
+ // Copy due to lifetime weirdness
+ let device = device.box_clone();
+ let device = device
+ .as_any()
+ .downcast_ref::<dbus::Path<'_>>()
+ .ok_or(Error::ObtainDevices)?;
-fn create_empty_global_settings() -> GlobalDnsConfig {
- HashMap::new()
-}
+ let device_name: String = self
+ .dbus_connection
+ .with_path(NM_BUS, device, RPC_TIMEOUT_MS)
+ .get(NM_DEVICE, "Interface")
+ .map_err(Error::Dbus)?;
+
+ if device_name != interface_name {
+ continue;
+ }
+
+ let state: u32 = self
+ .dbus_connection
+ .with_path(NM_BUS, device, RPC_TIMEOUT_MS)
+ .get(NM_DEVICE, "State")
+ .map_err(Error::Dbus)?;
+
+ if state != NM_DEVICE_STATE_ACTIVATED {
+ let mut current_state = state;
+
+ let match_rule = &format!(
+ "destination='{}',path='{}',interface='{}',member='{}'",
+ NM_BUS,
+ device,
+ NM_DEVICE,
+ NM_DEVICE_STATE_CHANGED.to_string()
+ );
+ self.dbus_connection
+ .add_match(match_rule)
+ .map_err(Error::Dbus)?;
+
+ for message in self.dbus_connection.incoming(RPC_TIMEOUT_MS as u32) {
+ if message.member().as_ref() != Some(&NM_DEVICE_STATE_CHANGED) {
+ continue;
+ }
+ let (new_state, _old_state, _reason): (u32, u32, u32) = message
+ .read3()
+ .map_err(Error::MatchDBusTypeError)
+ .map_err(|error| {
+ let _ = self.dbus_connection.remove_match(match_rule);
+ error
+ })?;
-fn as_variant<T: RefArg + 'static>(t: T) -> Variant<Box<dyn RefArg>> {
- Variant(Box::new(t) as Box<dyn RefArg>)
+ current_state = new_state;
+ log::trace!("New tunnel device state: {}", current_state);
+ if current_state == NM_DEVICE_STATE_ACTIVATED {
+ break;
+ }
+ }
+
+ if let Err(error) = self.dbus_connection.remove_match(match_rule) {
+ log::warn!("Failed to remove signal listener: {}", error);
+ }
+
+ if current_state != NM_DEVICE_STATE_ACTIVATED {
+ return Err(Error::DeviceNotReady(state));
+ }
+ }
+
+ return Ok(device.clone());
+ }
+ Err(Error::LinkNotFound)
+ }
}
fn eq_file_content<P: AsRef<Path>>(a: &P, b: &P) -> bool {