use itertools::Itertools;
use std::{
fs::File,
os::fd::AsRawFd,
path::{Component, Path, PathBuf},
};
use sys_mount::{FilesystemType, Mount, MountFlags, Unmount, UnmountDrop, UnmountFlags};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct MountTarget {
pub target: PathBuf,
pub fstype: Option<String>,
pub flags: MountFlags,
pub data: Option<String>,
}
impl Default for MountTarget {
fn default() -> Self {
Self {
target: Default::default(),
fstype: Default::default(),
flags: MountFlags::empty(),
data: Default::default(),
}
}
}
impl MountTarget {
pub fn new(
target: PathBuf,
fstype: Option<String>,
flags: MountFlags,
data: Option<String>,
) -> Self {
Self {
target,
fstype,
flags,
data,
}
}
#[tracing::instrument]
pub fn mount(&self, source: &PathBuf, root: &Path) -> std::io::Result<UnmountDrop<Mount>> {
let target = self.target.strip_prefix("/").unwrap_or(&self.target);
tracing::info!(?root, "Mounting {source:?} to {target:?}");
let target = root.join(target);
std::fs::create_dir_all(&target)?;
let mut mount = Mount::builder().flags(self.flags);
if let Some(fstype) = &self.fstype {
mount = mount.fstype(FilesystemType::Manual(fstype));
}
if let Some(data) = &self.data {
mount = mount.data(data);
}
let mount = mount.mount_autodrop(source, &target, UnmountFlags::empty())?;
Ok(mount)
}
pub fn umount(&self, root: &Path) -> std::io::Result<()> {
let target = self.target.strip_prefix("/").unwrap_or(&self.target);
let target = root.join(target);
nix::mount::umount(&target)?;
Ok(())
}
}
#[derive(Default)]
pub struct MountTable {
inner: Vec<(PathBuf, MountTarget)>,
mounts: Vec<UnmountDrop<Mount>>,
}
impl MountTable {
pub fn new() -> Self {
Self::default()
}
pub fn set_table(&mut self, table: Vec<(PathBuf, MountTarget)>) {
self.inner = table;
}
pub fn add_mount(&mut self, mount: MountTarget, source: PathBuf) {
self.inner.push((source, mount));
}
pub fn add_sysmount(&mut self, mount: UnmountDrop<Mount>) {
self.mounts.push(mount);
}
fn sort_mounts(&self) -> impl Iterator<Item = &(PathBuf, MountTarget)> {
self.inner.iter().sorted_by(|(_, a), (_, b)| {
match (a.target.components().count(), b.target.components().count()) {
(1, _) if a.target.components().next() == Some(Component::RootDir) => {
std::cmp::Ordering::Less
} (_, 1) if b.target.components().next() == Some(Component::RootDir) => {
std::cmp::Ordering::Greater
} (x, y) if x == y => a.target.cmp(&b.target),
(x, y) => x.cmp(&y),
}
})
}
pub fn mount_chroot(&mut self, root: &Path) -> std::io::Result<()> {
self.mounts = self
.sort_mounts()
.map(|(source, mount)| {
tracing::trace!(?mount, ?source, "Mounting");
mount.mount(source, root)
})
.collect::<std::io::Result<_>>()?;
Ok(())
}
pub fn umount_chroot(&mut self) -> std::io::Result<()> {
self.mounts.drain(..).rev().try_for_each(|mount| {
tracing::trace!("Unmounting {:?}", mount.target_path());
mount.unmount(UnmountFlags::DETACH)
})
}
}
pub struct Container {
pub root: PathBuf,
pub mount_table: MountTable,
_initialized: bool,
chroot: bool,
sysroot: File,
pwd: File,
}
impl Container {
#[inline(always)]
pub fn chroot(&mut self) -> std::io::Result<()> {
if !self._initialized {
self.mount()?;
}
nix::unistd::chroot(&self.root)?;
self.chroot = true;
nix::unistd::chdir("/")?;
Ok(())
}
#[inline(always)]
pub fn exit_chroot(&mut self) -> std::io::Result<()> {
nix::unistd::fchdir(self.sysroot.as_raw_fd())?;
nix::unistd::chroot(".")?;
self.chroot = false;
nix::unistd::fchdir(self.pwd.as_raw_fd())?;
Ok(())
}
pub fn new(chrootpath: PathBuf) -> Self {
let pwd = std::fs::File::open("/proc/self/cwd").unwrap();
let sysroot = std::fs::File::open("/").unwrap();
let mut container = Self {
pwd,
root: chrootpath,
mount_table: MountTable::new(),
sysroot,
_initialized: false,
chroot: false,
};
container.setup_minimal_mounts();
container
}
#[inline(always)]
pub fn run<F, T>(&mut self, f: F) -> std::io::Result<T>
where
F: FnOnce() -> T,
{
if !self._initialized {
self.mount()?;
}
if !self.chroot {
self.chroot()?;
}
tracing::trace!("Running function inside container");
let ret = f();
if self.chroot {
self.exit_chroot()?;
}
if self._initialized {
self.umount()?;
}
Ok(ret)
}
pub fn mount(&mut self) -> std::io::Result<()> {
self.mount_table.mount_chroot(&self.root)?;
self._initialized = true;
Ok(())
}
pub fn umount(&mut self) -> std::io::Result<()> {
self.mount_table.umount_chroot()?;
self._initialized = false;
Ok(())
}
pub fn host_bind_mount(&mut self) -> &mut Self {
self.bind_mount(PathBuf::from("/"), PathBuf::from("/run/host"));
self
}
pub fn bind_mount(&mut self, source: PathBuf, target: PathBuf) {
self.mount_table.add_mount(
MountTarget {
target,
flags: MountFlags::BIND,
..MountTarget::default()
},
source,
);
}
pub fn add_mount(&mut self, mount: MountTarget, source: PathBuf) {
self.mount_table.add_mount(mount, source);
}
fn setup_minimal_mounts(&mut self) {
self.mount_table.add_mount(
MountTarget {
target: "proc".into(),
fstype: Some("proc".to_string()),
..MountTarget::default()
},
PathBuf::from("/proc"),
);
self.mount_table.add_mount(
MountTarget {
target: "sys".into(),
fstype: Some("sysfs".to_string()),
..MountTarget::default()
},
PathBuf::from("/sys"),
);
self.bind_mount("/dev".into(), "dev".into());
self.bind_mount("/dev/pts".into(), "dev/pts".into());
}
}
impl Drop for Container {
fn drop(&mut self) {
tracing::trace!("Dropping container, images will be unmounted");
if self.chroot {
self.exit_chroot().unwrap();
}
if self._initialized {
self.umount().unwrap();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[ignore = "This test requires root"]
#[test]
fn test_container() {
std::fs::create_dir_all("/tmp/tiffin").unwrap();
let mut container = Container::new(PathBuf::from("/tmp/tiffin"));
container
.run(|| std::fs::create_dir_all("/tmp/tiffin/test").unwrap())
.unwrap();
}
}