use std::process::Command;
use tracing::debug;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
use tracing::warn;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DependencyStatus {
Available,
Missing,
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProtocolDependency {
SystemLibrary(&'static str),
ToolInPath(&'static str),
RawSocketAccess,
PrivilegedPort(u16),
RootAccess,
TunDeviceAccess,
PromiscuousMode,
}
impl ProtocolDependency {
pub fn name(&self) -> String {
match self {
Self::SystemLibrary(name) => format!("lib{}", name),
Self::ToolInPath(name) => name.to_string(),
Self::RawSocketAccess => "raw socket access".to_string(),
Self::PrivilegedPort(port) => format!("port {}", port),
Self::RootAccess => "root access".to_string(),
Self::TunDeviceAccess => "TUN device access".to_string(),
Self::PromiscuousMode => "promiscuous mode".to_string(),
}
}
pub fn description(&self) -> String {
match self {
Self::SystemLibrary(name) => {
format!("System library lib{} must be installed", name)
}
Self::ToolInPath(name) => {
format!("Tool '{}' must be available in PATH", name)
}
Self::RawSocketAccess => {
"Raw socket access (requires root or CAP_NET_RAW capability)".to_string()
}
Self::PrivilegedPort(port) => {
format!(
"Privileged port {} (requires root or CAP_NET_BIND_SERVICE)",
port
)
}
Self::RootAccess => "Root/Administrator access required".to_string(),
Self::TunDeviceAccess => {
"TUN/TAP device creation (requires root or CAP_NET_ADMIN)".to_string()
}
Self::PromiscuousMode => "Promiscuous mode (requires root or CAP_NET_RAW)".to_string(),
}
}
pub fn is_available(&self, caps: &crate::privilege::SystemCapabilities) -> bool {
self.status(caps) != DependencyStatus::Missing
}
pub fn status(&self, caps: &crate::privilege::SystemCapabilities) -> DependencyStatus {
match self {
Self::SystemLibrary(name) => system_library_status(name),
Self::ToolInPath(name) => tool_in_path_status(name),
other => {
if other.is_met_by_capabilities(caps) {
DependencyStatus::Available
} else {
DependencyStatus::Missing
}
}
}
}
fn is_met_by_capabilities(&self, caps: &crate::privilege::SystemCapabilities) -> bool {
match self {
Self::SystemLibrary(_) | Self::ToolInPath(_) => true,
Self::RawSocketAccess => caps.has_raw_socket_access,
Self::PrivilegedPort(_) => caps.can_bind_privileged_ports,
Self::RootAccess => caps.is_root,
Self::TunDeviceAccess => {
#[cfg(target_os = "linux")]
{
caps.is_root || check_cap_net_admin()
}
#[cfg(not(target_os = "linux"))]
{
caps.is_root
}
}
Self::PromiscuousMode => caps.has_packet_capture_access,
}
}
pub fn installation_hint(&self) -> String {
match self {
Self::SystemLibrary(name) => {
#[cfg(target_os = "linux")]
{
match *name {
"pcap" => "Install with: apt-get install libpcap-dev (Debian/Ubuntu) or yum install libpcap-devel (RHEL/CentOS)".to_string(),
"smbclient" => "Install with: apt-get install libsmbclient-dev (Debian/Ubuntu)".to_string(),
"git2" => "Install with: apt-get install libgit2-dev (Debian/Ubuntu)".to_string(),
_ => format!("Install with: apt-get install lib{}-dev (Debian/Ubuntu)", name),
}
}
#[cfg(target_os = "macos")]
{
match *name {
"pcap" => "Install with: brew install libpcap".to_string(),
"smbclient" => "Install with: brew install samba".to_string(),
"git2" => "Install with: brew install libgit2".to_string(),
_ => format!("Install with: brew install lib{}", name),
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
format!("Install lib{} for your platform", name)
}
}
Self::ToolInPath(name) => {
match *name {
"protoc" => "Install from: https://github.com/protocolbuffers/protobuf/releases or use package manager".to_string(),
_ => format!("Ensure '{}' is installed and in PATH", name),
}
}
Self::RawSocketAccess => {
#[cfg(target_os = "linux")]
{
"Grant capability with: sudo setcap cap_net_raw+ep /path/to/netget".to_string()
}
#[cfg(not(target_os = "linux"))]
{
"Run as root/administrator".to_string()
}
}
Self::PrivilegedPort(port) => {
format!("Run as root/administrator, or use an unprivileged port (>= 1024) instead of {}", port)
}
Self::RootAccess => "Run as root/administrator".to_string(),
Self::TunDeviceAccess => {
#[cfg(target_os = "linux")]
{
"Grant capability with: sudo setcap cap_net_admin+ep /path/to/netget".to_string()
}
#[cfg(not(target_os = "linux"))]
{
"Run as root/administrator".to_string()
}
}
Self::PromiscuousMode => {
#[cfg(target_os = "linux")]
{
"Grant capability with: sudo setcap cap_net_raw+ep /path/to/netget".to_string()
}
#[cfg(not(target_os = "linux"))]
{
"Run as root/administrator".to_string()
}
}
}
}
}
#[cfg(unix)]
fn dlopen_succeeds(soname: &str) -> bool {
let Ok(cname) = std::ffi::CString::new(soname) else {
return false;
};
unsafe {
let handle = libc::dlopen(cname.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL);
if handle.is_null() {
false
} else {
libc::dlclose(handle);
true
}
}
}
#[cfg(unix)]
fn library_candidates(name: &str) -> Vec<String> {
#[cfg(target_os = "macos")]
{
vec![
format!("lib{}.dylib", name),
format!("{}.dylib", name),
format!("/usr/lib/lib{}.dylib", name),
format!("/opt/homebrew/lib/lib{}.dylib", name),
format!("/usr/local/lib/lib{}.dylib", name),
]
}
#[cfg(not(target_os = "macos"))]
{
vec![
format!("lib{}.so", name),
format!("lib{}.so.1", name),
format!("lib{}.so.0", name),
format!("lib{}.so.2", name),
]
}
}
fn system_library_status(name: &str) -> DependencyStatus {
#[cfg(unix)]
{
for candidate in library_candidates(name) {
if dlopen_succeeds(&candidate) {
debug!("dlopen({}) succeeded - lib{} is loadable", candidate, name);
return DependencyStatus::Available;
}
}
debug!(
"dlopen found no loadable lib{}; falling back to platform probes",
name
);
}
#[cfg(target_os = "linux")]
{
let output = Command::new("ldconfig").arg("-p").output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
let lib_pattern = format!("lib{}.so", name);
let found = stdout.contains(&lib_pattern);
debug!("Checking for lib{}: found={}", name, found);
return if found {
DependencyStatus::Available
} else {
DependencyStatus::Missing
};
}
let pkg_config_output = Command::new("pkg-config")
.arg("--exists")
.arg(name)
.status();
if let Ok(status) = pkg_config_output {
let found = status.success();
debug!("Checking for {} via pkg-config: found={}", name, found);
return if found {
DependencyStatus::Available
} else {
DependencyStatus::Missing
};
}
debug!(
"Could not check for lib{} (ldconfig and pkg-config unavailable)",
name
);
DependencyStatus::Unknown(format!(
"dlopen found no lib{name} and neither ldconfig nor pkg-config could be run"
))
}
#[cfg(target_os = "macos")]
{
let pkg_config_output = Command::new("pkg-config")
.arg("--exists")
.arg(name)
.status();
if let Ok(status) = pkg_config_output {
let found = status.success();
debug!("Checking for {} via pkg-config: found={}", name, found);
return if found {
DependencyStatus::Available
} else {
DependencyStatus::Missing
};
}
debug!("dlopen and pkg-config found no lib{} on macOS", name);
DependencyStatus::Missing
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
warn!(
"Cannot check for lib{} on this platform; not treating it as missing",
name
);
DependencyStatus::Unknown(format!("no library probe for lib{name} on this platform"))
}
}
fn tool_in_path_status(tool: &str) -> DependencyStatus {
let Some(path) = std::env::var_os("PATH") else {
return DependencyStatus::Unknown("PATH is not set, so there is nothing to search".into());
};
#[cfg(windows)]
let names: Vec<String> = ["", ".exe", ".cmd", ".bat", ".com"]
.iter()
.map(|ext| format!("{tool}{ext}"))
.collect();
#[cfg(not(windows))]
let names: Vec<String> = vec![tool.to_string()];
for dir in std::env::split_paths(&path) {
if dir.as_os_str().is_empty() {
continue;
}
for name in &names {
let candidate = dir.join(name);
if is_executable_file(&candidate) {
debug!("Found tool '{}' at {}", tool, candidate.display());
return DependencyStatus::Available;
}
}
}
debug!("Tool '{}' is not on PATH", tool);
DependencyStatus::Missing
}
fn is_executable_file(path: &std::path::Path) -> bool {
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
pub fn startup_blocker(
deps: &[ProtocolDependency],
caps: &crate::privilege::SystemCapabilities,
) -> Option<ProtocolDependency> {
for dep in deps {
if !matches!(
dep,
ProtocolDependency::SystemLibrary(_) | ProtocolDependency::ToolInPath(_)
) {
continue;
}
match dep.status(caps) {
DependencyStatus::Available => {}
DependencyStatus::Missing => return Some(dep.clone()),
DependencyStatus::Unknown(why) => {
tracing::warn!(
"Could not determine whether {} is present ({}); starting anyway",
dep.name(),
why
);
}
}
}
None
}
#[cfg(target_os = "linux")]
fn check_cap_net_admin() -> bool {
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if line.starts_with("CapEff:") {
if let Some(cap_hex) = line.split_whitespace().nth(1) {
if let Ok(caps) = u64::from_str_radix(cap_hex, 16) {
const CAP_NET_ADMIN: u64 = 1 << 12;
let has_cap = (caps & CAP_NET_ADMIN) != 0;
debug!(
"Checked CAP_NET_ADMIN via /proc: caps=0x{:x}, has_cap={}",
caps, has_cap
);
return has_cap;
}
}
}
}
}
debug!("Could not detect CAP_NET_ADMIN capability");
false
}