1use std::collections::HashMap;
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum PlatformCapability {
7 LinuxOriginalDstIpv4,
9 LinuxOriginalDstIpv6,
11 LinuxTransparentBind,
13 MacosPfOriginalDst,
15 UnixDomainSockets,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum CapabilityStatus {
22 Available,
24 MissingPrivilege,
26 UnsupportedPlatform,
28 KernelUnsupported,
30 DisabledAtCompileTime,
32}
33
34impl fmt::Display for PlatformCapability {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::LinuxOriginalDstIpv4 => write!(f, "LinuxOriginalDstIpv4"),
38 Self::LinuxOriginalDstIpv6 => write!(f, "LinuxOriginalDstIpv6"),
39 Self::LinuxTransparentBind => write!(f, "LinuxTransparentBind"),
40 Self::MacosPfOriginalDst => write!(f, "MacosPfOriginalDst"),
41 Self::UnixDomainSockets => write!(f, "UnixDomainSockets"),
42 }
43 }
44}
45
46impl fmt::Display for CapabilityStatus {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::Available => write!(f, "available"),
50 Self::MissingPrivilege => write!(f, "missing privilege"),
51 Self::UnsupportedPlatform => write!(f, "unsupported platform"),
52 Self::KernelUnsupported => write!(f, "kernel unsupported"),
53 Self::DisabledAtCompileTime => write!(f, "disabled at compile time"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct CapabilityReport {
61 pub capability: PlatformCapability,
62 pub status: CapabilityStatus,
63}
64
65impl fmt::Display for CapabilityReport {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(f, "{}: {}", self.capability, self.status)
68 }
69}
70
71pub fn check_capability(cap: PlatformCapability) -> CapabilityStatus {
73 match cap {
74 PlatformCapability::UnixDomainSockets => check_unix_domain_sockets(),
75 PlatformCapability::LinuxOriginalDstIpv4 => check_linux_original_dst_ipv4(),
76 PlatformCapability::LinuxOriginalDstIpv6 => check_linux_original_dst_ipv6(),
77 PlatformCapability::LinuxTransparentBind => check_linux_transparent_bind(),
78 PlatformCapability::MacosPfOriginalDst => check_macos_pf_original_dst(),
79 }
80}
81
82pub fn check_capability_with_overrides(
88 cap: PlatformCapability,
89 overrides: Option<&HashMap<PlatformCapability, CapabilityStatus>>,
90) -> CapabilityStatus {
91 if let Some(overrides) = overrides {
92 if let Some(status) = overrides.get(&cap) {
93 return status.clone();
94 }
95 }
96 check_capability(cap)
97}
98
99pub fn platform_info() -> Vec<CapabilityReport> {
103 ALL_CAPABILITIES
104 .iter()
105 .map(|&cap| CapabilityReport {
106 capability: cap,
107 status: check_capability(cap),
108 })
109 .collect()
110}
111
112pub fn platform_info_with_overrides(
114 overrides: &HashMap<PlatformCapability, CapabilityStatus>,
115) -> Vec<CapabilityReport> {
116 ALL_CAPABILITIES
117 .iter()
118 .map(|&cap| CapabilityReport {
119 capability: cap,
120 status: check_capability_with_overrides(cap, Some(overrides)),
121 })
122 .collect()
123}
124
125const ALL_CAPABILITIES: &[PlatformCapability] = &[
127 PlatformCapability::UnixDomainSockets,
128 PlatformCapability::LinuxOriginalDstIpv4,
129 PlatformCapability::LinuxOriginalDstIpv6,
130 PlatformCapability::LinuxTransparentBind,
131 PlatformCapability::MacosPfOriginalDst,
132];
133
134fn check_unix_domain_sockets() -> CapabilityStatus {
139 #[cfg(unix)]
140 {
141 check_unix_domain_sockets_unix()
142 }
143 #[cfg(not(unix))]
144 {
145 CapabilityStatus::UnsupportedPlatform
146 }
147}
148
149#[cfg(unix)]
150fn check_unix_domain_sockets_unix() -> CapabilityStatus {
151 use std::os::unix::net::UnixListener;
152
153 let dir = std::env::temp_dir();
154 let id: u64 = fastrand::u64(..);
155 let path = dir.join(format!("eggress_cap_test_{id}.sock"));
156
157 match UnixListener::bind(&path) {
158 Ok(_listener) => {
159 drop(std::fs::remove_file(&path));
160 CapabilityStatus::Available
161 }
162 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
163 CapabilityStatus::MissingPrivilege
164 }
165 Err(_) => CapabilityStatus::KernelUnsupported,
166 }
167}
168
169#[cfg(target_os = "linux")]
174fn check_linux_original_dst_ipv4() -> CapabilityStatus {
175 check_linux_netfilter_proc("/proc/net/ip_tables_names")
176}
177
178#[cfg(target_os = "linux")]
179fn check_linux_original_dst_ipv6() -> CapabilityStatus {
180 check_linux_netfilter_proc("/proc/net/ip6_tables_names")
181}
182
183#[cfg(not(target_os = "linux"))]
184fn check_linux_original_dst_ipv4() -> CapabilityStatus {
185 CapabilityStatus::UnsupportedPlatform
186}
187
188#[cfg(not(target_os = "linux"))]
189fn check_linux_original_dst_ipv6() -> CapabilityStatus {
190 CapabilityStatus::UnsupportedPlatform
191}
192
193#[cfg(target_os = "linux")]
195fn check_linux_netfilter_proc(path: &str) -> CapabilityStatus {
196 match std::fs::read_to_string(path) {
197 Ok(content) if !content.trim().is_empty() => CapabilityStatus::Available,
198 Ok(_) => CapabilityStatus::KernelUnsupported,
199 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
200 CapabilityStatus::MissingPrivilege
201 }
202 Err(_) => CapabilityStatus::KernelUnsupported,
203 }
204}
205
206#[cfg(target_os = "linux")]
221fn check_linux_transparent_bind() -> CapabilityStatus {
222 match std::fs::read_to_string("/proc/sys/net/ipv4/ip_nonlocal_bind") {
223 Ok(content) => {
224 let val = content.trim();
225 if val == "1" {
226 CapabilityStatus::Available
227 } else {
228 CapabilityStatus::KernelUnsupported
229 }
230 }
231 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
232 CapabilityStatus::MissingPrivilege
233 }
234 Err(_) => CapabilityStatus::KernelUnsupported,
235 }
236}
237
238#[cfg(not(target_os = "linux"))]
239fn check_linux_transparent_bind() -> CapabilityStatus {
240 CapabilityStatus::UnsupportedPlatform
241}
242
243#[cfg(target_os = "macos")]
255fn check_macos_pf_original_dst() -> CapabilityStatus {
256 CapabilityStatus::KernelUnsupported
258}
259
260#[cfg(not(target_os = "macos"))]
261fn check_macos_pf_original_dst() -> CapabilityStatus {
262 CapabilityStatus::UnsupportedPlatform
263}
264
265pub fn format_capability_report(reports: &[CapabilityReport]) -> String {
271 let mut out = String::from("Platform capabilities:\n");
272 for report in reports {
273 out.push_str(&format!(" {}: {}\n", report.capability, report.status));
274 }
275 out
276}
277
278pub fn missing_capabilities(reports: &[CapabilityReport]) -> Vec<&CapabilityReport> {
280 reports
281 .iter()
282 .filter(|r| r.status != CapabilityStatus::Available)
283 .collect()
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn display_platform_capability() {
292 assert_eq!(
293 PlatformCapability::LinuxOriginalDstIpv4.to_string(),
294 "LinuxOriginalDstIpv4"
295 );
296 assert_eq!(
297 PlatformCapability::UnixDomainSockets.to_string(),
298 "UnixDomainSockets"
299 );
300 }
301
302 #[test]
303 fn display_capability_status() {
304 assert_eq!(CapabilityStatus::Available.to_string(), "available");
305 assert_eq!(
306 CapabilityStatus::MissingPrivilege.to_string(),
307 "missing privilege"
308 );
309 assert_eq!(
310 CapabilityStatus::UnsupportedPlatform.to_string(),
311 "unsupported platform"
312 );
313 assert_eq!(
314 CapabilityStatus::KernelUnsupported.to_string(),
315 "kernel unsupported"
316 );
317 assert_eq!(
318 CapabilityStatus::DisabledAtCompileTime.to_string(),
319 "disabled at compile time"
320 );
321 }
322
323 #[test]
324 fn capability_report_display() {
325 let report = CapabilityReport {
326 capability: PlatformCapability::UnixDomainSockets,
327 status: CapabilityStatus::Available,
328 };
329 assert_eq!(report.to_string(), "UnixDomainSockets: available");
330 }
331
332 #[test]
333 fn unix_domain_sockets_available_on_unix() {
334 #[cfg(unix)]
335 {
336 assert_eq!(
337 check_capability(PlatformCapability::UnixDomainSockets),
338 CapabilityStatus::Available
339 );
340 }
341 }
342
343 #[test]
344 fn override_returns_override_value() {
345 let mut overrides = HashMap::new();
346 overrides.insert(
347 PlatformCapability::LinuxOriginalDstIpv4,
348 CapabilityStatus::Available,
349 );
350 overrides.insert(
351 PlatformCapability::UnixDomainSockets,
352 CapabilityStatus::KernelUnsupported,
353 );
354
355 assert_eq!(
356 check_capability_with_overrides(
357 PlatformCapability::LinuxOriginalDstIpv4,
358 Some(&overrides)
359 ),
360 CapabilityStatus::Available
361 );
362 assert_eq!(
363 check_capability_with_overrides(
364 PlatformCapability::UnixDomainSockets,
365 Some(&overrides)
366 ),
367 CapabilityStatus::KernelUnsupported
368 );
369 }
370
371 #[test]
372 fn override_does_not_affect_unset_capabilities() {
373 let mut overrides = HashMap::new();
374 overrides.insert(
375 PlatformCapability::LinuxTransparentBind,
376 CapabilityStatus::Available,
377 );
378
379 assert_eq!(
381 check_capability_with_overrides(
382 PlatformCapability::LinuxTransparentBind,
383 Some(&overrides)
384 ),
385 CapabilityStatus::Available
386 );
387
388 #[allow(unused_variables)]
390 let real_status = check_capability_with_overrides(
391 PlatformCapability::UnixDomainSockets,
392 Some(&overrides),
393 );
394
395 #[cfg(unix)]
396 assert_eq!(real_status, CapabilityStatus::Available);
397 }
398
399 #[test]
400 fn platform_info_returns_all_capabilities() {
401 let info = platform_info();
402 assert_eq!(info.len(), 5);
403
404 let names: Vec<_> = info.iter().map(|r| r.capability.to_string()).collect();
405 assert!(names.contains(&"UnixDomainSockets".to_string()));
406 assert!(names.contains(&"LinuxOriginalDstIpv4".to_string()));
407 assert!(names.contains(&"MacosPfOriginalDst".to_string()));
408 }
409
410 #[test]
411 fn format_capability_report_contains_all_names() {
412 let info = platform_info();
413 let formatted = format_capability_report(&info);
414 assert!(formatted.contains("UnixDomainSockets"));
415 assert!(formatted.contains("LinuxOriginalDstIpv4"));
416 assert!(formatted.contains("Platform capabilities:"));
417 }
418
419 #[test]
420 fn missing_capabilities_filters_available() {
421 let reports = vec![
422 CapabilityReport {
423 capability: PlatformCapability::UnixDomainSockets,
424 status: CapabilityStatus::Available,
425 },
426 CapabilityReport {
427 capability: PlatformCapability::LinuxOriginalDstIpv4,
428 status: CapabilityStatus::UnsupportedPlatform,
429 },
430 ];
431 let missing = missing_capabilities(&reports);
432 assert_eq!(missing.len(), 1);
433 assert_eq!(
434 missing[0].capability,
435 PlatformCapability::LinuxOriginalDstIpv4
436 );
437 }
438
439 #[test]
440 fn override_roundtrip() {
441 let mut overrides = HashMap::new();
442 overrides.insert(
443 PlatformCapability::MacosPfOriginalDst,
444 CapabilityStatus::Available,
445 );
446
447 assert_eq!(
449 check_capability_with_overrides(
450 PlatformCapability::MacosPfOriginalDst,
451 Some(&overrides)
452 ),
453 CapabilityStatus::Available
454 );
455
456 let real = check_capability(PlatformCapability::MacosPfOriginalDst);
460 match real {
461 CapabilityStatus::KernelUnsupported => {}
462 #[cfg(not(target_os = "macos"))]
463 CapabilityStatus::UnsupportedPlatform => {}
464 other => panic!("unexpected status: {other:?}"),
465 }
466 }
467
468 #[test]
469 fn platform_info_respects_overrides() {
470 let mut overrides = HashMap::new();
471 overrides.insert(
472 PlatformCapability::LinuxOriginalDstIpv4,
473 CapabilityStatus::Available,
474 );
475 overrides.insert(
476 PlatformCapability::LinuxOriginalDstIpv6,
477 CapabilityStatus::MissingPrivilege,
478 );
479
480 let info = platform_info_with_overrides(&overrides);
481 assert_eq!(info.len(), 5);
482
483 let ipv4 = info
484 .iter()
485 .find(|r| r.capability == PlatformCapability::LinuxOriginalDstIpv4)
486 .unwrap();
487 assert_eq!(ipv4.status, CapabilityStatus::Available);
488
489 let ipv6 = info
490 .iter()
491 .find(|r| r.capability == PlatformCapability::LinuxOriginalDstIpv6)
492 .unwrap();
493 assert_eq!(ipv6.status, CapabilityStatus::MissingPrivilege);
494 }
495
496 #[test]
497 fn linux_checks_return_unsupported_on_non_linux() {
498 #[cfg(not(target_os = "linux"))]
499 {
500 assert_eq!(
501 check_capability(PlatformCapability::LinuxOriginalDstIpv4),
502 CapabilityStatus::UnsupportedPlatform
503 );
504 assert_eq!(
505 check_capability(PlatformCapability::LinuxOriginalDstIpv6),
506 CapabilityStatus::UnsupportedPlatform
507 );
508 assert_eq!(
509 check_capability(PlatformCapability::LinuxTransparentBind),
510 CapabilityStatus::UnsupportedPlatform
511 );
512 }
513 }
514
515 #[test]
516 fn macos_check_returns_unsupported_on_non_macos() {
517 #[cfg(not(target_os = "macos"))]
518 {
519 assert_eq!(
520 check_capability(PlatformCapability::MacosPfOriginalDst),
521 CapabilityStatus::UnsupportedPlatform
522 );
523 }
524 }
525
526 #[test]
527 fn none_overrides_probes_real_system() {
528 let result = check_capability_with_overrides(PlatformCapability::UnixDomainSockets, None);
529 #[cfg(unix)]
530 assert_eq!(result, CapabilityStatus::Available);
531 #[cfg(not(unix))]
532 assert_eq!(result, CapabilityStatus::UnsupportedPlatform);
533 }
534
535 #[test]
540 fn linux_transparent_bind_override_paths() {
541 for status in [
542 CapabilityStatus::Available,
543 CapabilityStatus::MissingPrivilege,
544 CapabilityStatus::KernelUnsupported,
545 CapabilityStatus::UnsupportedPlatform,
546 CapabilityStatus::DisabledAtCompileTime,
547 ] {
548 let mut overrides = HashMap::new();
549 overrides.insert(PlatformCapability::LinuxTransparentBind, status.clone());
550 assert_eq!(
551 check_capability_with_overrides(
552 PlatformCapability::LinuxTransparentBind,
553 Some(&overrides),
554 ),
555 status,
556 );
557 }
558 }
559
560 #[cfg(target_os = "linux")]
561 #[test]
562 fn linux_transparent_bind_real_probe_returns_known_status() {
563 let status = check_capability(PlatformCapability::LinuxTransparentBind);
564 match status {
565 CapabilityStatus::Available | CapabilityStatus::KernelUnsupported => {}
566 other => panic!(
567 "LinuxTransparentBind real probe must be Available or KernelUnsupported (sysctl read), got {other:?}"
568 ),
569 }
570 }
571
572 #[cfg(target_os = "linux")]
573 #[test]
574 fn linux_original_dst_real_probe_returns_known_status() {
575 let v4 = check_capability(PlatformCapability::LinuxOriginalDstIpv4);
576 let v6 = check_capability(PlatformCapability::LinuxOriginalDstIpv6);
577 for s in [&v4, &v6] {
578 match s {
579 CapabilityStatus::Available
580 | CapabilityStatus::MissingPrivilege
581 | CapabilityStatus::KernelUnsupported => {}
582 other => panic!("unexpected real-probe status: {other:?}"),
583 }
584 }
585 }
586
587 #[cfg(target_os = "macos")]
588 #[test]
589 fn macos_pf_real_probe_always_kernel_unsupported() {
590 let status = check_capability(PlatformCapability::MacosPfOriginalDst);
591 assert_eq!(
592 status,
593 CapabilityStatus::KernelUnsupported,
594 "PF integration is intentionally unimplemented; probe must not claim Available"
595 );
596 }
597}