Skip to main content

eggress_runtime/
platform.rs

1use std::collections::HashMap;
2use std::fmt;
3
4/// Platform-specific capabilities for transparent proxy and Unix socket support.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum PlatformCapability {
7    /// SO_ORIGINAL_DST for IPv4 on Linux (requires nf_conntrack / iptables DNAT).
8    LinuxOriginalDstIpv4,
9    /// IPv6 equivalent of SO_ORIGINAL_DST on Linux.
10    LinuxOriginalDstIpv6,
11    /// IP_TRANSPARENT socket option on Linux (for transparent proxy binding).
12    LinuxTransparentBind,
13    /// macOS PF integration for original destination retrieval.
14    MacosPfOriginalDst,
15    /// Unix domain socket support (AF_UNIX).
16    UnixDomainSockets,
17}
18
19/// Status of a platform capability check.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum CapabilityStatus {
22    /// The capability is available on this system.
23    Available,
24    /// The capability exists but requires elevated privileges.
25    MissingPrivilege,
26    /// The capability is not supported on this platform.
27    UnsupportedPlatform,
28    /// The capability is not supported by the running kernel.
29    KernelUnsupported,
30    /// The capability was disabled at compile time.
31    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/// Capability check result with the capability that was checked.
59#[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
71/// Check the status of a specific platform capability by probing the system.
72pub 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
82/// Check a platform capability, consulting an overrides map first.
83///
84/// If `overrides` contains the capability, its value is returned immediately.
85/// Otherwise the real system is probed. This is the primary entry point for
86/// tests that need deterministic results without global state.
87pub 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
99/// Return a summary of all platform capabilities and their statuses.
100///
101/// Useful for startup diagnostics to name which capabilities are missing.
102pub 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
112/// Return a summary using provided overrides for specified capabilities.
113pub 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
125/// All known platform capabilities.
126const ALL_CAPABILITIES: &[PlatformCapability] = &[
127    PlatformCapability::UnixDomainSockets,
128    PlatformCapability::LinuxOriginalDstIpv4,
129    PlatformCapability::LinuxOriginalDstIpv6,
130    PlatformCapability::LinuxTransparentBind,
131    PlatformCapability::MacosPfOriginalDst,
132];
133
134// ---------------------------------------------------------------------------
135// Unix domain sockets
136// ---------------------------------------------------------------------------
137
138fn 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// ---------------------------------------------------------------------------
170// Linux original destination (IPv4 and IPv6)
171// ---------------------------------------------------------------------------
172
173#[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/// Check if a netfilter proc file exists and contains at least one table name.
194#[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// ---------------------------------------------------------------------------
207// Linux transparent bind (IP_TRANSPARENT)
208// ---------------------------------------------------------------------------
209//
210// `ip_nonlocal_bind=1` is a sysctl value: it does NOT mean the running
211// process can successfully call setsockopt(IP_TRANSPARENT). Setting that
212// option requires CAP_NET_ADMIN (or root) and an active socket. We surface
213// the sysctl reading here for diagnostics only; the supervisor must treat
214// any later bind failure as the authoritative answer.
215//
216// A successful probe only indicates that the sysctl knob has been flipped
217// globally; it does not assert CAP_NET_ADMIN, nor that IP_TRANSPARENT will
218// actually succeed. Treat this as a soft hint, not a privilege assertion.
219
220#[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// ---------------------------------------------------------------------------
244// macOS PF original destination
245// ---------------------------------------------------------------------------
246//
247// macOS exposes PF via `/dev/pf`, but Eggress does not implement PF-based
248// original-destination recovery (see ADR_macos_pf_transparent_proxy.md).
249// Reporting `/dev/pf` as Available would falsely imply that running eggress
250// on macOS yields full transparent proxy semantics; we instead always
251// return UnsupportedPlatform so callers cannot route traffic on that
252// assumption.
253
254#[cfg(target_os = "macos")]
255fn check_macos_pf_original_dst() -> CapabilityStatus {
256    // Even when /dev/pf exists, Eggress has no PF integration.
257    CapabilityStatus::KernelUnsupported
258}
259
260#[cfg(not(target_os = "macos"))]
261fn check_macos_pf_original_dst() -> CapabilityStatus {
262    CapabilityStatus::UnsupportedPlatform
263}
264
265// ---------------------------------------------------------------------------
266// Display helpers for startup diagnostics
267// ---------------------------------------------------------------------------
268
269/// Format a list of capability reports as a human-readable diagnostic string.
270pub 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
278/// Filter reports to only those that are not available, for startup warnings.
279pub 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        // LinuxTransparentBind should return the override
380        assert_eq!(
381            check_capability_with_overrides(
382                PlatformCapability::LinuxTransparentBind,
383                Some(&overrides)
384            ),
385            CapabilityStatus::Available
386        );
387
388        // UnixDomainSockets has no override, falls through to real check
389        #[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        // With override: returns Available
448        assert_eq!(
449            check_capability_with_overrides(
450                PlatformCapability::MacosPfOriginalDst,
451                Some(&overrides)
452            ),
453            CapabilityStatus::Available
454        );
455
456        // Without override: PF is intentionally not implemented on any
457        // platform, so the real probe always reports either
458        // KernelUnsupported (macOS) or UnsupportedPlatform (non-macOS).
459        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    /// The `LinuxTransparentBind` capability reports the `ip_nonlocal_bind`
536    /// sysctl value, not a verified privilege or kernel feature. Override
537    /// paths verify that the sysctl returns the expected enum regardless
538    /// of host state.
539    #[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}