Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ windows = { version = "0.62", features = [
"Win32_NetworkManagement_Ndis",
"Win32_Networking_WinSock",
"Win32_System_Com",
]}
] }
wireguard-nt = "0.5.0"

[target.'cfg(target_os = "linux")'.dependencies]
Expand All @@ -49,6 +49,9 @@ netlink-packet-utils = "0.6"
netlink-packet-wireguard = "0.2"
netlink-sys = "0.8"

[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))'.dependencies]
regex = "1.12"

[features]
default = ["serde"]
check_dependencies = []
Expand Down
46 changes: 14 additions & 32 deletions src/dependencies.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::env;

use crate::error::WireguardInterfaceError;
use crate::{error::WireguardInterfaceError, utils::get_command_path};

#[cfg(target_os = "linux")]
const COMMANDS: [&str; 2] = ["resolvconf", "ip"];
Expand All @@ -16,37 +16,19 @@ const COMMANDS: [&str; 1] = ["resolvconf"];

pub(crate) fn check_external_dependencies() -> Result<(), WireguardInterfaceError> {
debug!("Checking if all commands required by wireguard-rs are available");
let paths = env::var_os("PATH").ok_or(WireguardInterfaceError::MissingDependency(
"Environment variable `PATH` not found".into(),
))?;

// Find the missing command to provide a more informative error message later.
let missing = COMMANDS.iter().find(|cmd| {
!env::split_paths(&paths).any(|dir| {
trace!("Trying to find {cmd} in {dir:?}");
match dir.join(cmd).try_exists() {
Ok(true) => {
debug!("{cmd} found in {dir:?}");
true
}
Ok(false) => {
trace!("{cmd} not found in {dir:?}");
false
}
Err(err) => {
warn!("Error while checking for {cmd} in {dir:?}: {err}");
false
}
}
})
let paths = env::var_os("PATH").ok_or_else(|| {
WireguardInterfaceError::MissingDependency("Environment variable `PATH` not found".into())
});

if let Some(cmd) = missing {
Err(WireguardInterfaceError::MissingDependency(format!(
"Command `{cmd}` required by wireguard-rs couldn't be found. The following directories were checked: {paths:?}"
)))
} else {
debug!("All commands required by wireguard-rs are available");
Ok(())
}
// Find the missing command to provide a more informative error message later.
let missing_command = COMMANDS
.iter()
.find(|cmd| get_command_path(cmd).map_or(true, |path_opt| path_opt.is_none()));

missing_command.map_or_else(|| {
debug!("All commands required by wireguard-rs are available");
Ok(())
}, |cmd| Err(WireguardInterfaceError::MissingDependency(format!(
"Command `{cmd}` required by wireguard-rs couldn't be found. The following directories were checked: {paths:?}"
))))
}
94 changes: 90 additions & 4 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
#[cfg(target_os = "macos")]
use std::io::{BufRead, BufReader, Cursor, Error as IoError};
use std::io::{Cursor, Error as IoError};
#[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))]
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::{SocketAddr, ToSocketAddrs};
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
use std::path::Path;
#[cfg(target_os = "linux")]
use std::{collections::HashSet, fs::OpenOptions};
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
use std::{io::Write, process::Stdio};
use std::{
io::{BufRead, BufReader},
net::{SocketAddr, ToSocketAddrs},
};
#[cfg(not(target_os = "windows"))]
use std::{net::IpAddr, process::Command};

Expand All @@ -24,6 +29,55 @@ use crate::{
#[cfg(target_os = "linux")]
use crate::{IpVersion, check_command_output_status, netlink};

#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
const IFACE_ORDER_PATH: &str = "/etc/resolvconf/interface-order";

/// Constructs the resolvconf interface name for manipulating DNS settings.
/// Resolvconf may be symlinked to resolvectl on some systems that use systemd-resolved.
/// In such cases there is no need to prefix the interface name and this function just returns
/// the base interface name.
///
/// On other systems, especially those that don't use systemd-resolved (Debian 13)
/// resolvconf may be a binary from the "resolvconf" package. In such cases, this function
/// reads the interface-order file to find a highest priority interface prefix and constructs
/// the full interface name prefixing the base interface name with the found prefix.
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
fn construct_resolvconf_ifname(base_ifname: &str) -> String {
let iface_order = Path::new(IFACE_ORDER_PATH);
if iface_order.exists() {
// Check if resolvconf command is a symlink or a binary
if let Ok(Some(resolvconf_path)) = get_command_path("resolvconf") {
if let Ok(metadata) = std::fs::symlink_metadata(&resolvconf_path) {
if !metadata.file_type().is_symlink() {
// It's a binary, proceed to read interface_order file
let iface_regex = regex::Regex::new(r"^([A-Za-z0-9-]+)\*$").unwrap();
if let Ok(file) = std::fs::File::open(iface_order) {
let reader = BufReader::new(file);
if let Some(constructed_ifname) = reader.lines().map_while(Result::ok).find_map(|line| {
let iface = line.trim();
iface_regex.captures(iface).and_then(|captures| {
captures.get(1).map(|matched_iface| {
// Output format: <highest_priority_iface>.<base_ifname>
let constructed_ifname =
format!("{}.{base_ifname}", matched_iface.as_str());
debug!(
"Constructed interface name from interface_order: {constructed_ifname}"
);
constructed_ifname
})
})
}) {
return constructed_ifname;
}
}
}
}
}
}

base_ifname.into()
}

#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
pub(crate) fn configure_dns(
ifname: &str,
Expand All @@ -36,7 +90,8 @@ pub(crate) fn configure_dns(
domains: {search_domains:?}"
);
let mut cmd = Command::new("resolvconf");
let mut args = vec!["-a", ifname, "-m", "0"];
let ifname = construct_resolvconf_ifname(ifname);
let mut args = vec!["-a", &ifname, "-m", "0"];
// Set the exclusive flag if no search domains are provided,
// making the DNS servers a preferred route for any domain
if search_domains.is_empty() {
Expand Down Expand Up @@ -170,7 +225,8 @@ pub(crate) fn configure_dns(
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
pub(crate) fn clear_dns(ifname: &str) -> Result<(), WireguardInterfaceError> {
debug!("Removing DNS configuration for interface {ifname}");
let args = ["-d", ifname, "-f"];
let ifname = construct_resolvconf_ifname(ifname);
let args = ["-d", &ifname, "-f"];
debug!("Executing resolvconf with args: {args:?}");
let mut cmd = Command::new("resolvconf");
let output = cmd.args(args).output()?;
Expand Down Expand Up @@ -537,3 +593,33 @@ pub(crate) fn resolve(addr: &str) -> Result<SocketAddr, WireguardInterfaceError>
.next()
.ok_or_else(error)
}

pub(crate) fn get_command_path(
command: &str,
) -> Result<Option<std::path::PathBuf>, WireguardInterfaceError> {
use std::env;

debug!("Searching for command {command} in PATH");
let paths = env::var_os("PATH").ok_or_else(|| {
WireguardInterfaceError::MissingDependency("Environment variable `PATH` not found".into())
})?;
debug!("PATH variable: {paths:?}");

Ok(env::split_paths(&paths).find_map(|dir| {
let full_path = dir.join(command);
match full_path.try_exists() {
Ok(true) => {
debug!("Command {command} found in {dir:?}");
Some(full_path)
}
Ok(false) => {
debug!("Command {command} not found in {dir:?}");
None
}
Err(err) => {
warn!("Error while checking for {command} in {dir:?}: {err}");
None
}
}
}))
}
Loading