diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-11-20 18:17:26 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-11-26 11:14:40 +0100 |
| commit | ad3ccfefe8ba3c329c1df32f5b7cdb2180276575 (patch) | |
| tree | 27618f677c141a4559f24bd13517c8e657ba4cb2 | |
| parent | 992703fd6bd27f2d627675a41d9d114bcda4729b (diff) | |
| download | mullvadvpn-ad3ccfefe8ba3c329c1df32f5b7cdb2180276575.tar.xz mullvadvpn-ad3ccfefe8ba3c329c1df32f5b7cdb2180276575.zip | |
Add talpid-dns crate
| -rw-r--r-- | Cargo.lock | 25 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-daemon/Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-daemon/src/dns.rs | 8 | ||||
| -rw-r--r-- | talpid-dns/Cargo.toml | 64 | ||||
| -rw-r--r-- | talpid-dns/src/android.rs | 24 | ||||
| -rw-r--r-- | talpid-dns/src/lib.rs | 226 | ||||
| -rw-r--r-- | talpid-dns/src/linux/interface.rs | 16 | ||||
| -rw-r--r-- | talpid-dns/src/linux/mod.rs | 180 | ||||
| -rw-r--r-- | talpid-dns/src/linux/network_manager.rs | 53 | ||||
| -rw-r--r-- | talpid-dns/src/linux/resolvconf.rs | 197 | ||||
| -rw-r--r-- | talpid-dns/src/linux/static_resolv_conf.rs | 237 | ||||
| -rw-r--r-- | talpid-dns/src/linux/systemd_resolved.rs | 83 | ||||
| -rw-r--r-- | talpid-dns/src/macos.rs | 757 | ||||
| -rw-r--r-- | talpid-dns/src/windows/auto.rs | 110 | ||||
| -rw-r--r-- | talpid-dns/src/windows/dnsapi.rs | 96 | ||||
| -rw-r--r-- | talpid-dns/src/windows/iphlpapi.rs | 224 | ||||
| -rw-r--r-- | talpid-dns/src/windows/mod.rs | 96 | ||||
| -rw-r--r-- | talpid-dns/src/windows/netsh.rs | 213 | ||||
| -rw-r--r-- | talpid-dns/src/windows/tcpip.rs | 177 |
20 files changed, 2785 insertions, 3 deletions
diff --git a/Cargo.lock b/Cargo.lock index 7598abb4ef..04c686d712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3059,6 +3059,7 @@ dependencies = [ "socket2 0.5.8", "talpid-core", "talpid-dbus", + "talpid-dns", "talpid-future", "talpid-macos", "talpid-platform-metadata", @@ -5427,6 +5428,30 @@ dependencies = [ ] [[package]] +name = "talpid-dns" +version = "0.0.0" +dependencies = [ + "futures", + "inotify 0.10.2", + "log", + "nix 0.30.1", + "once_cell", + "parking_lot", + "resolv-conf", + "system-configuration", + "talpid-dbus", + "talpid-routing", + "talpid-types", + "talpid-windows", + "thiserror 2.0.9", + "tokio", + "triggered", + "which", + "windows-sys 0.61.1", + "winreg 0.55.0", +] + +[[package]] name = "talpid-future" version = "0.0.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index 607cf07635..bbcdef6cb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "mullvad-version", "talpid-core", "talpid-dbus", + "talpid-dns", "talpid-future", "talpid-macos", "talpid-net", diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index a5e010f99a..d9844f20a5 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -45,6 +45,7 @@ mullvad-version = { path = "../mullvad-version" } mullvad-update = { path = "../mullvad-update", features = ["client"] } mullvad-leak-checker = { path = "../mullvad-leak-checker", default-features = false } talpid-core = { path = "../talpid-core" } +talpid-dns = { path = "../talpid-dns" } talpid-future = { path = "../talpid-future" } talpid-platform-metadata = { path = "../talpid-platform-metadata" } talpid-time = { path = "../talpid-time" } diff --git a/mullvad-daemon/src/dns.rs b/mullvad-daemon/src/dns.rs index cf0c1e9084..e264acdbd1 100644 --- a/mullvad-daemon/src/dns.rs +++ b/mullvad-daemon/src/dns.rs @@ -1,6 +1,8 @@ -use mullvad_types::settings::{DnsOptions, DnsState}; use std::net::{IpAddr, Ipv4Addr}; -use talpid_core::{dns::DnsConfig, firewall::is_local_address}; + +use mullvad_types::settings::{DnsOptions, DnsState}; +use talpid_core::firewall::is_local_address; +use talpid_dns::DnsConfig; /// When we want to block certain contents with the help of DNS server side, /// we compute the resolver IP to use based on these constants. The last @@ -66,7 +68,7 @@ pub fn addresses_from_options(options: &DnsOptions) -> DnsConfig { mod test { use crate::dns::addresses_from_options; use mullvad_types::settings::{CustomDnsOptions, DefaultDnsOptions, DnsOptions, DnsState}; - use talpid_core::dns::DnsConfig; + use talpid_dns::DnsConfig; #[test] fn test_default_dns() { diff --git a/talpid-dns/Cargo.toml b/talpid-dns/Cargo.toml new file mode 100644 index 0000000000..b9d0c852b8 --- /dev/null +++ b/talpid-dns/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "talpid-dns" +description = "Privacy preserving and secure DNS library" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +thiserror = { workspace = true } +log = { workspace = true } +talpid-types = { path = "../talpid-types" } + +[target.'cfg(target_os = "linux")'.dependencies] +futures = { workspace = true } +inotify = "0.10" +nix = { workspace = true, features = ["net"] } +parking_lot = "0.12.0" +resolv-conf = "0.7" +talpid-dbus = { path = "../talpid-dbus" } +talpid-routing = { path = "../talpid-routing" } +tokio = { workspace = true, features = ["macros"] } +triggered = "0.1.1" +which = { version = "4.0", default-features = false } + +[target.'cfg(target_os = "macos")'.dependencies] +parking_lot = "0.12.0" +system-configuration = "0.5.1" +talpid-routing = { path = "../talpid-routing" } + +[target.'cfg(windows)'.dependencies] +once_cell = { workspace = true } +talpid-windows = { path = "../talpid-windows" } +winreg = { workspace = true, features = ["transactions"] } + +[target.'cfg(windows)'.dependencies.windows-sys] +workspace = true +features = [ + "Win32_Foundation", + "Win32_Globalization", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Ioctl", + "Win32_System_IO", + "Win32_System_LibraryLoader", + "Win32_System_ProcessStatus", + "Win32_System_Registry", + "Win32_System_Rpc", + "Win32_System_Services", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_Networking_WinSock", + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", + "Win32_System_SystemInformation", +] diff --git a/talpid-dns/src/android.rs b/talpid-dns/src/android.rs new file mode 100644 index 0000000000..2ed452e0b7 --- /dev/null +++ b/talpid-dns/src/android.rs @@ -0,0 +1,24 @@ +use super::ResolvedDnsConfig; + +/// Stub error type for DNS errors on Android. +#[derive(Debug, thiserror::Error)] +#[error("Unknown Android DNS error")] +pub struct Error; + +pub struct DnsMonitor; + +impl super::DnsMonitorT for DnsMonitor { + type Error = Error; + + fn new() -> Result<Self, Self::Error> { + Ok(DnsMonitor) + } + + fn set(&mut self, _interface: &str, _servers: ResolvedDnsConfig) -> Result<(), Self::Error> { + Ok(()) + } + + fn reset(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/talpid-dns/src/lib.rs b/talpid-dns/src/lib.rs new file mode 100644 index 0000000000..03a26f43c8 --- /dev/null +++ b/talpid-dns/src/lib.rs @@ -0,0 +1,226 @@ +//! Abstractions over operating system DNS settings. +use std::fmt; +use std::net::IpAddr; + +#[cfg(target_os = "linux")] +use talpid_routing::RouteManagerHandle; + +#[cfg(target_os = "macos")] +#[path = "macos.rs"] +mod imp; + +#[cfg(target_os = "linux")] +#[path = "linux/mod.rs"] +mod imp; + +#[cfg(target_os = "linux")] +pub use imp::will_use_nm; + +#[cfg(windows)] +#[path = "windows/mod.rs"] +mod imp; + +#[cfg(target_os = "android")] +#[path = "android.rs"] +mod imp; + +pub use self::imp::Error; + +/// DNS configuration +#[derive(Debug, Clone, PartialEq)] +pub struct DnsConfig { + config: InnerDnsConfig, +} + +impl Default for DnsConfig { + fn default() -> Self { + Self { + config: InnerDnsConfig::Default, + } + } +} + +impl DnsConfig { + /// Use the specified addresses for DNS resolution + pub fn from_addresses(tunnel_config: &[IpAddr], non_tunnel_config: &[IpAddr]) -> Self { + DnsConfig { + config: InnerDnsConfig::Override { + tunnel_config: tunnel_config.to_owned(), + non_tunnel_config: non_tunnel_config.to_owned(), + }, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum InnerDnsConfig { + /// Use gateway addresses from the tunnel config + Default, + /// Use the specified addresses for DNS resolution + Override { + /// Addresses to configure on the tunnel interface + tunnel_config: Vec<IpAddr>, + /// Addresses to allow on non-tunnel interface. + /// For the most part, the tunnel state machine will not handle any of this configuration + /// on non-tunnel interface, only allow them in the firewall. + non_tunnel_config: Vec<IpAddr>, + }, +} + +impl DnsConfig { + pub fn resolve( + &self, + default_tun_config: &[IpAddr], + #[cfg(target_os = "macos")] port: u16, + ) -> ResolvedDnsConfig { + match &self.config { + InnerDnsConfig::Default => ResolvedDnsConfig { + tunnel_config: default_tun_config.to_owned(), + non_tunnel_config: vec![], + #[cfg(target_os = "macos")] + port, + }, + InnerDnsConfig::Override { + tunnel_config, + non_tunnel_config, + } => ResolvedDnsConfig { + tunnel_config: tunnel_config.to_owned(), + non_tunnel_config: non_tunnel_config.to_owned(), + #[cfg(target_os = "macos")] + port, + }, + } + } +} + +/// DNS configuration with `DnsConfig::Default` resolved +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedDnsConfig { + /// Addresses to configure on the tunnel interface + tunnel_config: Vec<IpAddr>, + /// Addresses to allow on non-tunnel interface. + /// For the most part, the tunnel state machine will not handle any of this configuration + /// on non-tunnel interface, only allow them in the firewall. + non_tunnel_config: Vec<IpAddr>, + /// Port to use + #[cfg(target_os = "macos")] + port: u16, +} + +impl fmt::Display for ResolvedDnsConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Tunnel DNS: ")?; + Self::fmt_addr_set(f, &self.tunnel_config)?; + + f.write_str(" Non-tunnel DNS: ")?; + Self::fmt_addr_set(f, &self.non_tunnel_config)?; + + #[cfg(target_os = "macos")] + write!(f, " Port: {}", self.port)?; + + Ok(()) + } +} + +impl ResolvedDnsConfig { + fn fmt_addr_set(f: &mut fmt::Formatter<'_>, addrs: &[IpAddr]) -> fmt::Result { + f.write_str("{")?; + for (i, addr) in addrs.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + write!(f, "{addr}")?; + } + f.write_str("}") + } + + /// Addresses to configure on the tunnel interface + pub fn tunnel_config(&self) -> &[IpAddr] { + &self.tunnel_config + } + + /// Addresses to allow on non-tunnel interface. + /// For the most part, the tunnel state machine will not handle any of this configuration + /// on non-tunnel interface, only allow them in the firewall. + pub fn non_tunnel_config(&self) -> &[IpAddr] { + &self.non_tunnel_config + } + + /// Consume `self` and return a vector of all addresses + pub fn addresses(self) -> impl Iterator<Item = IpAddr> { + self.non_tunnel_config.into_iter().chain(self.tunnel_config) + } + + /// Return whether the config contains only (and at least one) loopback addresses, and zero + /// non-loopback addresses + pub fn is_loopback(&self) -> bool { + let (loopback_addrs, non_loopback_addrs) = self + .tunnel_config + .iter() + .chain(self.non_tunnel_config.iter()) + .copied() + .partition::<Vec<_>, _>(|ip| ip.is_loopback()); + + !loopback_addrs.is_empty() && non_loopback_addrs.is_empty() + } +} + +/// Sets and monitors system DNS settings. Makes sure the desired DNS servers are being used. +pub struct DnsMonitor { + inner: imp::DnsMonitor, +} + +impl DnsMonitor { + /// Returns a new `DnsMonitor` that can set and monitor the system DNS. + pub fn new( + #[cfg(target_os = "linux")] handle: tokio::runtime::Handle, + #[cfg(target_os = "linux")] route_manager: RouteManagerHandle, + ) -> Result<Self, Error> { + Ok(DnsMonitor { + inner: imp::DnsMonitor::new( + #[cfg(target_os = "linux")] + handle, + #[cfg(target_os = "linux")] + route_manager, + )?, + }) + } + + /// Set DNS to the given servers. And start monitoring the system for changes. + pub fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), Error> { + log::info!("Setting DNS servers: {config}",); + self.inner.set(interface, config) + } + + /// Reset system DNS settings to what it was before being set by this instance. + /// This succeeds if the interface does not exist. + pub fn reset(&mut self) -> Result<(), Error> { + log::info!("Resetting DNS"); + self.inner.reset() + } + + /// Reset DNS settings to what they were before being set by this instance. + /// If the settings only affect a specific interface, this can be a no-op, + /// as the interface will be destroyed. + pub fn reset_before_interface_removal(&mut self) -> Result<(), Error> { + log::info!("Resetting DNS"); + self.inner.reset_before_interface_removal() + } +} + +trait DnsMonitorT: Sized { + type Error: std::error::Error; + + fn new( + #[cfg(target_os = "linux")] handle: tokio::runtime::Handle, + #[cfg(target_os = "linux")] route_manager: RouteManagerHandle, + ) -> Result<Self, Self::Error>; + + fn set(&mut self, interface: &str, servers: ResolvedDnsConfig) -> Result<(), Self::Error>; + + fn reset(&mut self) -> Result<(), Self::Error>; + + fn reset_before_interface_removal(&mut self) -> Result<(), Self::Error> { + self.reset() + } +} diff --git a/talpid-dns/src/linux/interface.rs b/talpid-dns/src/linux/interface.rs new file mode 100644 index 0000000000..7bca9ac6fb --- /dev/null +++ b/talpid-dns/src/linux/interface.rs @@ -0,0 +1,16 @@ +use nix::{errno::Errno, libc, net::if_::if_nametoindex}; + +/// Converts an interface name into the corresponding index. +pub fn iface_index(name: &str) -> Result<libc::c_uint, IfaceIndexLookupError> { + if_nametoindex(name).map_err(|error| IfaceIndexLookupError { + interface_name: name.to_owned(), + error, + }) +} + +#[derive(Debug, thiserror::Error)] +#[error("Failed to get index for interface {interface_name}: {error}")] +pub struct IfaceIndexLookupError { + pub interface_name: String, + pub error: Errno, +} diff --git a/talpid-dns/src/linux/mod.rs b/talpid-dns/src/linux/mod.rs new file mode 100644 index 0000000000..b716b4a9e1 --- /dev/null +++ b/talpid-dns/src/linux/mod.rs @@ -0,0 +1,180 @@ +mod interface; +mod network_manager; +mod resolvconf; +mod static_resolv_conf; +mod systemd_resolved; + +use std::env; +use std::fmt::{self, Display}; +use std::net::IpAddr; +use talpid_routing::RouteManagerHandle; + +use self::network_manager::NetworkManager; +use self::resolvconf::Resolvconf; +use self::static_resolv_conf::StaticResolvConf; +use self::systemd_resolved::SystemdResolved; +use crate::ResolvedDnsConfig; + +pub type Result<T> = std::result::Result<T, Error>; + +/// Errors that can happen in the Linux DNS monitor +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Error in systemd-resolved DNS monitor + #[error("Error in systemd-resolved DNS monitor")] + SystemdResolved(#[from] systemd_resolved::Error), + + /// Error in NetworkManager DNS monitor + #[error("Error in NetworkManager DNS monitor")] + NetworkManager(#[from] network_manager::Error), + + /// Error in resolvconf DNS monitor + #[error("Error in resolvconf DNS monitor")] + Resolvconf(#[from] resolvconf::Error), + + /// Error in static /etc/resolv.conf DNS monitor + #[error("Error in static /etc/resolv.conf DNS monitor")] + StaticResolvConf(#[from] static_resolv_conf::Error), + + /// No suitable DNS monitor implementation detected + #[error("No suitable DNS monitor implementation detected")] + NoDnsMonitor, +} + +pub struct DnsMonitor { + route_manager: RouteManagerHandle, + handle: tokio::runtime::Handle, + inner: Option<DnsMonitorHolder>, +} + +impl super::DnsMonitorT for DnsMonitor { + type Error = Error; + + fn new(handle: tokio::runtime::Handle, route_manager: RouteManagerHandle) -> Result<Self> { + Ok(DnsMonitor { + route_manager, + handle, + inner: None, + }) + } + + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<()> { + let servers = config.tunnel_config(); + self.reset()?; + // Creating a new DNS monitor for each set, in case the system changed how it manages DNS. + let mut inner = DnsMonitorHolder::new()?; + if !servers.is_empty() { + inner.set(&self.handle, &self.route_manager, interface, servers)?; + self.inner = Some(inner); + } + Ok(()) + } + + fn reset(&mut self) -> Result<()> { + if let Some(mut inner) = self.inner.take() { + inner.reset(&self.handle)?; + } + Ok(()) + } +} + +pub enum DnsMonitorHolder { + SystemdResolved(SystemdResolved), + NetworkManager(NetworkManager), + Resolvconf(Resolvconf), + StaticResolvConf(StaticResolvConf), +} + +impl fmt::Display for DnsMonitorHolder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::DnsMonitorHolder::*; + let name = match self { + Resolvconf(..) => "resolvconf", + StaticResolvConf(..) => "/etc/resolv.conf", + SystemdResolved(..) => "systemd-resolved", + NetworkManager(..) => "NetworkManager", + }; + f.write_str(name) + } +} + +impl DnsMonitorHolder { + fn new() -> Result<Self> { + let dns_module = env::var_os("TALPID_DNS_MODULE"); + + let manager = match dns_module.as_ref().and_then(|value| value.to_str()) { + Some("static-file") => DnsMonitorHolder::StaticResolvConf(StaticResolvConf::new()?), + Some("resolvconf") => DnsMonitorHolder::Resolvconf(Resolvconf::new()?), + Some("systemd") => DnsMonitorHolder::SystemdResolved(SystemdResolved::new()?), + Some("network-manager") => DnsMonitorHolder::NetworkManager(NetworkManager::new()?), + Some(_) | None => Self::with_detected_dns_manager()?, + }; + log::debug!("Managing DNS via {}", manager); + Ok(manager) + } + + fn with_detected_dns_manager() -> Result<Self> { + fn log_err<E: Display>(method: &'static str) -> impl Fn(&E) { + move |err: &E| { + log::debug!("Can't manage DNS using {method}: {err}"); + } + } + + SystemdResolved::new() + .map(DnsMonitorHolder::SystemdResolved) + .inspect_err(log_err("systemd-resolved")) + .or_else(|_| { + NetworkManager::new() + .map(DnsMonitorHolder::NetworkManager) + .inspect_err(log_err("NetworkManager")) + }) + .or_else(|_| { + Resolvconf::new() + .map(DnsMonitorHolder::Resolvconf) + .inspect_err(log_err("resolveconf")) + }) + .or_else(|_| { + StaticResolvConf::new() + .map(DnsMonitorHolder::StaticResolvConf) + .inspect_err(log_err("/etc/resolv.conf")) + }) + .map_err(|_| Error::NoDnsMonitor) + } + + fn set( + &mut self, + handle: &tokio::runtime::Handle, + route_manager: &RouteManagerHandle, + interface: &str, + servers: &[IpAddr], + ) -> Result<()> { + use self::DnsMonitorHolder::*; + match self { + Resolvconf(resolvconf) => resolvconf.set_dns(interface, servers)?, + StaticResolvConf(static_resolv_conf) => static_resolv_conf.set_dns(servers.to_vec())?, + SystemdResolved(systemd_resolved) => handle.block_on(systemd_resolved.set_dns( + route_manager.clone(), + interface, + servers, + ))?, + NetworkManager(network_manager) => network_manager.set_dns(interface, servers)?, + } + Ok(()) + } + + fn reset(&mut self, handle: &tokio::runtime::Handle) -> Result<()> { + use self::DnsMonitorHolder::*; + match self { + Resolvconf(resolvconf) => resolvconf.reset()?, + StaticResolvConf(static_resolv_conf) => static_resolv_conf.reset()?, + SystemdResolved(systemd_resolved) => handle.block_on(systemd_resolved.reset())?, + NetworkManager(network_manager) => network_manager.reset()?, + } + Ok(()) + } +} + +/// Returns true if DnsMonitor will use NetworkManager to manage DNS. +pub fn will_use_nm() -> bool { + SystemdResolved::new().is_err() && NetworkManager::new().is_ok() +} diff --git a/talpid-dns/src/linux/network_manager.rs b/talpid-dns/src/linux/network_manager.rs new file mode 100644 index 0000000000..14e3fe7178 --- /dev/null +++ b/talpid-dns/src/linux/network_manager.rs @@ -0,0 +1,53 @@ +use std::net::IpAddr; +pub use talpid_dbus::network_manager::Error; +use talpid_dbus::network_manager::{self, DeviceConfig, NetworkManager as DBus}; + +pub type Result<T> = std::result::Result<T, Error>; + +pub struct NetworkManager { + pub connection: DBus, + device: Option<String>, + settings_backup: Option<DeviceConfig>, +} + +impl NetworkManager { + pub fn new() -> Result<Self> { + let connection = DBus::new()?; + connection.ensure_can_be_used_to_manage_dns()?; + let manager = NetworkManager { + connection, + device: None, + settings_backup: None, + }; + Ok(manager) + } + + pub fn set_dns(&mut self, interface_name: &str, servers: &[IpAddr]) -> Result<()> { + let old_settings = self.connection.set_dns(interface_name, servers)?; + self.settings_backup = Some(old_settings); + self.device = Some(interface_name.to_string()); + Ok(()) + } + + pub fn reset(&mut self) -> Result<()> { + if let Some(settings_backup) = self.settings_backup.take() { + let device = match self.device.take() { + Some(device) => device, + None => return Ok(()), + }; + let device_path = match self.connection.fetch_device(&device) { + Ok(device_path) => device_path, + Err(Error::DeviceNotFound) => return Ok(()), + Err(error) => return Err(error), + }; + + if network_manager::device_is_ready(self.connection.get_device_state(&device_path)?) { + self.connection + .reapply_settings(&device_path, settings_backup, 0u64)?; + } + return Ok(()); + } + log::trace!("No DNS settings to reset"); + Ok(()) + } +} diff --git a/talpid-dns/src/linux/resolvconf.rs b/talpid-dns/src/linux/resolvconf.rs new file mode 100644 index 0000000000..d98d234458 --- /dev/null +++ b/talpid-dns/src/linux/resolvconf.rs @@ -0,0 +1,197 @@ +use std::{ + collections::HashSet, + ffi::OsStr, + fs, + io::{self, Write}, + net::IpAddr, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use which::which; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to detect 'resolvconf' program")] + NoResolvconf, + + #[error("The resolvconf in PATH is just a symlink to systemd-resolved")] + ResolvconfUsesResolved, + + #[error("Failed to execute 'resolvconf' program")] + RunResolvconf(#[from] io::Error), + + #[error("Using 'resolvconf' to add a record failed: {}", stderr)] + AddRecord { stderr: String }, + + #[error("Using 'resolvconf' to delete a record failed")] + DeleteRecord, + + #[error("Detected dnsmasq is running and misconfigured")] + DnsmasqMisconfiguration, + + #[error("Current /etc/resolv.conf is not generated by resolvconf")] + ResolvconfNotInUse, +} + +pub struct Resolvconf { + record_names: HashSet<String>, + resolvconf: PathBuf, +} + +impl Resolvconf { + pub fn new() -> Result<Self> { + let resolvconf_path = which("resolvconf").map_err(|_| Error::NoResolvconf)?; + if Self::resolvconf_is_resolved_symlink(&resolvconf_path) { + return Err(Error::ResolvconfUsesResolved); + } + + let is_dnsmasq_running = Self::is_dnsmasq_running(); + + // Check if resolvconf is managing DNS by /etc/resolv.conf + if !is_dnsmasq_running + && !Self::check_if_resolvconf_is_symlinked_correctly() + && !Self::check_if_resolvconf_was_generated() + { + return Err(Error::ResolvconfNotInUse); + } + + // Check if resolvconf can manage DNS via dnsmasq + if is_dnsmasq_running && Self::is_dnsmasq_configured_wrong() { + return Err(Error::DnsmasqMisconfiguration); + } + + Ok(Resolvconf { + record_names: HashSet::new(), + resolvconf: resolvconf_path, + }) + } + + fn resolvconf_is_resolved_symlink(resolvconf_path: &Path) -> bool { + fs::read_link(resolvconf_path) + .map(|resolvconf_target| { + resolvconf_target.file_name() == Some(OsStr::new("resolvectl")) + }) + .unwrap_or_else(|_| false) + } + + pub fn set_dns(&mut self, interface: &str, servers: &[IpAddr]) -> Result<()> { + let record_name = format!("{interface}.mullvad"); + let mut record_contents = String::new(); + + for address in servers { + record_contents.push_str("nameserver "); + record_contents.push_str(&address.to_string()); + record_contents.push('\n'); + } + + let output = { + let mut resolveconf = Command::new(&self.resolvconf); + let mut child = resolveconf + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .args(["-a", &record_name]) + .spawn()?; + let mut stdin = child.stdin.take().expect("stdin to be present"); + stdin + .write_all(record_contents.as_bytes()) + .map_err(Error::RunResolvconf)?; + drop(stdin); + child.wait_with_output()? + }; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(Error::AddRecord { stderr }); + } + + self.record_names.insert(record_name); + + Ok(()) + } + + pub fn reset(&mut self) -> Result<()> { + let mut result = Ok(()); + + for record_name in self.record_names.drain() { + let output = Command::new(&self.resolvconf) + .args(["-d", &record_name, "-f"]) + .output() + .map_err(Error::RunResolvconf)?; + + if !output.status.success() { + log::error!( + "Failed to delete 'resolvconf' record '{}':\n{}", + record_name, + String::from_utf8_lossy(&output.stderr) + ); + result = Err(Error::DeleteRecord); + } + } + + result + } + + fn is_dnsmasq_running() -> bool { + let pid = match fs::read_to_string("/var/run/dnsmasq/dnsmasq.pid") { + Ok(pid) => pid, + Err(_err) => { + return false; + } + }; + + PathBuf::from(format!("/proc/{}/", &pid)).exists() + } + + // Have to check whether dnsmasq has been configured to ignore + // DNS server lists from external sources + // Verify if dnsmasq is configured to ignore any external servers + // by checking for the `no-resolv` config option. + fn is_dnsmasq_configured_wrong() -> bool { + let mut config_paths: Vec<_> = fs::read_dir("/etc/dnsmasq.d/") + .map(|entries| { + entries + .into_iter() + .filter_map(|entry| entry.ok().map(|e| e.path())) + .collect() + }) + .unwrap_or_default(); + + config_paths.push(PathBuf::from("/etc/dnsmasq.conf")); + config_paths + .iter() + .filter_map(|file_path| fs::read(file_path).ok()) + .any(|contents| { + String::from_utf8_lossy(contents.as_slice()) + .lines() + .any(|line| line.trim().starts_with("no-resolv")) + }) + } + + // Returns true if /etc/resolv.conf contents indicate that they've been generated by resolvconf + fn check_if_resolvconf_was_generated() -> bool { + match fs::read_to_string("/etc/resolv.conf") { + Ok(contents) => contents.contains("Generated by resolvconf"), + Err(err) => { + log::error!("Couldn't read /etc/resolv.conf: {}", err); + false + } + } + } + + // Returns true if /etc/resolv.conf is symlinked to resolvconf's runtime directory + // (`/var/run/resolvconf`) + fn check_if_resolvconf_is_symlinked_correctly() -> bool { + match fs::canonicalize("/etc/resolv.conf") { + Err(err) => { + if err.kind() != io::ErrorKind::NotFound { + log::error!("Failed to canonicalize /etc/resolv.conf: {}", err); + } + false + } + Ok(path) => path.starts_with("/var/run/resolvconf"), + } + } +} diff --git a/talpid-dns/src/linux/static_resolv_conf.rs b/talpid-dns/src/linux/static_resolv_conf.rs new file mode 100644 index 0000000000..068ba286a6 --- /dev/null +++ b/talpid-dns/src/linux/static_resolv_conf.rs @@ -0,0 +1,237 @@ +use futures::StreamExt; +use inotify::{Inotify, WatchMask}; +use parking_lot::Mutex; +use resolv_conf::{Config, ScopedIp}; +use std::{fs, io, net::IpAddr, sync::Arc}; +use talpid_types::ErrorExt; +use triggered::{Listener, Trigger, trigger}; + +const RESOLV_CONF_BACKUP_PATH: &str = "/etc/resolv.conf.mullvadbackup"; +const RESOLV_CONF_PATH: &str = "/etc/resolv.conf"; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to watch /etc/resolv.conf for changes")] + WatchResolvConf(#[source] std::io::Error), + + #[error("Failed to write to {0}")] + WriteResolvConf(&'static str, #[source] io::Error), + + #[error("Failed to read from {0}")] + ReadResolvConf(&'static str, #[source] io::Error), + + #[error("resolv.conf at {0} could not be parsed")] + Parse(&'static str, #[source] resolv_conf::ParseError), + + #[error("Failed to remove stale resolv.conf backup at {0}")] + RemoveBackup(&'static str, #[source] io::Error), +} + +pub struct StaticResolvConf { + state: Arc<Mutex<Option<State>>>, + _watcher: DnsWatcher, +} + +impl StaticResolvConf { + pub fn new() -> Result<Self> { + restore_from_backup()?; + + let state = Arc::new(Mutex::new(None)); + let watcher = DnsWatcher::start(state.clone())?; + + Ok(StaticResolvConf { + state, + _watcher: watcher, + }) + } + + pub fn set_dns(&mut self, servers: Vec<IpAddr>) -> Result<()> { + let mut state = self.state.lock(); + let new_state = match state.take() { + None => { + let backup = read_config()?; + write_backup(&backup)?; + + State { + backup, + desired_dns: servers, + } + } + Some(previous_state) => State { + backup: previous_state.backup, + desired_dns: servers, + }, + }; + + let new_config = new_state.desired_config(); + + *state = Some(new_state); + + write_config(&new_config) + } + + pub fn reset(&mut self) -> Result<()> { + if let Some(state) = self.state.lock().take() { + write_config(&state.backup)?; + let _ = fs::remove_file(RESOLV_CONF_BACKUP_PATH); + } + + Ok(()) + } +} + +struct State { + backup: Config, + desired_dns: Vec<IpAddr>, +} + +impl State { + fn desired_config(&self) -> Config { + let mut config = self.backup.clone(); + + config.nameservers = self + .desired_dns + .iter() + .map(|&address| ScopedIp::from(address)) + .collect(); + + config + } +} + +struct DnsWatcher { + cancel_trigger: Trigger, +} + +impl Drop for DnsWatcher { + fn drop(&mut self) { + self.cancel_trigger.trigger(); + } +} + +impl DnsWatcher { + fn start(state: Arc<Mutex<Option<State>>>) -> Result<Self> { + let watcher = Inotify::init().map_err(Error::WatchResolvConf)?; + let mut mask = WatchMask::empty(); + // Documentation for the meaning of these masks can be found in `man inotify` + // + // We do not watch for writes but instead for when a file opened for writing is closed. + // This way we don't have collisions. + mask.insert(WatchMask::CLOSE_WRITE); + // DELETE_SELF is generated if the file watched is itself deleted + mask.insert(WatchMask::DELETE_SELF); + mask.insert(WatchMask::MOVE_SELF); + + watcher + .watches() + .add(RESOLV_CONF_PATH, mask) + .map_err(Error::WatchResolvConf)?; + + let (cancel_trigger, cancel_listener) = trigger(); + + tokio::spawn(async move { Self::event_loop(watcher, cancel_listener, &state).await }); + + Ok(DnsWatcher { cancel_trigger }) + } + + async fn event_loop( + watcher: Inotify, + mut cancel_listener: Listener, + state: &Arc<Mutex<Option<State>>>, + ) { + const EVENT_BUFFER_SIZE: usize = 1024; + let mut buffer = [0; EVENT_BUFFER_SIZE]; + let mut events = watcher + .into_event_stream(&mut buffer) + .expect("Could not read events for resolv.conf"); + + loop { + tokio::select! { + _ = &mut cancel_listener => { + break; + }, + Some(_) = events.next() => { + let mut locked_state = state.lock(); + if let Err(error) = Self::update(locked_state.as_mut()) { + log::error!( + "{}", + error.display_chain_with_msg( + "Failed to update DNS state after DNS settings changed" + ) + ); + } + } + } + } + } + + fn update(state: Option<&mut State>) -> Result<()> { + if let Some(state) = state { + let mut new_config = read_config()?; + let desired_nameservers = state + .desired_dns + .iter() + .map(|&address| ScopedIp::from(address)) + .collect(); + + if new_config.nameservers != desired_nameservers { + state.backup = new_config.clone(); + new_config.nameservers = desired_nameservers; + + write_config(&new_config) + } else { + new_config.nameservers.clear(); + new_config.nameservers.append(&mut state.backup.nameservers); + state.backup = new_config; + + write_backup(&state.backup) + } + } else { + Ok(()) + } + } +} + +fn read_config() -> Result<Config> { + if !std::path::Path::new(RESOLV_CONF_PATH).exists() { + return Ok(Config::new()); + } + + let contents = fs::read_to_string(RESOLV_CONF_PATH) + .map_err(|e| Error::ReadResolvConf(RESOLV_CONF_PATH, e))?; + let config = Config::parse(contents).map_err(|e| Error::Parse(RESOLV_CONF_PATH, e))?; + + Ok(config) +} + +fn write_config(config: &Config) -> Result<()> { + fs::write(RESOLV_CONF_PATH, config.to_string().as_bytes()) + .map_err(|e| Error::WriteResolvConf(RESOLV_CONF_PATH, e)) +} + +fn write_backup(backup: &Config) -> Result<()> { + fs::write(RESOLV_CONF_BACKUP_PATH, backup.to_string().as_bytes()) + .map_err(|e| Error::WriteResolvConf(RESOLV_CONF_BACKUP_PATH, e)) +} + +fn restore_from_backup() -> Result<()> { + match fs::read_to_string(RESOLV_CONF_BACKUP_PATH) { + Ok(backup) => { + log::info!("Restoring DNS state from backup"); + let config = + Config::parse(backup).map_err(|e| Error::Parse(RESOLV_CONF_BACKUP_PATH, e))?; + + write_config(&config)?; + + fs::remove_file(RESOLV_CONF_BACKUP_PATH) + .map_err(|e| Error::RemoveBackup(RESOLV_CONF_BACKUP_PATH, e)) + } + Err(ref error) if error.kind() == io::ErrorKind::NotFound => { + log::debug!("No DNS state backup to restore"); + Ok(()) + } + Err(error) => Err(Error::ReadResolvConf(RESOLV_CONF_BACKUP_PATH, error)), + } +} diff --git a/talpid-dns/src/linux/systemd_resolved.rs b/talpid-dns/src/linux/systemd_resolved.rs new file mode 100644 index 0000000000..13241aa898 --- /dev/null +++ b/talpid-dns/src/linux/systemd_resolved.rs @@ -0,0 +1,83 @@ +use std::net::IpAddr; +use talpid_dbus::systemd_resolved::{AsyncHandle, SystemdResolved as DbusInterface}; +use talpid_routing::RouteManagerHandle; +use talpid_types::ErrorExt; + +pub(crate) use talpid_dbus::systemd_resolved::Error as SystemdDbusError; + +use crate::imp::interface::{IfaceIndexLookupError, iface_index}; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("systemd-resolved operation failed")] + SystemdResolvedError(#[from] SystemdDbusError), + + #[error("Failed to resolve interface index with error {0}")] + InterfaceNameError(#[from] IfaceIndexLookupError), +} + +pub struct SystemdResolved { + pub dbus_interface: AsyncHandle, + tunnel_index: u32, +} + +impl SystemdResolved { + pub fn new() -> Result<Self> { + let dbus_interface = DbusInterface::new()?.async_handle(); + + let systemd_resolved = SystemdResolved { + dbus_interface, + tunnel_index: 0, + }; + + Ok(systemd_resolved) + } + + pub async fn set_dns( + &mut self, + _route_manager: RouteManagerHandle, + interface_name: &str, + servers: &[IpAddr], + ) -> Result<()> { + let tunnel_index = iface_index(interface_name)?; + self.tunnel_index = tunnel_index; + + if let Err(error) = self.dbus_interface.disable_dot(self.tunnel_index).await { + log::error!("Failed to disable DoT: {}", error.display_chain()); + } + + if let Err(error) = self + .dbus_interface + .set_domains(tunnel_index, &[(".", true)]) + .await + { + log::error!("Failed to set search domains: {}", error.display_chain()); + } + + let _ = self + .dbus_interface + .set_dns(self.tunnel_index, servers.to_vec()) + .await?; + + Ok(()) + } + + pub async fn reset(&mut self) -> Result<()> { + if let Err(error) = self + .dbus_interface + .set_domains(self.tunnel_index, &[]) + .await + { + log::error!("Failed to set search domains: {}", error.display_chain()); + } + + let _ = self + .dbus_interface + .set_dns(self.tunnel_index, vec![]) + .await?; + + Ok(()) + } +} diff --git a/talpid-dns/src/macos.rs b/talpid-dns/src/macos.rs new file mode 100644 index 0000000000..c9e865b766 --- /dev/null +++ b/talpid-dns/src/macos.rs @@ -0,0 +1,757 @@ +#![allow(clippy::undocumented_unsafe_blocks)] // Remove me if you dare. + +use parking_lot::Mutex; +use std::{ + collections::{BTreeSet, HashMap}, + fmt, mem, + net::{IpAddr, SocketAddr}, + sync::{Arc, RwLock, mpsc as sync_mpsc}, + thread, + time::Duration, +}; +use system_configuration::{ + core_foundation::{ + array::CFArray, + base::{CFType, TCFType, ToVoid}, + dictionary::{CFDictionary, CFMutableDictionary}, + number::CFNumber, + propertylist::CFPropertyList, + runloop::{CFRunLoop, kCFRunLoopCommonModes}, + string::CFString, + }, + dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext}, + sys::schema_definitions::{ + kSCPropNetDNSServerAddresses, kSCPropNetDNSServerPort, kSCPropNetInterfaceDeviceName, + }, +}; +use talpid_routing::debounce::BurstGuard; + +use super::ResolvedDnsConfig; + +pub type Result<T> = std::result::Result<T, Error>; + +const DNS_PORT: u16 = 53; + +/// Errors that can happen when setting/monitoring DNS on macOS. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Error while setting DNS servers + #[error("Error while setting DNS servers")] + SettingDnsFailed, + + /// Failed to initialize dynamic store + #[error("Failed to initialize dynamic store")] + DynamicStoreInitError, + + /// Failed to obtain name for interface + #[error("Failed to obtain interface name")] + GetInterfaceNameError, + + /// Failed to load interface config + #[error("Failed to load interface config at path {0}")] + LoadInterfaceConfigError(String), + + /// Failed to load DNS config + #[error("Failed to load DNS config at path {0}")] + LoadDnsConfigError(String), +} + +const STATE_PATH_PATTERN: &str = "State:/Network/Service/.*/DNS"; +const SETUP_PATH_PATTERN: &str = "Setup:/Network/Service/.*/DNS"; + +const BURST_BUFFER_PERIOD: Duration = Duration::from_millis(500); +const BURST_LONGEST_BUFFER_PERIOD: Duration = Duration::from_secs(5); + +type ServicePath = String; +type DnsServer = String; + +struct State { + /// The settings this monitor is currently enforcing as active settings. + dns_settings: Option<DnsSettings>, + /// The backup of all DNS settings. These are being applied back on reset. + backup: HashMap<ServicePath, Option<DnsSettings>>, +} + +impl State { + fn new() -> Self { + Self { + dns_settings: None, + backup: HashMap::new(), + } + } + + /// Construct [`DnsSettings`] from the arguments and apply the desired addresses to all network services. + fn apply_new_config( + &mut self, + store: &SCDynamicStore, + interface: &str, + servers: &[IpAddr], + port: u16, + ) -> Result<()> { + talpid_types::detect_flood!(); + + let servers: Vec<DnsServer> = servers.iter().map(|ip| ip.to_string()).collect(); + let new_settings = + DnsSettings::from_server_addresses(&servers, interface.to_string(), port); + match &self.dns_settings { + None => { + self.dns_settings = Some(new_settings); + self.update_and_apply_state(store); + } + Some(old_settings) => { + if new_settings.server_addresses() != old_settings.server_addresses() { + for service_path in self.backup.keys() { + new_settings.save(store, service_path.as_str())?; + } + self.dns_settings = Some(new_settings); + } + } + }; + + Ok(()) + } + + /// Store changes to the DNS config, ignoring any changes that we have applied. Then apply our + /// desired state to any services to which it has not already been applied. + fn update_and_apply_state(&mut self, store: &SCDynamicStore) { + let actual_state = read_all_dns(store); + self.update_backup_state(&actual_state); + self.apply_desired_state(store, &actual_state); + } + + /// Store changes to the DNS config, ignoring any changes that we have applied. The operation is + /// idempotent. + fn update_backup_state(&mut self, actual_state: &HashMap<ServicePath, Option<DnsSettings>>) { + let Some(ref desired_settings) = self.dns_settings else { + return; + }; + + let prev_state = mem::take(&mut self.backup); + let desired_set = desired_settings.server_addresses(); + + self.backup = Self::merge_states(actual_state, prev_state, desired_set); + } + + /// Merge `new_state` set by the OS with a previous `prev_state`, but ignore any service whose + /// addresses are `ignore_addresses`. + fn merge_states( + new_state: &HashMap<ServicePath, Option<DnsSettings>>, + mut prev_state: HashMap<ServicePath, Option<DnsSettings>>, + ignore_addresses: BTreeSet<SocketAddr>, + ) -> HashMap<ServicePath, Option<DnsSettings>> { + let mut modified_state = HashMap::new(); + + for (path, settings) in new_state { + let old_entry = prev_state.remove(path); + match settings { + // If the service is using the desired addresses, don't save changes + Some(settings) if settings.server_addresses() == ignore_addresses => { + let settings = old_entry.unwrap_or_else(|| Some(settings.to_owned())); + modified_state.insert(path.to_owned(), settings); + } + // Otherwise, save the new settings + settings => { + let servers = settings + .as_ref() + .map(|settings| settings.format_addresses()) + .unwrap_or_default(); + log::debug!("Saving DNS settings [{}] for {}", servers, path); + modified_state.insert(path.to_owned(), settings.to_owned()); + } + } + } + + for path in prev_state.keys() { + log::debug!("DNS removed for {path}"); + } + + modified_state + } + + /// Apply the desired addresses to all network services. The operation is idempotent. + fn apply_desired_state( + &mut self, + store: &SCDynamicStore, + actual_state: &HashMap<ServicePath, Option<DnsSettings>>, + ) { + let Some(ref desired_settings) = self.dns_settings else { + return; + }; + let desired_set = desired_settings.server_addresses(); + + for (path, settings) in actual_state { + match settings { + // Do nothing if the state is already what we want + Some(settings) if settings.server_addresses() == desired_set => (), + // Apply desired state to service + _ => { + let path_cf = CFString::new(path); + if let Err(e) = desired_settings.save(store, path_cf) { + log::error!("Failed changing DNS for {}: {}", path, e); + } + } + } + } + } + + fn reset(&mut self, store: &SCDynamicStore) -> Result<()> { + log::trace!("Restoring DNS settings to: {:#?}", self.backup); + + let actual_state = read_all_dns(store); + self.update_backup_state(&actual_state); + self.dns_settings.take(); + + let old_backup = std::mem::take(&mut self.backup); + + for (service_path, settings) in old_backup { + if let Some(settings) = settings { + settings.save(store, service_path.as_str())?; + } else { + log::debug!("Removing DNS for {}", service_path); + if !store.remove(CFString::new(&service_path)) { + return Err(Error::SettingDnsFailed); + } + } + } + Ok(()) + } +} + +/// Holds the configuration for one service. +#[derive(Debug, Eq, PartialEq, Clone)] +struct DnsSettings { + dict: CFDictionary, + name: String, +} + +unsafe impl Send for DnsSettings {} + +impl DnsSettings { + pub fn from_server_addresses(server_addresses: &[DnsServer], name: String, port: u16) -> Self { + let mut mut_dict = CFMutableDictionary::new(); + if !server_addresses.is_empty() { + let cf_string_servers: Vec<CFString> = + server_addresses.iter().map(|s| CFString::new(s)).collect(); + let server_addresses_value = CFArray::from_CFTypes(&cf_string_servers).into_untyped(); + let server_addresses_key = + unsafe { CFString::wrap_under_get_rule(kSCPropNetDNSServerAddresses) }; + mut_dict.add( + &server_addresses_key.to_void(), + &server_addresses_value.to_void(), + ); + + // Set port if non-standard + if port != DNS_PORT { + let server_port_key = + unsafe { CFString::wrap_under_get_rule(kSCPropNetDNSServerPort) }; + let server_port_value = CFNumber::from(i32::from(port)); + mut_dict.add(&server_port_key.to_void(), &server_port_value.to_void()); + } + } + let dict = mut_dict.to_immutable(); + DnsSettings { dict, name } + } + + /// Get DNS settings for a given service path. Returns `None` If the path does not exist. + pub fn load<S: Into<CFString>>(store: &SCDynamicStore, path: S) -> Result<Self> { + let cf_path = path.into(); + + let dict = store + .get(cf_path.clone()) + .and_then(CFPropertyList::downcast_into::<CFDictionary>) + .ok_or(Error::LoadDnsConfigError(cf_path.to_string()))?; + + let name = + InterfaceSettings::load_from_dns_key(store, cf_path.to_string())?.interface_name()?; + + Ok(DnsSettings { dict, name }) + } + + /// Set the dynamic store entry at `path` to a dictionary these DNS settings. + pub fn save<S: Into<CFString> + fmt::Display>( + &self, + store: &SCDynamicStore, + path: S, + ) -> Result<()> { + log::trace!( + "Setting DNS to [{}] for {}", + self.format_addresses(), + path.to_string() + ); + if store.set(path, self.dict.clone()) { + Ok(()) + } else { + Err(Error::SettingDnsFailed) + } + } + + pub fn server_addresses(&self) -> BTreeSet<SocketAddr> { + let port = self + .dict + .find(unsafe { kSCPropNetDNSServerPort }.to_void()) + .map(|ptr| unsafe { CFType::wrap_under_get_rule(*ptr) }) + .and_then(|port| port.downcast::<CFNumber>()) + .and_then(|port| port.to_i32()) + .and_then(|port| u16::try_from(port).ok()) + .unwrap_or(DNS_PORT); + + self.dict + .find(unsafe { kSCPropNetDNSServerAddresses }.to_void()) + .map(|array_ptr| unsafe { CFType::wrap_under_get_rule(*array_ptr) }) + .and_then(|array| array.downcast::<CFArray>()) + .and_then(Self::parse_cf_array_to_strings) + .unwrap_or_default() + .into_iter() + .flat_map(|addr| addr.parse::<IpAddr>()) + .map(|ip| SocketAddr::new(ip, port)) + .collect() + } + + fn format_addresses(&self) -> String { + let mut s = String::new(); + for addr in self.server_addresses() { + if !s.is_empty() { + s.push_str(", "); + } + s.push_str(&addr.to_string()); + } + s + } + + /// Parses a CFArray into a Rust vector of Rust strings, if the array contains CFString + /// instances only, otherwise `None` is returned. + fn parse_cf_array_to_strings(array: CFArray) -> Option<Vec<String>> { + let mut strings = Vec::new(); + for item_ptr in array.iter() { + let item = unsafe { CFType::wrap_under_get_rule(*item_ptr) }; + if let Some(string) = item.downcast::<CFString>() { + strings.push(string.to_string()); + } else { + log::error!("DNS server entry is not a string: {:?}", item); + return None; + }; + } + Some(strings) + } +} + +#[derive(Debug, Eq, PartialEq)] +struct InterfaceSettings(CFDictionary); + +impl InterfaceSettings { + /// Get network interface settings for the given path + pub fn load_from_dns_key(store: &SCDynamicStore, dns_path: String) -> Result<Self> { + // remove the "DNS" part of the path + let path = match dns_path.strip_prefix("State") { + Some(service_path) => "Setup".to_owned() + service_path, + None => dns_path.clone(), + }; + let interface_path = path.replace("/DNS", "/Interface"); + + Ok(Self( + store + .get(CFString::from(interface_path.as_str())) + .and_then(CFPropertyList::downcast_into::<CFDictionary>) + .ok_or(Error::LoadInterfaceConfigError(path))?, + )) + } + + pub fn interface_name(&self) -> Result<String> { + self.0 + .find(unsafe { kSCPropNetInterfaceDeviceName }.to_void()) + .map(|str_pointer| unsafe { CFType::wrap_under_get_rule(*str_pointer) }) + .and_then(|string| string.downcast::<CFString>()) + .map(|cf_string| cf_string.to_string()) + .ok_or(Error::GetInterfaceNameError) + } +} + +unsafe impl Send for InterfaceSettings {} + +pub struct DnsMonitor { + /// The backing "System Configuration framework" store, which allow us to access and detect + /// changes to the device's network configuration. + store: SCDynamicStore, + /// The current DNS injection state. If this is `None` it means we are not injecting any DNS. + /// When it's `Some(state)` we are actively making sure `state.dns_settings` is configured + /// on all network interfaces. + state: Arc<Mutex<State>>, +} + +/// SAFETY: The `SCDynamicStore` can be sent to other threads since it doesn't share mutable state +/// with anything else. +unsafe impl Send for DnsMonitor {} + +impl super::DnsMonitorT for DnsMonitor { + type Error = Error; + + /// Creates and returns a new `DnsMonitor`. This spawns a background thread that will monitor + /// DNS settings for all network interfaces. If any changes occur it will instantly reset + /// the DNS settings for that interface back to the last server list set to this instance + /// with `set_dns`. + fn new() -> Result<Self> { + let state = Arc::new(Mutex::new(State::new())); + Self::spawn(state.clone())?; + Ok(DnsMonitor { + store: SCDynamicStoreBuilder::new("mullvad-dns").build(), + state, + }) + } + + /// Update the system config to use the DNS `config`. + /// + /// Note that the `interface` parameter does nothing on macOS. Since we can't configure DNS + /// on the tunnel interface, we have to configure all interfaces. + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<()> { + let port = config.port; + let servers: Vec<_> = config.addresses().collect(); + + let mut state = self.state.lock(); + state.apply_new_config(&self.store, interface, &servers, port) + } + + fn reset(&mut self) -> Result<()> { + self.state.lock().reset(&self.store) + } +} + +impl DnsMonitor { + /// Spawns the background thread running the CoreFoundation main loop and monitors the system + /// for DNS changes. + fn spawn(state: Arc<Mutex<State>>) -> Result<()> { + let (result_tx, result_rx) = sync_mpsc::channel(); + thread::spawn(move || match create_dynamic_store(state) { + Ok(store) => { + result_tx.send(Ok(())).unwrap(); + run_dynamic_store_runloop(store); + // TODO(linus): This is critical. Improve later by sending error signal to Daemon + log::error!("Core Foundation main loop exited! It should run forever"); + } + Err(e) => result_tx.send(Err(e)).unwrap(), + }); + result_rx.recv().unwrap() + } +} + +/// Creates a `SCDynamicStore` that watches all network interfaces for changes to the DNS settings. +fn create_dynamic_store(state: Arc<Mutex<State>>) -> Result<SCDynamicStore> { + struct StoreContainer { + store: SCDynamicStore, + } + // SAFETY: The store is thread-safe + unsafe impl Send for StoreContainer {} + // SAFETY: The store is thread-safe + unsafe impl Sync for StoreContainer {} + + let store_container: Arc<RwLock<Option<StoreContainer>>> = Arc::new(RwLock::new(None)); + let store_container_copy = store_container.clone(); + + let update_trigger = BurstGuard::new( + BURST_BUFFER_PERIOD, + BURST_LONGEST_BUFFER_PERIOD, + move || { + if let Some(store) = &*store_container.read().unwrap() { + state.lock().update_and_apply_state(&store.store); + } + }, + ); + + let callback_context = SCDynamicStoreCallBackContext { + callout: dns_change_callback, + info: update_trigger, + }; + + let store = SCDynamicStoreBuilder::new("talpid-dns-monitor") + .callback_context(callback_context) + .build(); + + let mut store_container = store_container_copy.write().unwrap(); + *store_container = Some(StoreContainer { + store: store.clone(), + }); + + let watch_keys: CFArray<CFString> = CFArray::from_CFTypes(&[]); + let watch_patterns = CFArray::from_CFTypes(&[ + CFString::new(STATE_PATH_PATTERN), + CFString::new(SETUP_PATH_PATTERN), + ]); + + if store.set_notification_keys(&watch_keys, &watch_patterns) { + log::trace!("Registered for dynamic store notifications"); + Ok(store) + } else { + Err(Error::DynamicStoreInitError) + } +} + +fn run_dynamic_store_runloop(store: SCDynamicStore) { + let run_loop_source = store.create_run_loop_source(); + CFRunLoop::get_current().add_source(&run_loop_source, unsafe { kCFRunLoopCommonModes }); + + log::trace!("Entering DNS CFRunLoop"); + CFRunLoop::run_current(); +} + +/// This function is called by the Core Foundation event loop when there is a change to one or more +/// watched dynamic store values. In our case we watch all DNS settings. +fn dns_change_callback( + _store: SCDynamicStore, + _changed_keys: CFArray<CFString>, + state: &mut BurstGuard, +) { + state.trigger(); +} + +/// Read all existing DNS settings and return them. +fn read_all_dns(store: &SCDynamicStore) -> HashMap<ServicePath, Option<DnsSettings>> { + let mut settings: HashMap<_, _> = HashMap::new(); + // All "state" DNS, and all corresponding "setup" DNS even if they don't exist + if let Some(paths) = store.get_keys(STATE_PATH_PATTERN) { + for state_path in paths.iter() { + let state_path_str = state_path.to_string(); + let setup_path_str = state_to_setup_path(&state_path_str).unwrap(); + settings.insert( + state_path_str, + DnsSettings::load(store, state_path.clone()).ok(), + ); + settings.insert( + setup_path_str.clone(), + DnsSettings::load(store, setup_path_str.as_ref()).ok(), + ); + } + } + // All "setup" DNS not already covered + if let Some(paths) = store.get_keys(SETUP_PATH_PATTERN) { + for setup_path in paths.iter() { + let setup_path_str = setup_path.to_string(); + settings + .entry(setup_path_str) + .or_insert_with(|| DnsSettings::load(store, setup_path.clone()).ok()); + } + } + settings +} + +fn state_to_setup_path(state_path: &str) -> Option<String> { + if state_path.starts_with("State:/") { + Some(state_path.replacen("State:/", "Setup:/", 1)) + } else { + None + } +} + +#[cfg(test)] +mod test { + use super::{DNS_PORT, DnsSettings, State}; + use std::{ + collections::{BTreeSet, HashMap}, + net::SocketAddr, + }; + + /// The initial backup should equal whatever the first provided state is. + #[test] + fn test_backup_new_dns_config() { + let prev_state = HashMap::new(); + + let new_state = HashMap::from([ + ("a".to_owned(), None), + ( + "b".to_owned(), + Some(DnsSettings::from_server_addresses( + &["1.2.3.4".to_owned()], + "iface_b".to_owned(), + DNS_PORT, + )), + ), + // One of our states already equals the desired state. It should be stored regardless. + ( + "c".to_owned(), + Some(DnsSettings::from_server_addresses( + &["10.64.0.1".to_owned()], + "iface_c".to_owned(), + DNS_PORT, + )), + ), + ]); + + let desired_addresses: BTreeSet<SocketAddr> = ["10.64.0.1:53".parse().unwrap()].into(); + + let merged_state = State::merge_states(&new_state, prev_state, desired_addresses); + + assert_eq!(merged_state, new_state); + } + + /// Any changes equal to the desired state should be ignored. Other changes should be recorded. + #[test] + fn test_backup_ignore_desired_state() { + let prev_state = HashMap::from([ + ("a".to_owned(), None), + ( + "b".to_owned(), + Some(DnsSettings::from_server_addresses( + &["1.2.3.4".to_owned()], + "iface_b".to_owned(), + DNS_PORT, + )), + ), + ( + "c".to_owned(), + Some(DnsSettings::from_server_addresses( + &["10.64.0.1".to_owned()], + "iface_c".to_owned(), + DNS_PORT, + )), + ), + ( + "d".to_owned(), + Some(DnsSettings::from_server_addresses( + &["1.3.3.7".to_owned()], + "iface_d".to_owned(), + DNS_PORT, + )), + ), + ]); + let new_state = HashMap::from([ + // This change should be ignored + ( + "a".to_owned(), + Some(DnsSettings::from_server_addresses( + &["10.64.0.1".to_owned()], + "iface_a".to_owned(), + DNS_PORT, + )), + ), + // This change should be ignored + ( + "b".to_owned(), + Some(DnsSettings::from_server_addresses( + &["10.64.0.1".to_owned()], + "iface_b".to_owned(), + DNS_PORT, + )), + ), + // This change should be ignored + ( + "c".to_owned(), + Some(DnsSettings::from_server_addresses( + &["4.3.2.1".to_owned()], + "iface_c".to_owned(), + DNS_PORT, + )), + ), + // This change should NOT be ignored + ( + "d".to_owned(), + Some(DnsSettings::from_server_addresses( + &["4.3.2.1".to_owned()], + "iface_d".to_owned(), + DNS_PORT, + )), + ), + ]); + let expect_state = HashMap::from([ + ("a".to_owned(), None), + ( + "b".to_owned(), + Some(DnsSettings::from_server_addresses( + &["1.2.3.4".to_owned()], + "iface_b".to_owned(), + DNS_PORT, + )), + ), + ( + "c".to_owned(), + Some(DnsSettings::from_server_addresses( + &["4.3.2.1".to_owned()], + "iface_c".to_owned(), + DNS_PORT, + )), + ), + ( + "d".to_owned(), + Some(DnsSettings::from_server_addresses( + &["4.3.2.1".to_owned()], + "iface_d".to_owned(), + DNS_PORT, + )), + ), + ]); + + let desired_addresses: BTreeSet<SocketAddr> = ["10.64.0.1:53".parse().unwrap()].into(); + + let merged_state = State::merge_states(&new_state, prev_state, desired_addresses); + + assert_eq!(merged_state, expect_state); + } + + /// Services not specified in the new state should be removed from the backed up state + #[test] + fn test_backup_remove_dns_config() { + let prev_state = HashMap::from([ + ( + "a".to_owned(), + Some(DnsSettings::from_server_addresses( + &["10.64.0.1".to_owned()], + "iface_a".to_owned(), + DNS_PORT, + )), + ), + ( + "b".to_owned(), + Some(DnsSettings::from_server_addresses( + &["1.2.3.4".to_owned()], + "iface_b".to_owned(), + DNS_PORT, + )), + ), + ("c".to_owned(), None), + ]); + let new_state = HashMap::from([("c".to_owned(), None)]); + let expected_state = new_state.clone(); + + let desired_addresses: BTreeSet<SocketAddr> = ["10.64.0.1:53".parse().unwrap()].into(); + + let merged_state = State::merge_states(&new_state, prev_state, desired_addresses); + + assert_eq!(merged_state, expected_state); + } + + /// If DHCP provides an IP identical to our desired state, the tracked state will not reflect + /// this. This is a known limitation. + // TODO: This should actually succeed. If we happen to switch to a network whose IP equals + // the "desired IP", we should still back up the result. + #[test] + #[should_panic] + fn test_backup_change_equals_desired_state() { + let prev_state = HashMap::from([( + "a".to_owned(), + Some(DnsSettings::from_server_addresses( + &["192.168.100.1".to_owned()], + "iface_a".to_owned(), + DNS_PORT, + )), + )]); + let new_state = HashMap::from([( + "a".to_owned(), + Some(DnsSettings::from_server_addresses( + &["192.168.1.1".to_owned()], + "iface_a".to_owned(), + DNS_PORT, + )), + )]); + let expect_state = HashMap::from([( + "a".to_owned(), + Some(DnsSettings::from_server_addresses( + &["192.168.1.1".to_owned()], + "iface_a".to_owned(), + DNS_PORT, + )), + )]); + + let desired_addresses: BTreeSet<SocketAddr> = ["192.168.1.1:53".parse().unwrap()].into(); + + let merged_state = State::merge_states(&new_state, prev_state, desired_addresses); + + assert_eq!(merged_state, expect_state); + } +} diff --git a/talpid-dns/src/windows/auto.rs b/talpid-dns/src/windows/auto.rs new file mode 100644 index 0000000000..1d76e967dd --- /dev/null +++ b/talpid-dns/src/windows/auto.rs @@ -0,0 +1,110 @@ +use super::{DnsMonitorT, ResolvedDnsConfig}; +use super::{iphlpapi, netsh, tcpip}; +use windows_sys::Win32::System::Rpc::RPC_S_SERVER_UNAVAILABLE; + +pub struct DnsMonitor { + current_monitor: InnerMonitor, +} +enum InnerMonitor { + Iphlpapi(iphlpapi::DnsMonitor), + Netsh(netsh::DnsMonitor), + Tcpip(tcpip::DnsMonitor), +} + +impl InnerMonitor { + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), super::Error> { + match self { + InnerMonitor::Iphlpapi(monitor) => monitor.set(interface, config)?, + InnerMonitor::Netsh(monitor) => monitor.set(interface, config)?, + InnerMonitor::Tcpip(monitor) => monitor.set(interface, config)?, + } + Ok(()) + } + + fn reset(&mut self) -> Result<(), super::Error> { + match self { + InnerMonitor::Iphlpapi(monitor) => monitor.reset()?, + InnerMonitor::Netsh(monitor) => monitor.reset()?, + InnerMonitor::Tcpip(monitor) => monitor.reset()?, + } + Ok(()) + } + + fn reset_before_interface_removal(&mut self) -> Result<(), super::Error> { + match self { + InnerMonitor::Iphlpapi(monitor) => monitor.reset_before_interface_removal()?, + InnerMonitor::Netsh(monitor) => monitor.reset_before_interface_removal()?, + InnerMonitor::Tcpip(monitor) => monitor.reset_before_interface_removal()?, + } + Ok(()) + } +} + +impl DnsMonitorT for DnsMonitor { + type Error = super::Error; + + fn new() -> Result<Self, Self::Error> { + let current_monitor = if iphlpapi::DnsMonitor::is_supported() { + InnerMonitor::Iphlpapi(iphlpapi::DnsMonitor::new()?) + } else { + InnerMonitor::Netsh(netsh::DnsMonitor::new()?) + }; + + Ok(Self { current_monitor }) + } + + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), Self::Error> { + let result = self.current_monitor.set(interface, config.clone()); + if self.fallback_due_to_dnscache(&result) { + return self.set(interface, config); + } + result + } + + fn reset(&mut self) -> Result<(), Self::Error> { + let result = self.current_monitor.reset(); + if self.fallback_due_to_dnscache(&result) { + return self.reset(); + } + result + } + + fn reset_before_interface_removal(&mut self) -> Result<(), Self::Error> { + let result = self.current_monitor.reset_before_interface_removal(); + if self.fallback_due_to_dnscache(&result) { + return self.reset_before_interface_removal(); + } + result + } +} + +impl DnsMonitor { + fn fallback_due_to_dnscache(&mut self, result: &Result<(), super::Error>) -> bool { + let is_dnscache_error = match result { + Err(super::Error::Iphlpapi(iphlpapi::Error::SetInterfaceDnsSettings(error))) => { + error.raw_os_error() == Some(RPC_S_SERVER_UNAVAILABLE) + } + Err(super::Error::Netsh(netsh::Error::Netsh(Some(1)))) => true, + _ => false, + }; + if is_dnscache_error { + log::warn!("dnscache is not running? Falling back on tcpip method"); + + match tcpip::DnsMonitor::new() { + Ok(mut tcpip) => { + // We need to disable flushing here since it may fail. + // Because dnscache is disabled, there's nothing to flush anyhow. + tcpip.disable_flushing(); + self.current_monitor = InnerMonitor::Tcpip(tcpip); + true + } + Err(error) => { + log::error!("Failed to init tcpip DNS module: {error}"); + false + } + } + } else { + false + } + } +} diff --git a/talpid-dns/src/windows/dnsapi.rs b/talpid-dns/src/windows/dnsapi.rs new file mode 100644 index 0000000000..0465b233e6 --- /dev/null +++ b/talpid-dns/src/windows/dnsapi.rs @@ -0,0 +1,96 @@ +use std::{ + sync::{ + Arc, OnceLock, + atomic::{AtomicUsize, Ordering}, + mpsc, + }, + time::{Duration, Instant}, +}; + +static FLUSH_TIMEOUT: Duration = Duration::from_secs(5); +static DNSAPI_HANDLE: OnceLock<DnsApi> = OnceLock::new(); + +const MAX_CONCURRENT_FLUSHES: usize = 5; + +/// Errors that can happen when configuring DNS on Windows. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to flush the DNS cache. + #[error("Call to flush DNS cache failed")] + FlushCache, + + /// Too many flush attempts in progress. + #[error("Too many flush attempts in progress")] + TooManyFlushAttempts, + + /// Flushing the DNS cache timed out. + #[error("Timeout while flushing DNS cache")] + Timeout, +} + +pub fn flush_resolver_cache() -> Result<(), Error> { + DNSAPI_HANDLE.get_or_init(DnsApi::new).flush_cache() +} + +struct DnsApi { + in_flight_flush_count: Arc<AtomicUsize>, +} + +impl DnsApi { + fn new() -> Self { + DnsApi { + in_flight_flush_count: Arc::new(AtomicUsize::new(0)), + } + } + + fn flush_cache(&self) -> Result<(), Error> { + let update_flush_count_result = + self.in_flight_flush_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |val| { + if val >= MAX_CONCURRENT_FLUSHES { + return None; + } + Some(val + 1) + }); + if update_flush_count_result.is_err() { + return Err(Error::TooManyFlushAttempts); + } + + let (tx, rx) = mpsc::channel(); + let flush_count = self.in_flight_flush_count.clone(); + + std::thread::spawn(move || { + let begin = Instant::now(); + + // SAFETY: this function is trivially safe to call + let result = if unsafe { (DnsFlushResolverCache)() } { + let elapsed = begin.elapsed(); + if elapsed >= FLUSH_TIMEOUT { + log::warn!( + "Flushing system DNS cache took {} seconds", + elapsed.as_secs() + ); + } else { + log::debug!("Flushed system DNS cache"); + } + Ok(()) + } else { + Err(Error::FlushCache) + }; + let _ = tx.send(result); + + flush_count.fetch_sub(1, Ordering::SeqCst); + }); + + match rx.recv_timeout(FLUSH_TIMEOUT) { + Ok(result) => result, + Err(_timeout_err) => Err(Error::Timeout), + } + } +} + +#[link(name = "dnsapi")] +unsafe extern "system" { + // Flushes the DNS resolver cache + pub fn DnsFlushResolverCache() -> bool; +} diff --git a/talpid-dns/src/windows/iphlpapi.rs b/talpid-dns/src/windows/iphlpapi.rs new file mode 100644 index 0000000000..3ffc838d20 --- /dev/null +++ b/talpid-dns/src/windows/iphlpapi.rs @@ -0,0 +1,224 @@ +//! DNS monitor that uses `SetInterfaceDnsSettings`. According to +//! <https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-setinterfacednssettings>, +//! it requires at least Windows 10, build 19041. For that reason, use run-time linking and fall +//! back on other methods if it is not available. +#![allow(clippy::undocumented_unsafe_blocks)] // Remove me if you dare. + +use super::{DnsMonitorT, ResolvedDnsConfig}; +use once_cell::sync::OnceCell; +use std::{ + ffi::OsString, + io, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + os::windows::ffi::OsStrExt, + ptr, +}; +use talpid_types::win32_err; +use talpid_windows::net::{guid_from_luid, luid_from_alias}; +use windows_sys::{ + Win32::{ + Foundation::{ERROR_PROC_NOT_FOUND, FreeLibrary, WIN32_ERROR}, + NetworkManagement::IpHelper::{ + DNS_INTERFACE_SETTINGS, DNS_INTERFACE_SETTINGS_VERSION1, DNS_SETTING_IPV6, + DNS_SETTING_NAMESERVER, + }, + System::LibraryLoader::{GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW}, + }, + core::GUID, + s, w, +}; + +/// Errors that can happen when configuring DNS on Windows. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failure to obtain an interface LUID given an alias. + #[error("Failed to obtain LUID for the interface alias")] + ObtainInterfaceLuid(#[source] io::Error), + + /// Failure to obtain an interface GUID. + #[error("Failed to obtain GUID for the interface")] + ObtainInterfaceGuid(#[source] io::Error), + + /// Failed to set DNS settings on interface. + #[error("Failed to set DNS settings on interface")] + SetInterfaceDnsSettings(#[source] io::Error), + + /// Failure to flush DNS cache. + #[error("Failed to flush DNS resolver cache")] + FlushResolverCache(#[source] super::dnsapi::Error), + + /// Failed to load iphlpapi.dll. + #[error("Failed to load iphlpapi.dll")] + LoadDll(#[source] io::Error), + + /// Failed to obtain exported function. + #[error("Failed to obtain DNS function")] + GetFunction(#[source] io::Error), +} + +type SetInterfaceDnsSettingsFn = unsafe extern "system" fn( + interface: GUID, + settings: *const DNS_INTERFACE_SETTINGS, +) -> WIN32_ERROR; + +struct IphlpApi { + set_interface_dns_settings: SetInterfaceDnsSettingsFn, +} + +unsafe impl Send for IphlpApi {} +unsafe impl Sync for IphlpApi {} + +static IPHLPAPI_HANDLE: OnceCell<IphlpApi> = OnceCell::new(); + +impl IphlpApi { + fn new() -> Result<Self, Error> { + let module = unsafe { + LoadLibraryExW( + w!("iphlpapi.dll"), + ptr::null_mut(), + LOAD_LIBRARY_SEARCH_SYSTEM32, + ) + }; + if module.is_null() { + log::error!("Failed to load iphlpapi.dll"); + return Err(Error::LoadDll(io::Error::last_os_error())); + } + + // This function is loaded at runtime since it may be unavailable. See the module-level + // docs. TODO: `windows_sys` can be used directly when support for versions older + // than Windows 10, 2004, is dropped. + let set_interface_dns_settings = + unsafe { GetProcAddress(module, s!("SetInterfaceDnsSettings")) }; + let set_interface_dns_settings = set_interface_dns_settings.ok_or_else(|| { + let error = io::Error::last_os_error(); + + if error.raw_os_error() != Some(ERROR_PROC_NOT_FOUND as i32) { + log::error!( + "Could not find SetInterfaceDnsSettings due to an unexpected error: {error}" + ); + } + + unsafe { FreeLibrary(module) }; + Error::GetFunction(error) + })?; + + // NOTE: Leaking `module` here, since we're lazily initializing it + + Ok(Self { + set_interface_dns_settings: unsafe { + *((&raw const set_interface_dns_settings).cast()) + }, + }) + } +} + +pub struct DnsMonitor { + current_guid: Option<GUID>, +} + +impl DnsMonitor { + pub fn is_supported() -> bool { + IPHLPAPI_HANDLE.get_or_try_init(IphlpApi::new).is_ok() + } +} + +impl DnsMonitorT for DnsMonitor { + type Error = Error; + + fn new() -> Result<Self, Error> { + Ok(DnsMonitor { current_guid: None }) + } + + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), Error> { + let servers = config.tunnel_config(); + let guid = guid_from_luid(&luid_from_alias(interface).map_err(Error::ObtainInterfaceLuid)?) + .map_err(Error::ObtainInterfaceGuid)?; + + let mut v4_servers = vec![]; + let mut v6_servers = vec![]; + + for server in servers { + match server { + IpAddr::V4(addr) => v4_servers.push(addr), + IpAddr::V6(addr) => v6_servers.push(addr), + } + } + + self.current_guid = Some(guid); + + if !v4_servers.is_empty() { + set_interface_dns_servers_v4(&guid, &v4_servers)?; + } + if !v6_servers.is_empty() { + set_interface_dns_servers_v6(&guid, &v6_servers)?; + } + + flush_dns_cache()?; + + Ok(()) + } + + fn reset(&mut self) -> Result<(), Error> { + if let Some(guid) = self.current_guid.take() { + set_interface_dns_servers_v4(&guid, &[]) + .and(set_interface_dns_servers_v6(&guid, &[])) + .and(flush_dns_cache())?; + } + Ok(()) + } + + fn reset_before_interface_removal(&mut self) -> Result<(), Self::Error> { + // do nothing since the tunnel interface goes away + let _ = self.current_guid.take(); + Ok(()) + } +} + +fn set_interface_dns_servers_v4(guid: &GUID, servers: &[&Ipv4Addr]) -> Result<(), Error> { + set_interface_dns_servers(guid, servers, DNS_SETTING_NAMESERVER) +} + +fn set_interface_dns_servers_v6(guid: &GUID, servers: &[&Ipv6Addr]) -> Result<(), Error> { + set_interface_dns_servers(guid, servers, DNS_SETTING_NAMESERVER | DNS_SETTING_IPV6) +} + +fn set_interface_dns_servers<T: ToString>( + guid: &GUID, + servers: &[T], + flags: u32, +) -> Result<(), Error> { + let iphlpapi = IPHLPAPI_HANDLE.get_or_try_init(IphlpApi::new)?; + + // Create comma-separated nameserver list + let nameservers = servers + .iter() + .map(|addr| addr.to_string()) + .collect::<Vec<String>>() + .join(","); + let mut nameservers: Vec<u16> = OsString::from(nameservers) + .encode_wide() + .chain(std::iter::once(0u16)) + .collect(); + + let dns_interface_settings = DNS_INTERFACE_SETTINGS { + Version: DNS_INTERFACE_SETTINGS_VERSION1, + Flags: u64::from(flags), + Domain: ptr::null_mut(), + NameServer: nameservers.as_mut_ptr(), + SearchList: ptr::null_mut(), + RegistrationEnabled: 0, + RegisterAdapterName: 0, + EnableLLMNR: 0, + QueryAdapterName: 0, + ProfileNameServer: ptr::null_mut(), + }; + + win32_err!(unsafe { + (iphlpapi.set_interface_dns_settings)(guid.to_owned(), &raw const dns_interface_settings) + }) + .map_err(Error::SetInterfaceDnsSettings) +} + +fn flush_dns_cache() -> Result<(), Error> { + super::dnsapi::flush_resolver_cache().map_err(Error::FlushResolverCache) +} diff --git a/talpid-dns/src/windows/mod.rs b/talpid-dns/src/windows/mod.rs new file mode 100644 index 0000000000..138f75c35f --- /dev/null +++ b/talpid-dns/src/windows/mod.rs @@ -0,0 +1,96 @@ +use std::{env, fmt}; + +use super::{DnsMonitorT, ResolvedDnsConfig}; + +mod auto; +mod dnsapi; +mod iphlpapi; +mod netsh; +mod tcpip; + +/// Errors that can happen when configuring DNS on Windows. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to set DNS config using the iphlpapi module. + #[error("Error in iphlpapi module")] + Iphlpapi(#[from] iphlpapi::Error), + + /// Failed to set DNS config using the netsh module. + #[error("Error in netsh module")] + Netsh(#[from] netsh::Error), + + /// Failed to set DNS config using the tcpip module. + #[error("Error in tcpip module")] + Tcpip(#[from] tcpip::Error), +} + +pub struct DnsMonitor { + inner: DnsMonitorHolder, +} + +impl DnsMonitorT for DnsMonitor { + type Error = Error; + + fn new() -> Result<Self, Error> { + let dns_module = env::var_os("TALPID_DNS_MODULE"); + + let inner = match dns_module.as_ref().and_then(|value| value.to_str()) { + Some("iphlpapi") => DnsMonitorHolder::Iphlpapi(iphlpapi::DnsMonitor::new()?), + Some("tcpip") => DnsMonitorHolder::Tcpip(tcpip::DnsMonitor::new()?), + Some("netsh") => DnsMonitorHolder::Netsh(netsh::DnsMonitor::new()?), + Some(_) | None => DnsMonitorHolder::Auto(auto::DnsMonitor::new()?), + }; + + log::debug!("DNS monitor: {}", inner); + + Ok(DnsMonitor { inner }) + } + + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), Error> { + match self.inner { + DnsMonitorHolder::Auto(ref mut inner) => inner.set(interface, config)?, + DnsMonitorHolder::Iphlpapi(ref mut inner) => inner.set(interface, config)?, + DnsMonitorHolder::Netsh(ref mut inner) => inner.set(interface, config)?, + DnsMonitorHolder::Tcpip(ref mut inner) => inner.set(interface, config)?, + } + Ok(()) + } + + fn reset(&mut self) -> Result<(), Error> { + match self.inner { + DnsMonitorHolder::Auto(ref mut inner) => inner.reset()?, + DnsMonitorHolder::Iphlpapi(ref mut inner) => inner.reset()?, + DnsMonitorHolder::Netsh(ref mut inner) => inner.reset()?, + DnsMonitorHolder::Tcpip(ref mut inner) => inner.reset()?, + } + Ok(()) + } + + fn reset_before_interface_removal(&mut self) -> Result<(), Error> { + match self.inner { + DnsMonitorHolder::Auto(ref mut inner) => inner.reset_before_interface_removal()?, + DnsMonitorHolder::Iphlpapi(ref mut inner) => inner.reset_before_interface_removal()?, + DnsMonitorHolder::Netsh(ref mut inner) => inner.reset_before_interface_removal()?, + DnsMonitorHolder::Tcpip(ref mut inner) => inner.reset_before_interface_removal()?, + } + Ok(()) + } +} + +enum DnsMonitorHolder { + Auto(auto::DnsMonitor), + Iphlpapi(iphlpapi::DnsMonitor), + Netsh(netsh::DnsMonitor), + Tcpip(tcpip::DnsMonitor), +} + +impl fmt::Display for DnsMonitorHolder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DnsMonitorHolder::Auto(_) => f.write_str("auto (iphlpapi > netsh > tcpip)"), + DnsMonitorHolder::Iphlpapi(_) => f.write_str("SetInterfaceDnsSettings (iphlpapi)"), + DnsMonitorHolder::Netsh(_) => f.write_str("netsh"), + DnsMonitorHolder::Tcpip(_) => f.write_str("TCP/IP registry parameter"), + } + } +} diff --git a/talpid-dns/src/windows/netsh.rs b/talpid-dns/src/windows/netsh.rs new file mode 100644 index 0000000000..0f8e696c76 --- /dev/null +++ b/talpid-dns/src/windows/netsh.rs @@ -0,0 +1,213 @@ +#![allow(clippy::undocumented_unsafe_blocks)] // Remove me if you dare. + +use super::{DnsMonitorT, ResolvedDnsConfig}; +use std::{ + io::{self, Write}, + net::IpAddr, + os::windows::prelude::AsRawHandle, + process::{Child, Command, ExitStatus, Stdio}, + time::Duration, +}; +use talpid_types::{ErrorExt, net::IpVersion}; +use talpid_windows::{ + env::get_system_dir, + net::{index_from_luid, luid_from_alias}, +}; +use windows_sys::Win32::{ + Foundation::{WAIT_OBJECT_0, WAIT_TIMEOUT}, + System::Threading::{INFINITE, WaitForSingleObject}, +}; + +const NETSH_TIMEOUT: Duration = Duration::from_secs(10); + +/// Errors that can happen when configuring DNS on Windows. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failure to obtain an interface LUID given an alias. + #[error("Failed to obtain LUID for the interface alias")] + ObtainInterfaceLuid(#[source] io::Error), + + /// Failure to obtain an interface index. + #[error("Failed to obtain index of the interface")] + ObtainInterfaceIndex(#[source] io::Error), + + /// Failure to spawn netsh subprocess. + #[error("Failed to spawn 'netsh'")] + SpawnNetsh(#[source] io::Error), + + /// Failure to spawn netsh subprocess. + #[error("Failed to obtain system directory")] + GetSystemDir(#[source] io::Error), + + /// Failure to write to stdin. + #[error("Failed to write to stdin for 'netsh'")] + NetshInput(#[source] io::Error), + + /// Failure to wait for netsh result. + #[error("Failed to wait for 'netsh'")] + WaitNetsh(#[source] io::Error), + + /// netsh returned a non-zero status. + #[error("'netsh' returned an error: {0:?}")] + Netsh(Option<i32>), + + /// netsh did not return in a timely manner. + #[error("'netsh' took too long to complete")] + NetshTimeout, +} + +pub struct DnsMonitor { + current_index: Option<u32>, +} + +impl DnsMonitorT for DnsMonitor { + type Error = Error; + + fn new() -> Result<Self, Error> { + Ok(DnsMonitor { + current_index: None, + }) + } + + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), Error> { + let servers = config.tunnel_config(); + let interface_luid = luid_from_alias(interface).map_err(Error::ObtainInterfaceLuid)?; + let interface_index = + index_from_luid(&interface_luid).map_err(Error::ObtainInterfaceIndex)?; + + self.current_index = Some(interface_index); + + let mut added_ipv4_server = false; + let mut added_ipv6_server = false; + + let mut netsh_input = String::new(); + + for server in servers { + let is_additional_server; + + if server.is_ipv4() { + is_additional_server = added_ipv4_server; + added_ipv4_server = true; + } else { + is_additional_server = added_ipv6_server; + added_ipv6_server = true; + }; + + if is_additional_server { + netsh_input.push_str(&create_netsh_add_command(interface_index, server)); + } else { + netsh_input.push_str(&create_netsh_set_command(interface_index, server)); + } + } + + if !added_ipv4_server { + netsh_input.push_str(&create_netsh_flush_command(interface_index, IpVersion::V4)); + } + if !added_ipv6_server { + netsh_input.push_str(&create_netsh_flush_command(interface_index, IpVersion::V6)); + } + + run_netsh_with_timeout(netsh_input, NETSH_TIMEOUT)?; + + Ok(()) + } + + fn reset(&mut self) -> Result<(), Error> { + if let Some(index) = self.current_index.take() { + let mut netsh_input = String::new(); + netsh_input.push_str(&create_netsh_flush_command(index, IpVersion::V4)); + netsh_input.push_str(&create_netsh_flush_command(index, IpVersion::V6)); + + if let Err(error) = run_netsh_with_timeout(netsh_input, NETSH_TIMEOUT) { + log::error!("{}", error.display_chain_with_msg("Failed to reset DNS")); + } + } + Ok(()) + } + + fn reset_before_interface_removal(&mut self) -> Result<(), Self::Error> { + // do nothing since the tunnel interface goes away + let _ = self.current_index.take(); + Ok(()) + } +} + +fn run_netsh_with_timeout(netsh_input: String, timeout: Duration) -> Result<(), Error> { + log::debug!("running netsh:\n{}", netsh_input); + + let sysdir = get_system_dir().map_err(Error::GetSystemDir)?; + let mut netsh = Command::new(sysdir.join(r"netsh.exe")); + + let mut subproc = netsh + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(Error::SpawnNetsh)?; + + let mut stdin = subproc.stdin.take().unwrap(); + stdin + .write_all(netsh_input.as_bytes()) + .map_err(Error::NetshInput)?; + drop(stdin); + + match wait_for_child(&mut subproc, timeout) { + Ok(Some(status)) => { + if !status.success() { + return Err(Error::Netsh(status.code())); + } + Ok(()) + } + Ok(None) => { + let _ = subproc.kill(); + Err(Error::NetshTimeout) + } + Err(error) => Err(Error::WaitNetsh(error)), + } +} + +fn wait_for_child(subproc: &mut Child, timeout: Duration) -> io::Result<Option<ExitStatus>> { + let dur_millis = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE); + + let subproc_handle = subproc.as_raw_handle(); + match unsafe { WaitForSingleObject(subproc_handle, dur_millis) } { + WAIT_OBJECT_0 => subproc.try_wait(), + WAIT_TIMEOUT => Ok(None), + _error => Err(io::Error::last_os_error()), + } +} + +fn create_netsh_set_command(interface_index: u32, server: &IpAddr) -> String { + // Set primary DNS server: + // netsh interface ipv4 set dnsservers name="Mullvad" source=static address=10.64.0.1 + // validate=no + + let interface_type = if server.is_ipv4() { "ipv4" } else { "ipv6" }; + format!( + "interface {interface_type} set dnsservers name={interface_index} source=static address={server} validate=no\r\n" + ) +} + +fn create_netsh_add_command(interface_index: u32, server: &IpAddr) -> String { + // Add DNS server: + // netsh interface ipv4 add dnsservers name="Mullvad" address=10.64.0.2 validate=no + + let interface_type = if server.is_ipv4() { "ipv4" } else { "ipv6" }; + format!( + "interface {interface_type} add dnsservers name={interface_index} address={server} validate=no\r\n" + ) +} + +fn create_netsh_flush_command(interface_index: u32, ip_version: IpVersion) -> String { + // Flush DNS settings: + // netsh interface ipv4 set dnsservers name="Mullvad" source=static address=none validate=no + + let interface_type = match ip_version { + IpVersion::V4 => "ipv4", + IpVersion::V6 => "ipv6", + }; + + format!( + "interface {interface_type} set dnsservers name={interface_index} source=static address=none validate=no\r\n" + ) +} diff --git a/talpid-dns/src/windows/tcpip.rs b/talpid-dns/src/windows/tcpip.rs new file mode 100644 index 0000000000..db9c3c1240 --- /dev/null +++ b/talpid-dns/src/windows/tcpip.rs @@ -0,0 +1,177 @@ +use super::{DnsMonitorT, ResolvedDnsConfig}; +use std::{io, net::IpAddr}; +use talpid_types::ErrorExt; +use talpid_windows::net::{guid_from_luid, luid_from_alias}; +use windows_sys::{Win32::System::Com::StringFromGUID2, core::GUID}; +use winreg::{ + RegKey, + enums::{HKEY_LOCAL_MACHINE, KEY_SET_VALUE}, + transaction::Transaction, +}; + +/// Errors that can happen when configuring DNS on Windows. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failure to obtain an interface LUID given an alias. + #[error("Failed to obtain LUID for the interface alias")] + ObtainInterfaceLuid(#[source] io::Error), + + /// Failure to obtain an interface GUID. + #[error("Failed to obtain GUID for the interface")] + ObtainInterfaceGuid(#[source] io::Error), + + /// Failure to flush DNS cache. + #[error("Failed to flush DNS resolver cache")] + FlushResolverCache(#[source] super::dnsapi::Error), + + /// Failed to update DNS servers for interface. + #[error("Failed to update interface DNS servers")] + SetResolvers(#[source] io::Error), +} + +pub struct DnsMonitor { + current_guid: Option<GUID>, + should_flush: bool, +} + +impl DnsMonitorT for DnsMonitor { + type Error = Error; + + fn new() -> Result<Self, Error> { + Ok(DnsMonitor { + current_guid: None, + should_flush: true, + }) + } + + fn set(&mut self, interface: &str, config: ResolvedDnsConfig) -> Result<(), Error> { + let servers = config.tunnel_config(); + + let guid = guid_from_luid(&luid_from_alias(interface).map_err(Error::ObtainInterfaceLuid)?) + .map_err(Error::ObtainInterfaceGuid)?; + set_dns(&guid, servers)?; + self.current_guid = Some(guid); + if self.should_flush { + flush_dns_cache()?; + } + Ok(()) + } + + fn reset(&mut self) -> Result<(), Error> { + if let Some(guid) = self.current_guid.take() { + let mut result = set_dns(&guid, &[]); + if self.should_flush { + result = result.and(flush_dns_cache()); + } + return result; + } + Ok(()) + } +} + +impl DnsMonitor { + pub fn disable_flushing(&mut self) { + self.should_flush = false; + } +} + +fn set_dns(interface: &GUID, servers: &[IpAddr]) -> Result<(), Error> { + let transaction = Transaction::new().map_err(Error::SetResolvers)?; + let result = match set_dns_inner(&transaction, interface, servers) { + Ok(()) => transaction.commit(), + Err(error) => transaction.rollback().and(Err(error)), + }; + result.map_err(Error::SetResolvers) +} + +fn set_dns_inner( + transaction: &Transaction, + interface: &GUID, + servers: &[IpAddr], +) -> io::Result<()> { + let guid_str = string_from_guid(interface); + + config_interface( + transaction, + &guid_str, + "Tcpip", + servers.iter().filter(|addr| addr.is_ipv4()).copied(), + )?; + + config_interface( + transaction, + &guid_str, + "Tcpip6", + servers.iter().filter(|addr| addr.is_ipv6()).copied(), + )?; + + Ok(()) +} + +fn config_interface( + transaction: &Transaction, + guid: &str, + service: &str, + nameservers: impl Iterator<Item = IpAddr>, +) -> io::Result<()> { + let nameservers = nameservers + .map(|addr| addr.to_string()) + .collect::<Vec<String>>(); + + let reg_path = + format!(r#"SYSTEM\CurrentControlSet\Services\{service}\Parameters\Interfaces\{guid}"#,); + let adapter_key = match RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey_transacted_with_flags( + reg_path, + transaction, + KEY_SET_VALUE, + ) { + Ok(adapter_key) => Ok(adapter_key), + Err(error) => { + if nameservers.is_empty() && error.kind() == io::ErrorKind::NotFound { + return Ok(()); + } + Err(error) + } + }?; + + if !nameservers.is_empty() { + adapter_key.set_value("NameServer", &nameservers.join(","))?; + } else { + adapter_key.delete_value("NameServer").or_else(|error| { + if error.kind() == io::ErrorKind::NotFound { + Ok(()) + } else { + Err(error) + } + })?; + } + + // Try to disable LLMNR on the interface + if let Err(error) = adapter_key.set_value("EnableMulticast", &0u32) { + log::error!( + "{}\nService: {service}", + error.display_chain_with_msg("Failed to disable LLMNR on the tunnel interface") + ); + } + + Ok(()) +} + +fn flush_dns_cache() -> Result<(), Error> { + super::dnsapi::flush_resolver_cache().map_err(Error::FlushResolverCache) +} + +/// Obtain a string representation for a GUID object. +fn string_from_guid(guid: &GUID) -> String { + let mut buffer = [0u16; 40]; + + let length = + // SAFETY: `guid` and `buffer` are valid references. + // StringFromGUID2 won't write past the end of the provided length. + unsafe { StringFromGUID2(guid, buffer.as_mut_ptr(), buffer.len() as i32 - 1) } as usize; + + // cannot fail because `buffer` is large enough + assert!(length > 0); + let length = length - 1; + String::from_utf16(&buffer[0..length]).unwrap() +} |
