diff options
| author | David Lönnhager <david.l@mullvad.net> | 2023-03-01 15:07:13 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2023-03-01 15:07:13 +0100 |
| commit | 2bd5e536d34bb60ba4a77fb6d37f2c5eff23463c (patch) | |
| tree | ca569321f905e3b23a411a759e4bd39bb7e131ce | |
| parent | afcd5313b2daacf1c4f2dca138de0fc0745fb96e (diff) | |
| parent | 34d96528d55d78889be936dda556cb1d5c3cc3c7 (diff) | |
| download | mullvadvpn-2bd5e536d34bb60ba4a77fb6d37f2c5eff23463c.tar.xz mullvadvpn-2bd5e536d34bb60ba4a77fb6d37f2c5eff23463c.zip | |
Merge branch 'win-iphlpapi-dns-method' into main
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 1 | ||||
| -rw-r--r-- | talpid-core/src/dns/windows/auto.rs | 112 | ||||
| -rw-r--r-- | talpid-core/src/dns/windows/iphlpapi.rs | 212 | ||||
| -rw-r--r-- | talpid-core/src/dns/windows/mod.rs | 24 | ||||
| -rw-r--r-- | talpid-core/src/dns/windows/tcpip.rs | 22 |
7 files changed, 371 insertions, 5 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 148e65b012..fcf376d7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ Line wrap the file at 100 chars. Th - Add setting for quantum resistant tunnels to the desktop GUI. - Enable `TCP_NODELAY` for the socket used by WireGuard over TCP. Improves latency and performance. +#### Windows +- Use `SetInterfaceDnsSettings` to config DNS when it's available (on Windows 10, version 1809 and + above). + ### Changed - Update the Post-Quantum secure key exchange gRPC client to use the stabilized `PskExchangeV1` endpoint @@ -139,6 +139,7 @@ See [this](Release.md) for instructions on how to make a new release. * `"network-manager"`: use `NetworkManager` service through DBus * Windows + * `iphlpapi`: use the IP helper API * `netsh`: use the `netsh` program * `tcpip`: set TCP/IP parameters in the registry diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 24203d4a11..285d0c0d1b 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -94,6 +94,7 @@ features = [ "Win32_System_LibraryLoader", "Win32_System_ProcessStatus", "Win32_System_Registry", + "Win32_System_Rpc", "Win32_System_Services", "Win32_System_SystemServices", "Win32_System_Threading", diff --git a/talpid-core/src/dns/windows/auto.rs b/talpid-core/src/dns/windows/auto.rs new file mode 100644 index 0000000000..a09804b700 --- /dev/null +++ b/talpid-core/src/dns/windows/auto.rs @@ -0,0 +1,112 @@ +use super::{iphlpapi, netsh, tcpip}; +use crate::dns::DnsMonitorT; +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, servers: &[std::net::IpAddr]) -> Result<(), super::Error> { + match self { + InnerMonitor::Iphlpapi(monitor) => monitor.set(interface, servers)?, + InnerMonitor::Netsh(monitor) => monitor.set(interface, servers)?, + InnerMonitor::Tcpip(monitor) => monitor.set(interface, servers)?, + } + 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() { + current_monitor = InnerMonitor::Iphlpapi(iphlpapi::DnsMonitor::new()?); + } else { + current_monitor = InnerMonitor::Netsh(netsh::DnsMonitor::new()?); + } + + Ok(Self { current_monitor }) + } + + fn set(&mut self, interface: &str, servers: &[std::net::IpAddr]) -> Result<(), Self::Error> { + let result = self.current_monitor.set(interface, servers); + if self.fallback_due_to_dnscache(&result) { + return self.set(interface, servers); + } + 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 == RPC_S_SERVER_UNAVAILABLE + } + Err(super::Error::Netsh(netsh::Error::NetshError(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-core/src/dns/windows/iphlpapi.rs b/talpid-core/src/dns/windows/iphlpapi.rs new file mode 100644 index 0000000000..6f33cf3cf9 --- /dev/null +++ b/talpid-core/src/dns/windows/iphlpapi.rs @@ -0,0 +1,212 @@ +use crate::dns::DnsMonitorT; +use once_cell::sync::OnceCell; +use std::{ + ffi::OsString, + io, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + os::windows::ffi::OsStrExt, + ptr, +}; +use talpid_windows_net::{guid_from_luid, luid_from_alias}; +use windows_sys::{ + core::GUID, + s, w, + Win32::{ + Foundation::{ERROR_PROC_NOT_FOUND, NO_ERROR, NTSTATUS}, + NetworkManagement::IpHelper::{ + DNS_INTERFACE_SETTINGS, DNS_INTERFACE_SETTINGS_VERSION1, DNS_SETTING_IPV6, + DNS_SETTING_NAMESERVER, + }, + System::LibraryLoader::{ + FreeLibrary, GetProcAddress, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32, + }, + }, +}; + +/// Errors that can happen when configuring DNS on Windows. +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +pub enum Error { + /// Failure to obtain an interface LUID given an alias. + #[error(display = "Failed to obtain LUID for the interface alias")] + InterfaceLuidError(#[error(source)] io::Error), + + /// Failure to obtain an interface GUID. + #[error(display = "Failed to obtain GUID for the interface")] + InterfaceGuidError(#[error(source)] io::Error), + + /// Failed to set DNS settings on interface. + #[error(display = "Failed to set DNS settings on interface: {}", _0)] + SetInterfaceDnsSettings(i32), + + /// Failure to flush DNS cache. + #[error(display = "Failed to flush DNS resolver cache")] + FlushResolverCacheError(#[error(source)] super::dnsapi::Error), + + /// Failed to load iphlpapi.dll. + #[error(display = "Failed to load iphlpapi.dll")] + LoadDll(#[error(source)] io::Error), + + /// Failed to obtain exported function. + #[error(display = "Failed to obtain DNS function")] + GetFunction(#[error(source)] io::Error), +} + +type SetInterfaceDnsSettingsFn = unsafe extern "stdcall" fn( + interface: GUID, + settings: *const DNS_INTERFACE_SETTINGS, +) -> NTSTATUS; + +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"), 0, LOAD_LIBRARY_SEARCH_SYSTEM32) }; + if module == 0 { + log::error!("Failed to load iphlpapi.dll"); + return Err(Error::LoadDll(io::Error::last_os_error())); + } + + 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 { + *(&set_interface_dns_settings as *const _ as *const _) + }, + }) + } +} + +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, servers: &[IpAddr]) -> Result<(), Error> { + let guid = guid_from_luid(&luid_from_alias(interface).map_err(Error::InterfaceLuidError)?) + .map_err(Error::InterfaceGuidError)?; + + 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(), + }; + + let result = + unsafe { (iphlpapi.set_interface_dns_settings)(guid.to_owned(), &dns_interface_settings) }; + if result != (NO_ERROR as i32) { + return Err(Error::SetInterfaceDnsSettings(result)); + } + Ok(()) +} + +fn flush_dns_cache() -> Result<(), Error> { + super::dnsapi::flush_resolver_cache().map_err(Error::FlushResolverCacheError) +} diff --git a/talpid-core/src/dns/windows/mod.rs b/talpid-core/src/dns/windows/mod.rs index 10ce85c055..4ab9976417 100644 --- a/talpid-core/src/dns/windows/mod.rs +++ b/talpid-core/src/dns/windows/mod.rs @@ -1,12 +1,20 @@ use std::{env, fmt, net::IpAddr}; +use super::DnsMonitorT; + +mod auto; mod dnsapi; +mod iphlpapi; mod netsh; mod tcpip; /// Errors that can happen when configuring DNS on Windows. #[derive(err_derive::Error, Debug)] pub enum Error { + /// Failed to set DNS config using the iphlpapi module. + #[error(display = "Error in iphlpapi module")] + Iphlpapi(#[error(source)] iphlpapi::Error), + /// Failed to set DNS config using the netsh module. #[error(display = "Error in netsh module")] Netsh(#[error(source)] netsh::Error), @@ -20,15 +28,17 @@ pub struct DnsMonitor { inner: DnsMonitorHolder, } -impl super::DnsMonitorT for DnsMonitor { +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(_) | None => DnsMonitorHolder::Netsh(netsh::DnsMonitor::new()?), + Some("netsh") => DnsMonitorHolder::Netsh(netsh::DnsMonitor::new()?), + Some(_) | None => DnsMonitorHolder::Auto(auto::DnsMonitor::new()?), }; log::debug!("DNS monitor: {}", inner); @@ -38,6 +48,8 @@ impl super::DnsMonitorT for DnsMonitor { fn set(&mut self, interface: &str, servers: &[IpAddr]) -> Result<(), Error> { match self.inner { + DnsMonitorHolder::Auto(ref mut inner) => inner.set(interface, servers)?, + DnsMonitorHolder::Iphlpapi(ref mut inner) => inner.set(interface, servers)?, DnsMonitorHolder::Netsh(ref mut inner) => inner.set(interface, servers)?, DnsMonitorHolder::Tcpip(ref mut inner) => inner.set(interface, servers)?, } @@ -46,6 +58,8 @@ impl super::DnsMonitorT for DnsMonitor { 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()?, } @@ -54,6 +68,8 @@ impl super::DnsMonitorT for DnsMonitor { 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()?, } @@ -62,6 +78,8 @@ impl super::DnsMonitorT for DnsMonitor { } enum DnsMonitorHolder { + Auto(auto::DnsMonitor), + Iphlpapi(iphlpapi::DnsMonitor), Netsh(netsh::DnsMonitor), Tcpip(tcpip::DnsMonitor), } @@ -69,6 +87,8 @@ enum DnsMonitorHolder { 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-core/src/dns/windows/tcpip.rs b/talpid-core/src/dns/windows/tcpip.rs index f7536eaed1..2fdb7e3c28 100644 --- a/talpid-core/src/dns/windows/tcpip.rs +++ b/talpid-core/src/dns/windows/tcpip.rs @@ -32,13 +32,17 @@ pub enum 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 }) + Ok(DnsMonitor { + current_guid: None, + should_flush: true, + }) } fn set(&mut self, interface: &str, servers: &[IpAddr]) -> Result<(), Error> { @@ -46,18 +50,30 @@ impl DnsMonitorT for DnsMonitor { .map_err(Error::InterfaceGuidError)?; set_dns(&guid, servers)?; self.current_guid = Some(guid); - flush_dns_cache()?; + if self.should_flush { + flush_dns_cache()?; + } Ok(()) } fn reset(&mut self) -> Result<(), Error> { if let Some(guid) = self.current_guid.take() { - return set_dns(&guid, &[]).and(flush_dns_cache()); + 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::SetResolversError)?; let result = match set_dns_inner(&transaction, interface, servers) { |
