Skip to main content

zlayer_overlay/
capability.rs

1//! Cheap TUN / `CAP_NET_ADMIN` capability probes.
2//!
3//! Single source of truth for the two syscall-level probes both the zlayer
4//! daemon (`zlayer-agent`) and the unprivileged edge client use to decide
5//! whether overlay networking is viable:
6//!
7//! - [`probe_tun_device_available`] — can `/dev/net/tun` be opened r/w?
8//! - [`probe_has_cap_net_admin`] — is `CAP_NET_ADMIN` in the effective set?
9//!
10//! Both are intentionally cheap and non-destructive — a single `open(2)` whose
11//! fd is dropped immediately, or one read of `/proc/self/status`. Neither
12//! allocates any kernel resource (no TUN interface is created, no capability is
13//! changed), so they are safe to call repeatedly. Non-Linux targets report a
14//! fixed `false`, since the kernel features these probes target are Linux-only.
15
16/// `true` if `CAP_NET_ADMIN` is present in the process's *effective* set
17/// (the `CapEff:` line of `/proc/self/status`). Linux only; always `false` on
18/// other targets.
19#[cfg(target_os = "linux")]
20#[must_use]
21pub fn probe_has_cap_net_admin() -> bool {
22    // CAP_NET_ADMIN is bit 12 in the Linux capability bitmask.
23    // We need it in the EFFECTIVE set (`CapEff`), not just the bounding set
24    // (`CapBnd`). A regular user process has full CapBnd by default but empty
25    // CapPrm/CapEff — checking PR_CAPBSET_READ gives a false positive that
26    // makes the daemon think it can create TUN/WG interfaces when it cannot.
27    const CAP_NET_ADMIN_BIT: u64 = 1 << 12;
28    let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
29        return false;
30    };
31    for line in status.lines() {
32        if let Some(hex) = line.strip_prefix("CapEff:") {
33            let trimmed = hex.trim();
34            if let Ok(eff) = u64::from_str_radix(trimmed, 16) {
35                return eff & CAP_NET_ADMIN_BIT != 0;
36            }
37            return false;
38        }
39    }
40    false
41}
42
43#[cfg(not(target_os = "linux"))]
44#[must_use]
45pub fn probe_has_cap_net_admin() -> bool {
46    false
47}
48
49/// `true` if `/dev/net/tun` can be opened r/w in non-blocking mode without
50/// error. The fd is dropped immediately (no TUN interface is allocated). Linux
51/// only; always `false` on other targets.
52#[cfg(target_os = "linux")]
53#[must_use]
54pub fn probe_tun_device_available() -> bool {
55    use std::os::unix::fs::OpenOptionsExt;
56
57    // Opening /dev/net/tun without any ioctls is benign and does not allocate
58    // a TUN interface. The fd is dropped immediately when this scope ends.
59    // Any open error — missing device, no perms, kernel module not loaded,
60    // FD exhaustion — means we can't actually use TUN. Treat as unavailable.
61    std::fs::OpenOptions::new()
62        .read(true)
63        .write(true)
64        .custom_flags(libc::O_NONBLOCK)
65        .open("/dev/net/tun")
66        .is_ok()
67}
68
69#[cfg(not(target_os = "linux"))]
70#[must_use]
71pub fn probe_tun_device_available() -> bool {
72    false
73}
74
75#[cfg(test)]
76mod tests {
77    #[test]
78    fn tun_probe_is_total_and_does_not_panic() {
79        // The concrete result depends on the host, but the call must be total.
80        let _ = super::probe_tun_device_available();
81    }
82
83    #[test]
84    fn cap_net_admin_probe_is_total_and_does_not_panic() {
85        let _ = super::probe_has_cap_net_admin();
86    }
87
88    #[cfg(target_os = "linux")]
89    #[test]
90    fn probe_has_cap_net_admin_matches_cap_eff() {
91        // Just confirm the probe agrees with what /proc/self/status reports.
92        // The actual capability state depends on how the test is run (regular
93        // user vs root vs setcap'd binary), but the probe MUST agree with the
94        // CapEff line — that's the whole point of the bug fix.
95        let status = std::fs::read_to_string("/proc/self/status").unwrap();
96        let cap_eff_line = status
97            .lines()
98            .find(|l| l.starts_with("CapEff:"))
99            .expect("CapEff: present in /proc/self/status");
100        let hex = cap_eff_line.trim_start_matches("CapEff:").trim();
101        let eff: u64 = u64::from_str_radix(hex, 16).unwrap();
102        let expected = (eff & (1u64 << 12)) != 0;
103        assert_eq!(super::probe_has_cap_net_admin(), expected);
104    }
105}