summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-11-20 18:17:26 +0100
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-11-26 11:14:40 +0100
commitad3ccfefe8ba3c329c1df32f5b7cdb2180276575 (patch)
tree27618f677c141a4559f24bd13517c8e657ba4cb2
parent992703fd6bd27f2d627675a41d9d114bcda4729b (diff)
downloadmullvadvpn-ad3ccfefe8ba3c329c1df32f5b7cdb2180276575.tar.xz
mullvadvpn-ad3ccfefe8ba3c329c1df32f5b7cdb2180276575.zip
Add talpid-dns crate
-rw-r--r--Cargo.lock25
-rw-r--r--Cargo.toml1
-rw-r--r--mullvad-daemon/Cargo.toml1
-rw-r--r--mullvad-daemon/src/dns.rs8
-rw-r--r--talpid-dns/Cargo.toml64
-rw-r--r--talpid-dns/src/android.rs24
-rw-r--r--talpid-dns/src/lib.rs226
-rw-r--r--talpid-dns/src/linux/interface.rs16
-rw-r--r--talpid-dns/src/linux/mod.rs180
-rw-r--r--talpid-dns/src/linux/network_manager.rs53
-rw-r--r--talpid-dns/src/linux/resolvconf.rs197
-rw-r--r--talpid-dns/src/linux/static_resolv_conf.rs237
-rw-r--r--talpid-dns/src/linux/systemd_resolved.rs83
-rw-r--r--talpid-dns/src/macos.rs757
-rw-r--r--talpid-dns/src/windows/auto.rs110
-rw-r--r--talpid-dns/src/windows/dnsapi.rs96
-rw-r--r--talpid-dns/src/windows/iphlpapi.rs224
-rw-r--r--talpid-dns/src/windows/mod.rs96
-rw-r--r--talpid-dns/src/windows/netsh.rs213
-rw-r--r--talpid-dns/src/windows/tcpip.rs177
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()
+}