zlayer-overlay 0.14.2

Encrypted overlay networking for containers using boringtun userspace WireGuard
Documentation
//! TUN capability probing and edge-mode parsing for the edge client.
//!
//! Two concerns live here, both consumed by the edge CLI/client:
//!
//! - [`EdgeMode`] — parses the operator's `--mode` string (`auto` /
//!   `netstack` / `device`) into a typed choice.
//! - [`probe_device_mode`] — a cheap, total, cross-platform predicate that
//!   answers "could this host plausibly stand up a *real* TUN device (utun on
//!   macOS, `/dev/net/tun` on Linux, Wintun on Windows) right now?".
//!   [`EdgeMode::Auto`] consults it to decide whether *attempting* device mode
//!   is worthwhile before falling back to the userspace smoltcp netstack.

use std::str::FromStr;

use crate::edge::EdgeError;

/// Which transport the edge client should use to join the overlay.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeMode {
    /// Probe the host and pick the best available transport: a real TUN
    /// device when [`probe_device_mode`] reports [`DeviceVerdict::Grantable`],
    /// otherwise the unprivileged userspace netstack.
    Auto,
    /// Force the unprivileged userspace smoltcp netstack (no TUN device, no
    /// elevated privileges).
    Netstack,
    /// Force a real kernel TUN device (utun / `/dev/net/tun` / Wintun).
    Device,
}

impl FromStr for EdgeMode {
    type Err = EdgeError;

    /// Parse an edge-mode string, case-insensitively.
    ///
    /// # Errors
    ///
    /// Returns [`EdgeError::Probe`] for any value other than `auto`,
    /// `netstack`, or `device`.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_ascii_lowercase().as_str() {
            "auto" => Ok(Self::Auto),
            "netstack" => Ok(Self::Netstack),
            "device" => Ok(Self::Device),
            other => Err(EdgeError::Probe(format!(
                "unknown edge mode {other:?} (expected one of: auto, netstack, device)"
            ))),
        }
    }
}

/// Whether a real TUN device looks obtainable on this host right now.
///
/// Returned by [`probe_device_mode`]. `Denied` carries a human-readable reason
/// naming the missing prerequisite, suitable for surfacing to the operator.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceVerdict {
    /// Every cheap prerequisite for a real TUN device is satisfied; device
    /// mode is worth attempting.
    Grantable,
    /// A real TUN device is not obtainable; the payload explains why.
    Denied(String),
}

/// Best-effort predicate: can this host plausibly stand up a *real* TUN device
/// (utun on macOS, `/dev/net/tun` on Linux, Wintun on Windows) right now?
///
/// This is **intent, not a guarantee**. It only checks the cheap,
/// non-destructive prerequisites (effective capabilities, elevation, device
/// node) without creating any interface or allocating any kernel resource. The
/// client's real behavior is *try-device-then-fall-back*: even a
/// [`DeviceVerdict::Grantable`] verdict may still fail at actual
/// device-creation time, and the fallback to the userspace netstack lives in
/// the Wave-2 client — not here. [`EdgeMode::Auto`] uses this purely to decide
/// whether *attempting* device mode is worthwhile.
///
/// Total and cheap: never panics, creates no kernel resources, and allocates
/// nothing beyond the reason string on the `Denied` path.
#[must_use]
pub fn probe_device_mode() -> DeviceVerdict {
    #[cfg(target_os = "linux")]
    {
        // A real TUN device needs BOTH CAP_NET_ADMIN in the effective set (to
        // configure the interface) AND an openable /dev/net/tun (the device
        // node itself). Name whichever prerequisite is missing.
        let has_cap = crate::capability::probe_has_cap_net_admin();
        let has_dev = crate::capability::probe_tun_device_available();
        match (has_cap, has_dev) {
            (true, true) => DeviceVerdict::Grantable,
            (false, true) => DeviceVerdict::Denied(
                "no CAP_NET_ADMIN in the effective capability set".to_string(),
            ),
            (true, false) => DeviceVerdict::Denied("/dev/net/tun is not openable".to_string()),
            (false, false) => DeviceVerdict::Denied(
                "no CAP_NET_ADMIN and /dev/net/tun is not openable".to_string(),
            ),
        }
    }
    #[cfg(target_os = "macos")]
    {
        // Creating a utun interface requires root on macOS.
        if zlayer_paths::is_root() {
            DeviceVerdict::Grantable
        } else {
            DeviceVerdict::Denied("utun requires root on macOS".to_string())
        }
    }
    #[cfg(target_os = "windows")]
    {
        // Bringing up a Wintun adapter requires an elevated (Administrator)
        // token on Windows.
        if zlayer_paths::is_root() {
            DeviceVerdict::Grantable
        } else {
            DeviceVerdict::Denied("Wintun requires Administrator on Windows".to_string())
        }
    }
    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    {
        DeviceVerdict::Denied("TUN device mode is unsupported on this platform".to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::{DeviceVerdict, EdgeMode};
    use std::str::FromStr;

    #[test]
    fn edge_mode_from_str_accepts_all_three() {
        assert_eq!(EdgeMode::from_str("auto").unwrap(), EdgeMode::Auto);
        assert_eq!(EdgeMode::from_str("netstack").unwrap(), EdgeMode::Netstack);
        assert_eq!(EdgeMode::from_str("device").unwrap(), EdgeMode::Device);
    }

    #[test]
    fn edge_mode_from_str_is_case_insensitive_and_trims() {
        assert_eq!(EdgeMode::from_str("AUTO").unwrap(), EdgeMode::Auto);
        assert_eq!(EdgeMode::from_str("NetStack").unwrap(), EdgeMode::Netstack);
        assert_eq!(EdgeMode::from_str("  Device  ").unwrap(), EdgeMode::Device);
    }

    #[test]
    fn edge_mode_from_str_rejects_unknown() {
        let err = EdgeMode::from_str("bridge").unwrap_err();
        // The error must name the offending value and be a Probe error.
        let msg = err.to_string();
        assert!(
            msg.contains("bridge"),
            "reason should echo the bad value: {msg}"
        );
        assert!(
            msg.contains("edge probe error"),
            "should be a Probe error: {msg}"
        );
    }

    #[test]
    fn probe_device_mode_is_total_and_coherent() {
        // The concrete verdict is host-dependent (root vs unprivileged, TUN
        // present vs not), so we assert only that the call is total and that a
        // `Denied` verdict always carries a non-empty reason.
        match super::probe_device_mode() {
            DeviceVerdict::Grantable => {}
            DeviceVerdict::Denied(reason) => {
                assert!(!reason.is_empty(), "Denied must carry a human reason");
            }
        }
    }
}