mod fs;
mod handlers;
use std::ffi::OsString;
use std::os::unix::process::ExitStatusExt;
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use fuser::{Config, MountOption};
use nix::errno::Errno;
use nix::fcntl::{open, OFlag};
use nix::mount::{mount, MsFlags};
use nix::sched::{unshare, CloneFlags};
use nix::sys::stat::Mode;
use crate::fs::OverlayFs;
fn main() {
let mut args = std::env::args_os();
let _self = args.next();
let program = match args.next() {
Some(p) => p,
None => {
eprintln!("usage: airgap <program> [args...]");
std::process::exit(2);
}
};
let program_args: Vec<OsString> = args.collect();
match run(&program, &program_args) {
Ok(code) => std::process::exit(code),
Err(e) => {
eprintln!("airgap: {e:#}");
std::process::exit(1);
}
}
}
fn run(program: &OsString, program_args: &[OsString]) -> Result<i32> {
unshare(CloneFlags::CLONE_NEWNS).map_err(|e| match e {
Errno::EPERM => anyhow!(missing_cap_sys_admin_msg()),
other => anyhow!("unshare(CLONE_NEWNS) failed: {other}"),
})?;
mount(
None::<&str>,
"/",
None::<&str>,
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
None::<&str>,
)
.context("making mounts private")?;
let cwd = std::env::current_dir().context("getting current dir")?;
let root = open(
&cwd,
OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
Mode::empty(),
)
.with_context(|| format!("opening working directory {}", cwd.display()))?;
let mut config = Config::default();
config.mount_options = vec![MountOption::FSName("airgap".into())];
let session = fuser::spawn_mount2(OverlayFs::new(root), &cwd, &config)
.with_context(|| format!("mounting overlay at {}", cwd.display()))?;
std::env::set_current_dir(&cwd)
.with_context(|| format!("re-entering working directory {}", cwd.display()))?;
let result = spawn_and_wait(program, program_args);
drop(session); result
}
fn missing_cap_sys_admin_msg() -> String {
let exe = std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "airgap".to_string());
format!(
"missing CAP_SYS_ADMIN, required to create a mount namespace and mount \
the FUSE overlay.\n Grant it to the binary once with:\n\n sudo \
setcap cap_sys_admin+ep {exe}\n\n \
(the capability is lost on rebuild/copy, so re-run it after each \
`cargo build` or `cargo install`), or run airgap inside an unprivileged user namespace."
)
}
fn spawn_and_wait(program: &OsString, program_args: &[OsString]) -> Result<i32> {
let status = Command::new(program)
.args(program_args)
.status()
.with_context(|| format!("running {program:?}"))?;
Ok(match status.code() {
Some(code) => code,
None => 128 + status.signal().unwrap_or(0),
})
}