innisfree 0.4.3

Exposes local services on public IPv4 address, via cloud server.
Documentation
//! Pre-flight checks for the local environment.
//!
//! Validates that the binary has the privileges it needs to bring up
//! a Wireguard interface in-process via boringtun, and that the
//! cloud-provider credentials are available.

use anyhow::{anyhow, Result};
use std::fs;

/// Linux capability bit for `CAP_NET_ADMIN`. See `capabilities(7)`.
const CAP_NET_ADMIN: u64 = 12;

/// Remediation hint shown when `CAP_NET_ADMIN` is missing. Shared by
/// the advisory `platform_is_supported` path and the hard-gate
/// `require_cap_net_admin` used by `innisfree up`.
const CAP_NET_ADMIN_HINT: &str = "CAP_NET_ADMIN is not held — boringtun cannot open /dev/net/tun. \
     Fix with one of:\n  \
     setcap cap_net_admin+ep $(which innisfree)\n  \
     systemd: AmbientCapabilities=CAP_NET_ADMIN in the unit file";

/// Hard-gate variant of the cap check, for callers that must refuse
/// to proceed (e.g. `innisfree up`, before any cloud resources are
/// provisioned). Returns an error with the same remediation hints
/// `platform_is_supported` would log as a warning.
pub fn require_cap_net_admin() -> Result<()> {
    if has_cap_net_admin()? {
        Ok(())
    } else {
        Err(anyhow!(CAP_NET_ADMIN_HINT))
    }
}

/// Run all platform checks. Returns `Ok(true)` if everything looks
/// good, `Ok(false)` if any non-fatal check failed (logged via
/// `tracing::warn!`).
pub fn platform_is_supported() -> Result<bool> {
    let mut ok = true;

    if has_cap_net_admin()? {
        tracing::info!("CAP_NET_ADMIN is held — local Wireguard interface can be created");
    } else {
        tracing::warn!("{}", CAP_NET_ADMIN_HINT);
        ok = false;
    }

    if std::env::var("DIGITALOCEAN_API_TOKEN").is_ok() {
        tracing::info!("DIGITALOCEAN_API_TOKEN is set");
    } else {
        tracing::warn!("DIGITALOCEAN_API_TOKEN is not set — server provisioning will fail");
        ok = false;
    }

    Ok(ok)
}

/// Read the effective capability set from `/proc/self/status` and
/// return whether `CAP_NET_ADMIN` is held. Avoids pulling in the full
/// `caps` crate for a one-bit check.
fn has_cap_net_admin() -> Result<bool> {
    let status = fs::read_to_string("/proc/self/status")?;
    for line in status.lines() {
        if let Some(rest) = line.strip_prefix("CapEff:\t") {
            let bits = u64::from_str_radix(rest.trim(), 16)?;
            return Ok(bits & (1u64 << CAP_NET_ADMIN) != 0);
        }
    }
    Ok(false)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cap_check_does_not_panic() {
        // We don't assert the result — CI may or may not grant the
        // capability — only that the read+parse path is sound.
        let _ = has_cap_net_admin().unwrap();
    }

    #[test]
    fn require_matches_check() {
        // `require_cap_net_admin` must agree with `has_cap_net_admin`:
        // hard-error iff the cap is missing. Same caveat as above —
        // we don't assert which branch is taken, only the agreement.
        let held = has_cap_net_admin().unwrap();
        assert_eq!(held, require_cap_net_admin().is_ok());
    }
}