use crate::vortix_core::ports::interface::Interface;
use crate::vortix_process::CommandSpec;
fn cmd_output(program: &str, args: &[&str]) -> Option<std::process::Output> {
let owned: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
crate::vortix_process::run_to_output(CommandSpec::oneshot(program, owned)).ok()
}
pub struct LinuxInterface;
impl Interface for LinuxInterface {
fn check_wireguard_interface(name: &str) -> bool {
check_wg_interface_exists(name)
}
fn resolve_wireguard_interface(name: &str) -> Option<String> {
if check_wg_interface_exists(name) {
return Some(name.to_string());
}
if let Some(output) = cmd_output("wg", &["show"]) {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.starts_with("interface: ") {
let iface = line.trim_start_matches("interface: ").trim();
if iface == name {
return Some(iface.to_string());
}
}
}
}
None
}
fn get_wireguard_pid(interface: &str) -> Option<u32> {
find_pid_with_cmdline_substrings(&["wireguard", interface])
}
fn get_interface_info(interface: &str) -> (String, String) {
let ip = get_interface_ipv4(interface).unwrap_or_default();
let mtu = read_sysfs_mtu(interface).unwrap_or_default();
(ip, mtu)
}
}
fn check_wg_interface_exists(name: &str) -> bool {
cmd_output("wg", &["show", name, "public-key"]).is_some_and(|o| o.status.success())
}
pub(crate) fn find_pid_with_cmdline_substrings(needles: &[&str]) -> Option<u32> {
let needles_lower: Vec<String> = needles.iter().map(|n| n.to_lowercase()).collect();
let entries = std::fs::read_dir("/proc").ok()?;
for entry in entries.flatten() {
let file_name = entry.file_name();
let Some(name) = file_name.to_str() else {
continue;
};
let Ok(pid) = name.parse::<u32>() else {
continue;
};
let cmdline_path = format!("/proc/{pid}/cmdline");
let Ok(raw) = std::fs::read(&cmdline_path) else {
continue; };
let cmdline = String::from_utf8_lossy(&raw)
.replace('\0', " ")
.to_lowercase();
if needles_lower.iter().all(|n| cmdline.contains(n)) {
return Some(pid);
}
}
None
}
pub(crate) fn find_all_pids_with_cmdline_substring(needle: &str) -> Vec<u32> {
let needle_lower = needle.to_lowercase();
let mut matches = Vec::new();
let Ok(entries) = std::fs::read_dir("/proc") else {
return matches;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let Some(name) = file_name.to_str() else {
continue;
};
let Ok(pid) = name.parse::<u32>() else {
continue;
};
let cmdline_path = format!("/proc/{pid}/cmdline");
let Ok(raw) = std::fs::read(&cmdline_path) else {
continue;
};
let cmdline = String::from_utf8_lossy(&raw)
.replace('\0', " ")
.to_lowercase();
if cmdline.contains(&needle_lower) {
matches.push(pid);
}
}
matches
}
fn get_interface_ipv4(interface: &str) -> Option<String> {
#[allow(unsafe_code)]
unsafe {
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
if libc::getifaddrs(&raw mut ifap) != 0 {
return None;
}
let mut result: Option<String> = None;
let mut current = ifap;
while !current.is_null() {
let entry = &*current;
if !entry.ifa_name.is_null() {
let name_cstr = std::ffi::CStr::from_ptr(entry.ifa_name);
if name_cstr.to_bytes() == interface.as_bytes() && !entry.ifa_addr.is_null() {
let addr = &*entry.ifa_addr;
if i32::from(addr.sa_family) == libc::AF_INET {
#[allow(clippy::cast_ptr_alignment)]
let sin = entry.ifa_addr.cast::<libc::sockaddr_in>();
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes();
result = Some(format!(
"{}.{}.{}.{}",
bytes[0], bytes[1], bytes[2], bytes[3]
));
break;
}
}
}
current = entry.ifa_next;
}
libc::freeifaddrs(ifap);
result
}
}
fn read_sysfs_mtu(interface: &str) -> Option<String> {
let path = format!("/sys/class/net/{interface}/mtu");
std::fs::read_to_string(path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_interface_ipv4_returns_none_for_nonexistent_interface() {
let result = get_interface_ipv4("vortix-nonexistent-test-iface-xyz");
assert!(result.is_none());
}
#[test]
fn read_sysfs_mtu_returns_none_for_nonexistent_interface() {
let result = read_sysfs_mtu("vortix-nonexistent-test-iface-xyz");
assert!(result.is_none());
}
#[test]
fn get_interface_info_for_loopback_returns_known_values() {
let (ip, mtu) = LinuxInterface::get_interface_info("lo");
assert_eq!(ip, "127.0.0.1", "loopback IPv4 should be 127.0.0.1");
assert!(
mtu.parse::<u32>().is_ok(),
"loopback MTU should be a parseable integer; got: {mtu}"
);
}
}