1use std::process::Command;
7use tracing::debug;
8
9#[cfg(not(any(target_os = "linux", target_os = "macos")))]
10use tracing::warn;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum DependencyStatus {
20 Available,
22 Missing,
24 Unknown(String),
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub enum ProtocolDependency {
31 SystemLibrary(&'static str),
34
35 ToolInPath(&'static str),
37
38 RawSocketAccess,
40
41 PrivilegedPort(u16),
43
44 RootAccess,
46
47 TunDeviceAccess,
50
51 PromiscuousMode,
54}
55
56impl ProtocolDependency {
57 pub fn name(&self) -> String {
59 match self {
60 Self::SystemLibrary(name) => format!("lib{}", name),
61 Self::ToolInPath(name) => name.to_string(),
62 Self::RawSocketAccess => "raw socket access".to_string(),
63 Self::PrivilegedPort(port) => format!("port {}", port),
64 Self::RootAccess => "root access".to_string(),
65 Self::TunDeviceAccess => "TUN device access".to_string(),
66 Self::PromiscuousMode => "promiscuous mode".to_string(),
67 }
68 }
69
70 pub fn description(&self) -> String {
72 match self {
73 Self::SystemLibrary(name) => {
74 format!("System library lib{} must be installed", name)
75 }
76 Self::ToolInPath(name) => {
77 format!("Tool '{}' must be available in PATH", name)
78 }
79 Self::RawSocketAccess => {
80 "Raw socket access (requires root or CAP_NET_RAW capability)".to_string()
81 }
82 Self::PrivilegedPort(port) => {
83 format!(
84 "Privileged port {} (requires root or CAP_NET_BIND_SERVICE)",
85 port
86 )
87 }
88 Self::RootAccess => "Root/Administrator access required".to_string(),
89 Self::TunDeviceAccess => {
90 "TUN/TAP device creation (requires root or CAP_NET_ADMIN)".to_string()
91 }
92 Self::PromiscuousMode => "Promiscuous mode (requires root or CAP_NET_RAW)".to_string(),
93 }
94 }
95
96 pub fn is_available(&self, caps: &crate::privilege::SystemCapabilities) -> bool {
103 self.status(caps) != DependencyStatus::Missing
104 }
105
106 pub fn status(&self, caps: &crate::privilege::SystemCapabilities) -> DependencyStatus {
108 match self {
109 Self::SystemLibrary(name) => system_library_status(name),
110 Self::ToolInPath(name) => tool_in_path_status(name),
111 other => {
112 if other.is_met_by_capabilities(caps) {
113 DependencyStatus::Available
114 } else {
115 DependencyStatus::Missing
116 }
117 }
118 }
119 }
120
121 fn is_met_by_capabilities(&self, caps: &crate::privilege::SystemCapabilities) -> bool {
125 match self {
126 Self::SystemLibrary(_) | Self::ToolInPath(_) => true,
127 Self::RawSocketAccess => caps.has_raw_socket_access,
128 Self::PrivilegedPort(_) => caps.can_bind_privileged_ports,
129 Self::RootAccess => caps.is_root,
130 Self::TunDeviceAccess => {
131 #[cfg(target_os = "linux")]
133 {
134 caps.is_root || check_cap_net_admin()
135 }
136 #[cfg(not(target_os = "linux"))]
137 {
138 caps.is_root
139 }
140 }
141 Self::PromiscuousMode => caps.has_packet_capture_access,
147 }
148 }
149
150 pub fn installation_hint(&self) -> String {
152 match self {
153 Self::SystemLibrary(name) => {
154 #[cfg(target_os = "linux")]
155 {
156 match *name {
157 "pcap" => "Install with: apt-get install libpcap-dev (Debian/Ubuntu) or yum install libpcap-devel (RHEL/CentOS)".to_string(),
158 "smbclient" => "Install with: apt-get install libsmbclient-dev (Debian/Ubuntu)".to_string(),
159 "git2" => "Install with: apt-get install libgit2-dev (Debian/Ubuntu)".to_string(),
160 _ => format!("Install with: apt-get install lib{}-dev (Debian/Ubuntu)", name),
161 }
162 }
163 #[cfg(target_os = "macos")]
164 {
165 match *name {
166 "pcap" => "Install with: brew install libpcap".to_string(),
167 "smbclient" => "Install with: brew install samba".to_string(),
168 "git2" => "Install with: brew install libgit2".to_string(),
169 _ => format!("Install with: brew install lib{}", name),
170 }
171 }
172 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
173 {
174 format!("Install lib{} for your platform", name)
175 }
176 }
177 Self::ToolInPath(name) => {
178 match *name {
179 "protoc" => "Install from: https://github.com/protocolbuffers/protobuf/releases or use package manager".to_string(),
180 _ => format!("Ensure '{}' is installed and in PATH", name),
181 }
182 }
183 Self::RawSocketAccess => {
184 #[cfg(target_os = "linux")]
185 {
186 "Grant capability with: sudo setcap cap_net_raw+ep /path/to/netget".to_string()
187 }
188 #[cfg(not(target_os = "linux"))]
189 {
190 "Run as root/administrator".to_string()
191 }
192 }
193 Self::PrivilegedPort(port) => {
194 format!("Run as root/administrator, or use an unprivileged port (>= 1024) instead of {}", port)
195 }
196 Self::RootAccess => "Run as root/administrator".to_string(),
197 Self::TunDeviceAccess => {
198 #[cfg(target_os = "linux")]
199 {
200 "Grant capability with: sudo setcap cap_net_admin+ep /path/to/netget".to_string()
201 }
202 #[cfg(not(target_os = "linux"))]
203 {
204 "Run as root/administrator".to_string()
205 }
206 }
207 Self::PromiscuousMode => {
208 #[cfg(target_os = "linux")]
209 {
210 "Grant capability with: sudo setcap cap_net_raw+ep /path/to/netget".to_string()
211 }
212 #[cfg(not(target_os = "linux"))]
213 {
214 "Run as root/administrator".to_string()
215 }
216 }
217 }
218 }
219}
220
221#[cfg(unix)]
234fn dlopen_succeeds(soname: &str) -> bool {
235 let Ok(cname) = std::ffi::CString::new(soname) else {
236 return false;
237 };
238
239 unsafe {
243 let handle = libc::dlopen(cname.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL);
244 if handle.is_null() {
245 false
246 } else {
247 libc::dlclose(handle);
248 true
249 }
250 }
251}
252
253#[cfg(unix)]
259fn library_candidates(name: &str) -> Vec<String> {
260 #[cfg(target_os = "macos")]
261 {
262 vec![
263 format!("lib{}.dylib", name),
264 format!("{}.dylib", name),
265 format!("/usr/lib/lib{}.dylib", name),
266 format!("/opt/homebrew/lib/lib{}.dylib", name),
267 format!("/usr/local/lib/lib{}.dylib", name),
268 ]
269 }
270
271 #[cfg(not(target_os = "macos"))]
272 {
273 vec![
274 format!("lib{}.so", name),
275 format!("lib{}.so.1", name),
276 format!("lib{}.so.0", name),
277 format!("lib{}.so.2", name),
278 ]
279 }
280}
281
282fn system_library_status(name: &str) -> DependencyStatus {
288 #[cfg(unix)]
292 {
293 for candidate in library_candidates(name) {
294 if dlopen_succeeds(&candidate) {
295 debug!("dlopen({}) succeeded - lib{} is loadable", candidate, name);
296 return DependencyStatus::Available;
297 }
298 }
299 debug!(
300 "dlopen found no loadable lib{}; falling back to platform probes",
301 name
302 );
303 }
304
305 #[cfg(target_os = "linux")]
307 {
308 let output = Command::new("ldconfig").arg("-p").output();
309
310 if let Ok(output) = output {
311 let stdout = String::from_utf8_lossy(&output.stdout);
312 let lib_pattern = format!("lib{}.so", name);
313 let found = stdout.contains(&lib_pattern);
314 debug!("Checking for lib{}: found={}", name, found);
315 return if found {
316 DependencyStatus::Available
317 } else {
318 DependencyStatus::Missing
319 };
320 }
321
322 let pkg_config_output = Command::new("pkg-config")
324 .arg("--exists")
325 .arg(name)
326 .status();
327
328 if let Ok(status) = pkg_config_output {
329 let found = status.success();
330 debug!("Checking for {} via pkg-config: found={}", name, found);
331 return if found {
332 DependencyStatus::Available
333 } else {
334 DependencyStatus::Missing
335 };
336 }
337
338 debug!(
339 "Could not check for lib{} (ldconfig and pkg-config unavailable)",
340 name
341 );
342 DependencyStatus::Unknown(format!(
343 "dlopen found no lib{name} and neither ldconfig nor pkg-config could be run"
344 ))
345 }
346
347 #[cfg(target_os = "macos")]
348 {
349 let pkg_config_output = Command::new("pkg-config")
359 .arg("--exists")
360 .arg(name)
361 .status();
362
363 if let Ok(status) = pkg_config_output {
364 let found = status.success();
365 debug!("Checking for {} via pkg-config: found={}", name, found);
366 return if found {
367 DependencyStatus::Available
368 } else {
369 DependencyStatus::Missing
370 };
371 }
372
373 debug!("dlopen and pkg-config found no lib{} on macOS", name);
377 DependencyStatus::Missing
378 }
379
380 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
381 {
382 warn!(
384 "Cannot check for lib{} on this platform; not treating it as missing",
385 name
386 );
387 DependencyStatus::Unknown(format!("no library probe for lib{name} on this platform"))
388 }
389}
390
391fn tool_in_path_status(tool: &str) -> DependencyStatus {
398 let Some(path) = std::env::var_os("PATH") else {
399 return DependencyStatus::Unknown("PATH is not set, so there is nothing to search".into());
400 };
401
402 #[cfg(windows)]
403 let names: Vec<String> = ["", ".exe", ".cmd", ".bat", ".com"]
404 .iter()
405 .map(|ext| format!("{tool}{ext}"))
406 .collect();
407 #[cfg(not(windows))]
408 let names: Vec<String> = vec![tool.to_string()];
409
410 for dir in std::env::split_paths(&path) {
411 if dir.as_os_str().is_empty() {
412 continue;
413 }
414 for name in &names {
415 let candidate = dir.join(name);
416 if is_executable_file(&candidate) {
417 debug!("Found tool '{}' at {}", tool, candidate.display());
418 return DependencyStatus::Available;
419 }
420 }
421 }
422 debug!("Tool '{}' is not on PATH", tool);
423 DependencyStatus::Missing
424}
425
426fn is_executable_file(path: &std::path::Path) -> bool {
428 let Ok(metadata) = std::fs::metadata(path) else {
429 return false;
430 };
431 if !metadata.is_file() {
432 return false;
433 }
434 #[cfg(unix)]
435 {
436 use std::os::unix::fs::PermissionsExt;
437 metadata.permissions().mode() & 0o111 != 0
438 }
439 #[cfg(not(unix))]
440 {
441 true
442 }
443}
444
445pub fn startup_blocker(
453 deps: &[ProtocolDependency],
454 caps: &crate::privilege::SystemCapabilities,
455) -> Option<ProtocolDependency> {
456 for dep in deps {
457 if !matches!(
458 dep,
459 ProtocolDependency::SystemLibrary(_) | ProtocolDependency::ToolInPath(_)
460 ) {
461 continue;
462 }
463 match dep.status(caps) {
464 DependencyStatus::Available => {}
465 DependencyStatus::Missing => return Some(dep.clone()),
466 DependencyStatus::Unknown(why) => {
467 tracing::warn!(
468 "Could not determine whether {} is present ({}); starting anyway",
469 dep.name(),
470 why
471 );
472 }
473 }
474 }
475 None
476}
477
478#[cfg(target_os = "linux")]
480fn check_cap_net_admin() -> bool {
481 if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
482 for line in status.lines() {
483 if line.starts_with("CapEff:") {
484 if let Some(cap_hex) = line.split_whitespace().nth(1) {
485 if let Ok(caps) = u64::from_str_radix(cap_hex, 16) {
486 const CAP_NET_ADMIN: u64 = 1 << 12;
488 let has_cap = (caps & CAP_NET_ADMIN) != 0;
489 debug!(
490 "Checked CAP_NET_ADMIN via /proc: caps=0x{:x}, has_cap={}",
491 caps, has_cap
492 );
493 return has_cap;
494 }
495 }
496 }
497 }
498 }
499
500 debug!("Could not detect CAP_NET_ADMIN capability");
501 false
502}