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}