Skip to content

Commit

Permalink
Fix DNS server order to precisely match the way Windows works
Browse files Browse the repository at this point in the history
  • Loading branch information
Jacob Halsey committed Jun 21, 2022
1 parent 9b631b8 commit 1c6d4a9
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 186 deletions.
59 changes: 1 addition & 58 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
[package]
name = "wsl2-dns-agent"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0"
description = "An agent that automatically patches your WSL2 DNS configuration for users of Cisco AnyConnect (or similar VPNs)"
repository = "https://github.com/jacob-pro/wsl2-dns-agent"
homepage = "https://github.com/jacob-pro/wsl2-dns-agent"

[dependencies]
backoff = "0.4.0"
chrono = "0.4.19"
configparser = "3.0.0"
dirs = "4.0.0"
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ Launch the application.

Note: You must have the `chattr` command installed within your WSL2 distribution.

## Diagnostics

You can view the application log by clicking on the tray icon and "View Log".

Note that this tool *should* apply DNS servers based on their priority in Windows.

For example, from Windows Command Prompt try running:

```cmd
C:\Users\jdhalsey>nslookup.exe google.com
Server: OpenWrt.lan
Address: 10.2.9.254
Non-authoritative answer: ...
```

Therefore `10.2.9.254` will be the first server written to `/etc/resolv.conf`. If the server is not what you expected
then please look at [the DNS guide](./docs/ROUTING.md#step-3---working-windows-dns)

## Advanced options

For advanced use cases you can edit the config file in `%APPDATA%\WSL2 DNS Agent\config.toml`
Expand Down
163 changes: 38 additions & 125 deletions src/dns.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use crate::APP_NAME;
use backoff::ExponentialBackoffBuilder;
use itertools::Itertools;
use std::fmt::{Display, Formatter};
use std::mem::transmute;
use std::net::IpAddr;
use std::ptr::{null_mut, slice_from_raw_parts};
use std::time::Duration;
use thiserror::Error;
use win32_utils::net::ToStdSocket;
use win32_utils::str::FromWin32Str;
Expand All @@ -19,7 +16,6 @@ use windows::Win32::Networking::WinSock::AF_UNSPEC;
#[derive(Debug)]
struct Route {
interface_index: u32,
metric: u32,
destination_prefix_ip: IpAddr,
destination_prefix_len: u8,
}
Expand All @@ -31,8 +27,8 @@ impl Route {
}
}

