#[must_use]
#[cfg(unix)]
#[allow(unsafe_code)]
pub fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
#[must_use]
#[cfg(not(unix))]
pub fn is_root() -> bool {
false
}
#[cfg(target_os = "macos")]
pub fn run_with_timeout(
cmd: &mut std::process::Command,
timeout: std::time::Duration,
) -> Option<std::process::Output> {
use std::process::Stdio;
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let deadline = std::time::Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(_)) => return child.wait_with_output().ok(),
Ok(None) if std::time::Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait();
return None;
}
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)),
Err(_) => return None,
}
}
}
pub fn create_user_dir(path: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(path)?;
crate::config::fix_ownership(path);
Ok(())
}
pub fn write_user_file(path: &std::path::Path, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
std::fs::write(path, contents)?;
crate::config::fix_ownership(path);
Ok(())
}
#[must_use]
pub fn format_bytes_speed(bytes: u64) -> String {
#[allow(clippy::cast_precision_loss)]
if bytes >= 1_000_000 {
format!("{:.1} MB/s", bytes as f64 / 1_000_000.0)
} else if bytes >= 1_000 {
format!("{:.1} KB/s", bytes as f64 / 1_000.0)
} else {
format!("{bytes} B/s")
}
}
#[must_use]
pub fn is_private_ip(ip: &str) -> bool {
let parts: Vec<&str> = ip.split('.').collect();
if parts.len() != 4 {
return false;
}
let octets: Result<Vec<u8>, _> = parts.iter().map(|p| p.parse::<u8>()).collect();
let Ok(octets) = octets else {
return false;
};
match octets[0] {
10 => true, 172 if (16..=31).contains(&octets[1]) => true, 192 if octets[1] == 168 => true, _ => false,
}
}
pub fn get_app_config_dir() -> std::io::Result<std::path::PathBuf> {
crate::config::get_config_dir()
}
pub fn get_profiles_dir() -> std::io::Result<std::path::PathBuf> {
let root = get_app_config_dir()?;
let path = root.join(crate::constants::PROFILES_DIR_NAME);
if !path.exists() {
create_user_dir(&path)?;
}
Ok(path)
}
#[must_use]
pub fn sanitize_profile_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect()
}
pub fn get_openvpn_run_paths(
profile_name: &str,
) -> std::io::Result<(std::path::PathBuf, std::path::PathBuf)> {
let root = get_app_config_dir()?;
let run_dir = root.join(crate::constants::OPENVPN_RUN_DIR);
if !run_dir.exists() {
create_user_dir(&run_dir)?;
}
let safe_name = sanitize_profile_name(profile_name);
let pid_path = run_dir.join(format!("{safe_name}.pid"));
let log_path = run_dir.join(format!("{safe_name}.log"));
Ok((pid_path, log_path))
}
pub fn cleanup_openvpn_run_files(profile_name: &str) {
if let Ok((pid_path, log_path)) = get_openvpn_run_paths(profile_name) {
let _ = std::fs::remove_file(&pid_path);
let _ = std::fs::remove_file(&log_path);
}
}
#[must_use]
pub fn read_openvpn_pid(profile_name: &str) -> Option<u32> {
let (pid_path, _) = get_openvpn_run_paths(profile_name).ok()?;
let content = std::fs::read_to_string(&pid_path).ok()?;
content.trim().parse::<u32>().ok()
}
pub fn get_openvpn_auth_path(profile_name: &str) -> std::io::Result<std::path::PathBuf> {
let root = get_app_config_dir()?;
let auth_dir = root.join(crate::constants::OPENVPN_AUTH_DIR);
if !auth_dir.exists() {
create_user_dir(&auth_dir)?;
}
let safe_name = sanitize_profile_name(profile_name);
Ok(auth_dir.join(format!("{safe_name}.auth")))
}
#[cfg(unix)]
pub fn write_openvpn_auth_file(
profile_name: &str,
username: &str,
password: &str,
) -> std::io::Result<std::path::PathBuf> {
use std::os::unix::fs::PermissionsExt;
let auth_path = get_openvpn_auth_path(profile_name)?;
write_user_file(&auth_path, format!("{username}\n{password}\n"))?;
let mut perms = std::fs::metadata(&auth_path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(&auth_path, perms)?;
Ok(auth_path)
}
#[cfg(not(unix))]
pub fn write_openvpn_auth_file(
profile_name: &str,
username: &str,
password: &str,
) -> std::io::Result<std::path::PathBuf> {
let auth_path = get_openvpn_auth_path(profile_name)?;
write_user_file(&auth_path, format!("{username}\n{password}\n"))?;
Ok(auth_path)
}
#[must_use]
pub fn read_openvpn_saved_auth(profile_name: &str) -> Option<(String, String)> {
let auth_path = get_openvpn_auth_path(profile_name).ok()?;
let content = std::fs::read_to_string(&auth_path).ok()?;
let mut lines = content.lines();
let username = lines.next()?.to_string();
let password = lines.next()?.to_string();
if username.is_empty() || password.is_empty() {
return None;
}
Some((username, password))
}
pub fn delete_openvpn_auth_file(profile_name: &str) {
if let Ok(auth_path) = get_openvpn_auth_path(profile_name) {
let _ = std::fs::remove_file(&auth_path);
}
}
#[must_use]
pub fn openvpn_config_needs_auth(config_path: &std::path::Path) -> bool {
let Ok(content) = std::fs::read_to_string(config_path) else {
return false;
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
continue;
}
if trimmed == crate::constants::OVPN_AUTH_USER_PASS {
return true;
}
if let Some(rest) = trimmed.strip_prefix(crate::constants::OVPN_AUTH_USER_PASS) {
if rest.trim().is_empty() {
return true;
}
return false;
}
}
false
}
#[must_use]
pub fn truncate(s: &str, max_chars: usize) -> String {
if s.chars().count() > max_chars {
let mut t: String = s.chars().take(max_chars.saturating_sub(3)).collect();
t.push_str("...");
t
} else {
s.to_string()
}
}
#[must_use]
pub fn format_local_time() -> String {
format_system_time_local(std::time::SystemTime::now())
}
#[must_use]
pub fn format_system_time_local(time: std::time::SystemTime) -> String {
format_system_time_inner(time).unwrap_or_else(|| "00:00:00".to_string())
}
#[cfg(unix)]
#[allow(unsafe_code)]
fn format_system_time_inner(time: std::time::SystemTime) -> Option<String> {
let secs = time
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.ok()?
.as_secs();
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
#[allow(clippy::cast_possible_wrap)]
let time_t = secs as libc::time_t;
let result = unsafe { libc::localtime_r(&time_t, &mut tm) };
if result.is_null() {
return None;
}
Some(format!(
"{:02}:{:02}:{:02}",
tm.tm_hour, tm.tm_min, tm.tm_sec
))
}
#[cfg(not(unix))]
fn format_system_time_inner(time: std::time::SystemTime) -> Option<String> {
let _ = time;
std::process::Command::new("date")
.arg("+%H:%M:%S")
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
}
#[must_use]
pub fn format_relative_time(time: std::time::SystemTime) -> String {
let now = std::time::SystemTime::now();
match now.duration_since(time) {
Ok(duration) => {
let secs = duration.as_secs();
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86400 {
format!("{}h", secs / 3600)
} else if secs < 2_592_000 {
format!("{}d ago", secs / 86400)
} else if secs < 31_536_000 {
format!("{}M ago", secs / 2_592_000)
} else {
format!("{}Y ago", secs / 31_536_000)
}
}
Err(_) => "now".to_string(),
}
}
pub fn home_dir() -> Option<std::path::PathBuf> {
std::env::var("HOME")
.ok()
.map(std::path::PathBuf::from)
.or_else(home_dir_from_passwd)
}
#[cfg(unix)]
#[allow(unsafe_code)]
fn home_dir_from_passwd() -> Option<std::path::PathBuf> {
unsafe {
let uid = libc::getuid();
let pw = libc::getpwuid(uid);
if pw.is_null() {
return None;
}
let home = std::ffi::CStr::from_ptr((*pw).pw_dir);
home.to_str().ok().map(std::path::PathBuf::from)
}
}
#[cfg(not(unix))]
fn home_dir_from_passwd() -> Option<std::path::PathBuf> {
None
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ProfileMetadata {
#[serde(
with = "systemtime_serde",
skip_serializing_if = "Option::is_none",
default
)]
pub last_used: Option<std::time::SystemTime>,
}
mod systemtime_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::{SystemTime, UNIX_EPOCH};
#[allow(clippy::ref_option)]
pub fn serialize<S>(time: &Option<SystemTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match time {
Some(t) => {
let duration = t
.duration_since(UNIX_EPOCH)
.map_err(serde::ser::Error::custom)?;
duration.as_secs().serialize(serializer)
}
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<SystemTime>, D::Error>
where
D: Deserializer<'de>,
{
let secs: Option<u64> = Option::deserialize(deserializer)?;
Ok(secs.map(|s| UNIX_EPOCH + std::time::Duration::from_secs(s)))
}
}
pub fn load_profile_metadata() -> Result<std::collections::HashMap<String, ProfileMetadata>, String>
{
let metadata_path = get_app_config_dir()
.map_err(|e| format!("Failed to get config dir: {e}"))?
.join(crate::constants::METADATA_FILE_NAME);
if !metadata_path.exists() {
return Ok(std::collections::HashMap::new());
}
let content = std::fs::read_to_string(&metadata_path)
.map_err(|e| format!("Failed to read metadata: {e}"))?;
serde_json::from_str(&content).or_else(|e| {
crate::logger::log(
crate::logger::LogLevel::Warning,
"CONFIG",
format!(
"Failed to parse {}: {}. Using defaults.",
crate::constants::METADATA_FILE_NAME,
e
),
);
Ok(std::collections::HashMap::new())
})
}
pub fn save_profile_metadata(
data: &std::collections::HashMap<String, ProfileMetadata>,
) -> Result<(), String> {
let metadata_path = get_app_config_dir()
.map_err(|e| format!("Failed to get config dir: {e}"))?
.join(crate::constants::METADATA_FILE_NAME);
let json = serde_json::to_string_pretty(data)
.map_err(|e| format!("Failed to serialize metadata: {e}"))?;
write_user_file(&metadata_path, json).map_err(|e| format!("Failed to write metadata: {e}"))?;
Ok(())
}
#[must_use]
pub fn get_unique_path(dir: &std::path::Path, filename: &str) -> std::path::PathBuf {
let mut path = dir.join(filename);
let mut counter = 1;
let path_obj = std::path::Path::new(filename);
let stem = path_obj
.file_stem()
.map_or(filename, |s| s.to_str().unwrap_or(filename));
let ext = path_obj.extension().map(|e| e.to_str().unwrap_or(""));
while path.exists() {
let new_name = if let Some(e) = ext {
if e.is_empty() {
format!("{stem}_{counter}")
} else {
format!("{stem}_{counter}.{e}")
}
} else {
format!("{stem}_{counter}")
};
path = dir.join(new_name);
counter += 1;
}
path
}
pub(crate) fn binary_exists(name: &str) -> bool {
std::process::Command::new("which")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(target_os = "linux")]
pub(crate) fn resolvconf_works() -> bool {
if !binary_exists("resolvconf") {
return false;
}
std::process::Command::new("resolvconf")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(target_os = "linux")]
pub(crate) fn is_systemd_resolved() -> bool {
match std::fs::read_link("/etc/resolv.conf") {
Ok(target) => {
let s = target.to_string_lossy();
s.contains("systemd") || s.contains("resolvconf/run")
}
Err(_) => false,
}
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn wireguard_config_has_dns(config_path: &std::path::Path) -> bool {
let Ok(content) = std::fs::read_to_string(config_path) else {
return false;
};
for line in content.lines() {
let trimmed = line.trim().to_lowercase();
if trimmed.starts_with("dns") {
if let Some(rest) = trimmed.strip_prefix("dns") {
let rest = rest.trim_start();
if rest.starts_with('=') {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, SystemTime};
#[test]
fn test_format_bytes_speed_bytes() {
assert_eq!(format_bytes_speed(0), "0 B/s");
assert_eq!(format_bytes_speed(500), "500 B/s");
assert_eq!(format_bytes_speed(999), "999 B/s");
}
#[test]
fn test_format_bytes_speed_kilobytes() {
assert_eq!(format_bytes_speed(1_000), "1.0 KB/s");
assert_eq!(format_bytes_speed(1_500), "1.5 KB/s");
assert_eq!(format_bytes_speed(999_999), "1000.0 KB/s");
}
#[test]
fn test_format_bytes_speed_megabytes() {
assert_eq!(format_bytes_speed(1_000_000), "1.0 MB/s");
assert_eq!(format_bytes_speed(1_500_000), "1.5 MB/s");
assert_eq!(format_bytes_speed(100_000_000), "100.0 MB/s");
}
#[test]
fn test_truncate_short_string() {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("test", 4), "test");
}
#[test]
fn test_truncate_exact_length() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn test_truncate_long_string() {
assert_eq!(truncate("hello world", 8), "hello...");
assert_eq!(truncate("this is a long string", 10), "this is...");
}
#[test]
fn test_truncate_with_unicode() {
assert_eq!(truncate("héllo", 5), "héllo");
assert_eq!(truncate("héllo world", 8), "héllo...");
}
#[test]
fn test_home_dir_exists() {
let home = home_dir();
assert!(home.is_some());
assert!(home.unwrap().exists());
}
#[test]
fn test_format_relative_time() {
let now = SystemTime::now();
let just_now = now - Duration::from_secs(5);
assert_eq!(format_relative_time(just_now), "5s");
let five_mins = now - Duration::from_secs(300);
assert_eq!(format_relative_time(five_mins), "5m");
let two_hours = now - Duration::from_secs(7200);
assert_eq!(format_relative_time(two_hours), "2h");
let three_days = now - Duration::from_secs(86400 * 3);
assert_eq!(format_relative_time(three_days), "3d ago");
let two_months = now - Duration::from_secs(2_592_000 * 2);
assert_eq!(format_relative_time(two_months), "2M ago");
let three_years = now - Duration::from_secs(31_536_000 * 3);
assert_eq!(format_relative_time(three_years), "3Y ago");
let future = now + Duration::from_secs(10);
assert_eq!(format_relative_time(future), "now");
}
#[test]
fn test_is_private_ip_class_a() {
assert!(is_private_ip("10.0.0.1"));
assert!(is_private_ip("10.255.255.255"));
assert!(is_private_ip("10.1.2.3"));
}
#[test]
fn test_is_private_ip_class_b() {
assert!(is_private_ip("172.16.0.1"));
assert!(is_private_ip("172.31.255.255"));
assert!(is_private_ip("172.20.10.5"));
}
#[test]
fn test_is_private_ip_class_c() {
assert!(is_private_ip("192.168.0.1"));
assert!(is_private_ip("192.168.255.255"));
assert!(is_private_ip("192.168.1.100"));
}
#[test]
fn test_is_private_ip_public() {
assert!(!is_private_ip("8.8.8.8"));
assert!(!is_private_ip("1.2.3.4"));
assert!(!is_private_ip("172.15.0.1")); assert!(!is_private_ip("172.32.0.1")); assert!(!is_private_ip("192.169.0.1")); }
#[test]
fn test_is_private_ip_invalid() {
assert!(!is_private_ip("999.999.999.999"));
assert!(!is_private_ip("not.an.ip.address"));
assert!(!is_private_ip("10.0.0"));
assert!(!is_private_ip(""));
}
#[test]
fn test_get_unique_path_no_collision() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
let path = get_unique_path(dir.path(), "test.conf");
assert_eq!(path.file_name().unwrap(), "test.conf");
}
#[test]
fn test_get_unique_path_with_collision() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
std::fs::write(dir.path().join("test.conf"), "existing").unwrap();
let path = get_unique_path(dir.path(), "test.conf");
assert_eq!(path.file_name().unwrap(), "test_1.conf");
std::fs::write(dir.path().join("test_1.conf"), "also existing").unwrap();
let path2 = get_unique_path(dir.path(), "test.conf");
assert_eq!(path2.file_name().unwrap(), "test_2.conf");
}
#[test]
fn test_openvpn_config_needs_auth_bare_directive() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
let path = dir.path().join("test.ovpn");
std::fs::write(
&path,
"client\nremote example.com 1194\nauth-user-pass\ndev tun\n",
)
.unwrap();
assert!(openvpn_config_needs_auth(&path));
}
#[test]
fn test_openvpn_config_needs_auth_bare_with_trailing_space() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
let path = dir.path().join("test.ovpn");
std::fs::write(
&path,
"client\nremote example.com 1194\nauth-user-pass \ndev tun\n",
)
.unwrap();
assert!(openvpn_config_needs_auth(&path));
}
#[test]
fn test_openvpn_config_needs_auth_with_file_arg() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
let path = dir.path().join("test.ovpn");
std::fs::write(
&path,
"client\nremote example.com 1194\nauth-user-pass /etc/openvpn/creds.txt\ndev tun\n",
)
.unwrap();
assert!(!openvpn_config_needs_auth(&path));
}
#[test]
fn test_openvpn_config_needs_auth_absent() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
let path = dir.path().join("test.ovpn");
std::fs::write(
&path,
"client\nremote example.com 1194\ndev tun\nproto udp\n",
)
.unwrap();
assert!(!openvpn_config_needs_auth(&path));
}
#[test]
fn test_openvpn_config_needs_auth_commented_out() {
let dir = tempfile::Builder::new()
.prefix("vortix_test_")
.tempdir()
.unwrap();
let path = dir.path().join("test.ovpn");
std::fs::write(
&path,
"client\nremote example.com 1194\n# auth-user-pass\n; auth-user-pass\ndev tun\n",
)
.unwrap();
assert!(!openvpn_config_needs_auth(&path));
}
#[test]
fn test_openvpn_config_needs_auth_nonexistent_file() {
let path = std::path::PathBuf::from("/tmp/nonexistent_vortix_config_12345.ovpn");
assert!(!openvpn_config_needs_auth(&path));
}
fn set_temp_config_dir() -> tempfile::TempDir {
let dir = tempfile::Builder::new()
.prefix("vortix_utils_test_")
.tempdir()
.unwrap();
crate::config::set_config_dir(dir.path().to_path_buf());
dir
}
#[test]
fn test_write_read_openvpn_auth_file() {
let _tmp = set_temp_config_dir();
let name = "test_auth_roundtrip";
let result = write_openvpn_auth_file(name, "myuser", "mypass");
assert!(result.is_ok());
let path = result.unwrap();
assert!(path.exists());
let creds = read_openvpn_saved_auth(name);
assert!(creds.is_some());
let (user, pass) = creds.unwrap();
assert_eq!(user, "myuser");
assert_eq!(pass, "mypass");
delete_openvpn_auth_file(name);
assert!(!path.exists());
}
#[cfg(unix)]
#[test]
fn test_auth_file_permissions() {
use std::os::unix::fs::PermissionsExt;
let _tmp = set_temp_config_dir();
let name = "test_auth_perms";
let result = write_openvpn_auth_file(name, "user", "pass");
assert!(result.is_ok());
let path = result.unwrap();
let perms = std::fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o600);
delete_openvpn_auth_file(name);
}
#[test]
fn test_sanitize_profile_name_ascii() {
assert_eq!(sanitize_profile_name("my-vpn_1"), "my-vpn_1");
}
#[test]
fn test_sanitize_profile_name_spaces() {
assert_eq!(sanitize_profile_name("my vpn server"), "my_vpn_server");
}
#[test]
fn test_sanitize_profile_name_special_chars() {
assert_eq!(sanitize_profile_name("vpn@home!#$"), "vpn_home___");
}
#[test]
fn test_sanitize_profile_name_unicode_rejected() {
assert_eq!(sanitize_profile_name("café-vpn"), "caf_-vpn");
assert_eq!(sanitize_profile_name("München"), "M_nchen");
}
#[test]
fn test_sanitize_profile_name_cjk() {
assert_eq!(sanitize_profile_name("日本VPN"), "__VPN");
}
#[test]
fn test_sanitize_profile_name_empty() {
assert_eq!(sanitize_profile_name(""), "");
}
#[test]
fn test_truncate_very_small_budget() {
assert_eq!(truncate("hello world", 3), "...");
assert_eq!(truncate("hello world", 2), "...");
assert_eq!(truncate("hello world", 0), "...");
}
#[test]
fn test_read_openvpn_saved_auth_missing_file() {
let creds = read_openvpn_saved_auth("nonexistent_profile_xyz_12345");
assert!(creds.is_none());
}
#[test]
fn test_read_openvpn_saved_auth_empty_creds() {
let _tmp = set_temp_config_dir();
let name = "test_auth_empty_creds";
let path = get_openvpn_auth_path(name).unwrap();
std::fs::write(&path, "\npassword\n").unwrap();
assert!(read_openvpn_saved_auth(name).is_none());
std::fs::write(&path, "username\n\n").unwrap();
assert!(read_openvpn_saved_auth(name).is_none());
delete_openvpn_auth_file(name);
}
#[test]
fn test_wg_config_has_dns_present() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wg0.conf");
std::fs::write(
&path,
"[Interface]\nPrivateKey = abc\nAddress = 10.0.0.2/24\nDNS = 1.1.1.1\n\n[Peer]\nPublicKey = xyz\nEndpoint = 1.2.3.4:51820\n",
)
.unwrap();
assert!(wireguard_config_has_dns(&path));
}
#[test]
fn test_wg_config_has_dns_absent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wg0.conf");
std::fs::write(
&path,
"[Interface]\nPrivateKey = abc\nAddress = 10.0.0.2/24\n\n[Peer]\nPublicKey = xyz\nEndpoint = 1.2.3.4:51820\n",
)
.unwrap();
assert!(!wireguard_config_has_dns(&path));
}
#[test]
fn test_wg_config_has_dns_case_insensitive() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wg0.conf");
std::fs::write(
&path,
"[Interface]\nPrivateKey = abc\nAddress = 10.0.0.2/24\ndns = 8.8.8.8\n\n[Peer]\nPublicKey = xyz\nEndpoint = 1.2.3.4:51820\n",
)
.unwrap();
assert!(wireguard_config_has_dns(&path));
}
#[test]
fn test_wg_config_has_dns_with_spaces() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wg0.conf");
std::fs::write(
&path,
"[Interface]\nPrivateKey = abc\nAddress = 10.0.0.2/24\n DNS = 1.1.1.1, 8.8.8.8\n\n[Peer]\nPublicKey = xyz\nEndpoint = 1.2.3.4:51820\n",
)
.unwrap();
assert!(wireguard_config_has_dns(&path));
}
#[test]
fn test_wg_config_has_dns_nonexistent_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing.conf");
assert!(!wireguard_config_has_dns(&path));
}
#[test]
fn test_wg_config_dns_in_comment_not_matched() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wg0.conf");
std::fs::write(
&path,
"[Interface]\nPrivateKey = abc\nAddress = 10.0.0.2/24\n# DNS = 1.1.1.1\n\n[Peer]\nPublicKey = xyz\nEndpoint = 1.2.3.4:51820\n",
)
.unwrap();
assert!(!wireguard_config_has_dns(&path));
}
}