/// Returns list of routes to 0.0.0.0/0 and ::/0
fn get_internet_routes() -> Result<Vec<Route>, Error> {
/// Returns a list of IPv4 and IPv6 routes
fn get_routes() -> Result<Vec<Route>, Error> {
unsafe {
let mut ptr = null_mut::<MIB_IPFORWARD_TABLE2>();
GetIpForwardTable2(AF_UNSPEC.0 as u16, &mut ptr).map_err(Error::GetIpForwardTable2)?;
Expand All @@ -46,11 +42,9 @@ fn get_internet_routes() -> Result<Vec<Route>, Error> {
.map(|idx| &table[idx as usize])
.map(|row| Route {
interface_index: row.InterfaceIndex,
metric: row.Metric,
destination_prefix_ip: row.DestinationPrefix.Prefix.to_std_socket_addr().ip(),
destination_prefix_len: row.DestinationPrefix.PrefixLength,
})
.filter(Route::is_internet_route)
.collect::<Vec<_>>();
FreeMibTable(transmute(ptr));
Ok(res)
Expand Down Expand Up @@ -131,46 +125,15 @@ fn get_adapters() -> Result<Vec<Adapter>, Error> {
}
}

#[derive(Debug)]
struct RouteAndAdapter<'a> {
route: &'a Route,
adapter: &'a Adapter,
}

impl RouteAndAdapter<'_> {
/// "the overall metric that is used to determine the interface preference is the sum of the
/// route metric and the interface metric"
fn metric_sum(&self) -> u32 {
self.route.metric
+ match self.route.destination_prefix_ip {
IpAddr::V4(_) => self.adapter.ipv4_metric,
IpAddr::V6(_) => self.adapter.ipv6_metric,
}
}
}

impl Display for RouteAndAdapter<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let m_sum = self.metric_sum();
let s = format!(
"{}/{}, interface index: {}, metric: {} ({} + {}), dns servers: {:?}, dns suffixes: {:?}",
self.route.destination_prefix_ip,
self.route.destination_prefix_len,
self.route.interface_index,
m_sum,
self.route.metric,
m_sum - self.route.metric,
self.adapter.dns_servers,
self.adapter.dns_suffixes
);
f.write_str(s.as_str())
impl Adapter {
// For the purposes of DNS, the interface metric is whichever one is lowest
fn interface_metric(&self) -> u32 {
std::cmp::min(self.ipv4_metric, self.ipv6_metric)
}
}

#[derive(Debug, Error)]
pub enum Error {
#[error("Unable to find adapter for interface: {interface_index}")]
RouteInterfaceMismatch { interface_index: u32 },
#[error("Calls to GetAdaptersAddresses() returned different buffer sizes")]
GetAdaptersAddressesOverflow,
#[error("Call to GetIpForwardTable2() failed: {0}")]
Expand All @@ -179,97 +142,47 @@ pub enum Error {
GetAdaptersAddresses(#[source] windows::core::Error),
}

impl Error {
/// Some errors should be retried
pub fn into_backoff(self) -> backoff::Error<Self> {
match &self {
Error::RouteInterfaceMismatch { .. } => self.into(),
Error::GetAdaptersAddressesOverflow { .. } => self.into(),
_ => backoff::Error::Permanent(self),
}
}
}

impl From<backoff::Error<Error>> for Error {
fn from(e: backoff::Error<Error>) -> Self {
match e {
backoff::Error::Permanent(e) => e,
backoff::Error::Transient { err, .. } => err,
}
}
}

#[derive(Debug, Default)]
pub struct DnsConfiguration {
servers: Vec<IpAddr>,
suffixes: Vec<String>,
}

pub fn get_configuration() -> Result<DnsConfiguration, Error> {
let op = || {
{
let routes = get_internet_routes()?;
let adapters = get_adapters()?;
// Match the route interface index with an adapter index
let mut grouped = routes
// List of routes to the internet
let internet_routes = get_routes()?
.into_iter()
.filter(Route::is_internet_route)
.collect::<Vec<_>>();

// DNS priority is determined by interface metric
// However we also want to exclude various system adapters such as WSL
// so we will filter out any adapters that have a route to the internet
let internet_adapters = get_adapters()?
.into_iter()
.filter(|adapter| {
internet_routes
.iter()
.map(|r| {
match r.destination_prefix_ip {
IpAddr::V4(_) => adapters
.iter()
.find(|a| a.ipv4_interface_index.eq(&r.interface_index)),
IpAddr::V6(_) => adapters
.iter()
.find(|a| a.ipv6_interface_index.eq(&r.interface_index)),
}
.ok_or(Error::RouteInterfaceMismatch {
interface_index: r.interface_index,
})
.map(|a| RouteAndAdapter {
route: r,
adapter: a,
})
.any(|route| match route.destination_prefix_ip {
IpAddr::V4(_) => route.interface_index == adapter.ipv4_interface_index,
IpAddr::V6(_) => route.interface_index == adapter.ipv6_interface_index,
})
.collect::<Result<Vec<_>, Error>>()?;
// Sort by the lowest route metrics
grouped.sort_by_key(|r| r.metric_sum());
// Get the best routes for IPv4 and IPv6 internets respectively
let best_v4 = grouped
.iter()
.find(|g| g.route.destination_prefix_ip.is_ipv4());
if let Some(best_v4) = best_v4 {
log::info!("Best IPv4 Route: {}", best_v4);
}
let best_v6 = grouped
.iter()
.find(|g| g.route.destination_prefix_ip.is_ipv6());
if let Some(best_v6) = best_v6 {
log::info!("Best IPv6 Route: {}", best_v6);
}
// Collect the IPv4 and then IPv6 dns configurations
let mut dns_servers = Vec::new();
let mut dns_suffixes = Vec::new();
best_v4.iter().chain(best_v6.iter()).for_each(|g| {
g.adapter.dns_servers.iter().for_each(|d| {
dns_servers.push(d.to_owned());
});
g.adapter.dns_suffixes.iter().for_each(|d| {
dns_suffixes.push(d.to_owned());
});
});
// Ensure servers and suffixes are unique (preserving order)
Ok(DnsConfiguration {
servers: dns_servers.into_iter().unique().collect(),
suffixes: dns_suffixes.into_iter().unique().collect(),
})
}
.map_err(Error::into_backoff)
};
let b = ExponentialBackoffBuilder::new()
.with_initial_interval(Duration::from_millis(50))
.with_max_elapsed_time(Some(Duration::from_secs(1)))
.build();
backoff::retry(b, op).map_err(|e| e.into())
})
.sorted_by_key(Adapter::interface_metric)
.collect::<Vec<_>>();

let servers = internet_adapters
.iter()
.flat_map(|adapter| adapter.dns_servers.clone())
.unique()
.collect::<Vec<_>>();
let suffixes = internet_adapters
.iter()
.flat_map(|adapter| adapter.dns_suffixes.clone())
.unique()
.collect::<Vec<_>>();

Ok(DnsConfiguration { servers, suffixes })
}

impl DnsConfiguration {
Expand Down
2 changes: 1 addition & 1 deletion src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ enum Error {
fn update_dns(config: &Config) -> Result<(), Error> {
let dns = dns::get_configuration()?;
let resolv = dns.generate_resolv();
log::info!("Applying DNS config: {dns:?}");
log::info!("Detected Windows DNS config: {dns:?}");
let wsl = wsl::get_distributions()?
.into_iter()
.filter(|d| d.version == 2)
Expand Down

0 comments on commit 1c6d4a9

Please sign in to comment.