use super::{Init, Mount};
use crate::{
common::container::Container,
npk::{
manifest,
manifest::{mount, Manifest},
},
runtime::{config::Config, error::Error, state::State},
seccomp,
};
use itertools::Itertools;
use log::{debug, warn};
use nix::mount::MsFlags;
use std::{
ffi::{c_void, CString},
path::{Path, PathBuf},
ptr::null,
};
trait PathExt {
fn join_strip<T: AsRef<Path>>(&self, w: T) -> PathBuf;
}
pub async fn build<'a, I: Iterator<Item = &'a Container> + Clone>(
config: &Config,
manifest: &Manifest,
containers: I,
selinux: bool,
) -> Result<Init, Error> {
let container = manifest.container();
let root = config.run_dir.join(container.to_string());
let capabilities = manifest.capabilities.clone();
let console = manifest.console.is_some();
let gid = manifest.gid;
let sched = manifest.sched.clone();
let network = manifest.network.clone();
let groups = groups(manifest);
let mounts = prepare_mounts(config, &root, manifest, containers).await?;
let rlimits = manifest.rlimits.clone();
let seccomp = seccomp_filter(manifest);
let uid = manifest.uid;
let sockets = manifest
.sockets
.keys()
.cloned()
.map(Into::into)
.sorted()
.collect();
let selinux = selinux
.then_some(())
.and(manifest.selinux.as_ref())
.cloned();
Ok(Init {
container,
root,
uid,
gid,
sched,
mounts,
groups,
network,
capabilities,
rlimits,
seccomp,
console,
sockets,
selinux,
})
}
fn groups(manifest: &Manifest) -> Vec<u32> {
let mut result = Vec::with_capacity(manifest.suppl_groups.len());
for group in &manifest.suppl_groups {
let cgroup: CString = group.clone().into();
let group_info =
unsafe { nix::libc::getgrnam(cgroup.as_ptr() as *const nix::libc::c_char) };
if group_info == (null::<c_void>() as *mut nix::libc::group) {
log::warn!("Skipping invalid supplementary group {}", group);
} else {
let gid = unsafe { (*group_info).gr_gid };
result.push(gid)
}
}
result
}
fn seccomp_filter(manifest: &Manifest) -> Option<seccomp::AllowList> {
manifest.seccomp.as_ref().map(|seccomp| {
seccomp::seccomp_filter(
seccomp.profile.as_ref(),
seccomp.allow.as_ref(),
&manifest.capabilities,
)
})
}
async fn prepare_mounts<'a, I: Iterator<Item = &'a Container> + Clone>(
config: &Config,
root: &Path,
manifest: &Manifest,
containers: I,
) -> Result<Vec<Mount>, Error> {
let mut mounts = vec![];
let manifest_mounts = &manifest.mounts;
for (target, mount) in manifest_mounts {
match mount {
mount::Mount::Bind(manifest::mount::Bind { host, options }) => {
mounts.extend(bind(root, target.as_ref(), host.as_ref(), options));
}
mount::Mount::Persist => {
mounts.push(persist(config, manifest, root, target.as_ref()).await?);
}
mount::Mount::Proc => mounts.push(proc(root, target.as_ref())),
mount::Mount::Sysfs => mounts.push(sysfs(root, target.as_ref())),
mount::Mount::Sockets => {
mounts.push(sockets(&config.socket_dir, root, target.as_ref()))
}
mount::Mount::Resource(requirement) => {
let container = manifest.container();
let dependency = State::match_container(
&requirement.name,
&requirement.version,
containers.clone(),
)
.expect("failed to locate required resource container"); let (mount, remount_ro) = resource(
root,
target.as_ref(),
config,
&container,
dependency,
requirement.dir.as_ref(),
&requirement.options,
)?;
mounts.push(mount);
mounts.push(remount_ro);
}
mount::Mount::Tmpfs(mount::Tmpfs { size }) => {
mounts.push(tmpfs(root, target.as_ref(), *size))
}
mount::Mount::Dev => {}
}
}
Ok(mounts)
}
fn proc(root: &Path, target: &Path) -> Mount {
debug!(
"Adding proc on {} with options ro, nosuid, noexec and nodev",
target.display()
);
let source = PathBuf::from("proc");
let target = root.join_strip(target);
const FSTYPE: Option<&'static str> = Some("proc");
let flags = MsFlags::MS_RDONLY | MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV;
Mount::new(Some(source), target, FSTYPE, flags, None)
}
fn sysfs(root: &Path, target: &Path) -> Mount {
debug!(
"Adding sysfs on {} with options ro, nosuid, noexec and nodev",
target.display()
);
let source = PathBuf::from("sysfs");
let target = root.join_strip(target);
const FSTYPE: Option<&'static str> = Some("sysfs");
let flags = MsFlags::MS_RDONLY | MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV;
Mount::new(Some(source), target, FSTYPE, flags, None)
}
fn sockets(socket_dir: &Path, root: &Path, target: &Path) -> Mount {
debug!("Adding sockets on {}", target.display());
let target = root.join_strip(target);
Mount::new(
Some(socket_dir.to_owned()),
target,
None,
MsFlags::MS_BIND,
None,
)
}
fn bind(root: &Path, target: &Path, host: &Path, options: &mount::MountOptions) -> Vec<Mount> {
if host.exists() {
let rw = options.contains(&mount::MountOption::Rw);
let mut mounts = Vec::with_capacity(if rw { 2 } else { 1 });
debug!(
"Adding {} on {} with flags {}",
host.display(),
target.display(),
options
);
let source = host.to_owned();
let target = root.join_strip(target);
let mut flags = options_to_flags(options);
flags.set(MsFlags::MS_BIND, true);
mounts.push(Mount::new(
Some(source.clone()),
target.clone(),
None,
flags,
None,
));
if !rw {
debug!(
"Adding read only remount of {} on {}",
host.display(),
target.display()
);
flags.set(MsFlags::MS_REMOUNT, true);
flags.set(MsFlags::MS_RDONLY, true);
mounts.push(Mount::new(Some(source), target, None, flags, None));
}
mounts
} else {
debug!(
"Skipping bind mount of nonexistent source {} to {}",
host.display(),
target.display()
);
vec![]
}
}
async fn persist(
config: &Config,
manifest: &Manifest,
root: &Path,
target: &Path,
) -> Result<Mount, Error> {
let source = config.data_dir.join(manifest.name.as_ref());
debug!(
"Adding {} on {} with options nodev, nosuid and noexec",
source.display(),
target.display(),
);
let target = root.join_strip(target);
let flags = MsFlags::MS_BIND | MsFlags::MS_NODEV | MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC;
Ok(Mount::new(Some(source), target, None, flags, None))
}
fn resource(
root: &Path,
target: &Path,
config: &Config,
container: &Container,
dependency: &Container,
dir: &Path,
options: &mount::MountOptions,
) -> Result<(Mount, Mount), Error> {
let src = {
let resource_root =
config
.run_dir
.join(format!("{}:{}", dependency.name(), dependency.version()));
let src = resource_root.join_strip(dir);
if !src.exists() {
warn!(
"Resource directory {} is missing in resource rootfs",
dir.display()
);
return Err(Error::StartContainerMissingResource(
container.clone(),
dependency.name().clone(),
dependency.version().to_string(),
));
}
src
};
debug!(
"Mounting {} on {} with {}",
src.display(),
target.display(),
options
);
let target = root.join_strip(target);
let mut flags = options_to_flags(options);
flags |= MsFlags::MS_RDONLY | MsFlags::MS_BIND;
let mount = Mount::new(Some(src.clone()), target.clone(), None, flags, None);
flags.set(MsFlags::MS_REMOUNT, true);
let remount_ro = Mount::new(Some(src), target, None, flags, None);
Ok((mount, remount_ro))
}
fn tmpfs(root: &Path, target: &Path, size: u64) -> Mount {
debug!(
"Mounting tmpfs with size {} on {}",
bytesize::ByteSize::b(size),
target.display()
);
let target = root.join_strip(target);
let fstype = "tmpfs";
let flags = MsFlags::MS_NODEV | MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC;
let data = format!("size={size},mode=1777");
Mount::new(None, target, Some(fstype), flags, Some(data))
}
fn options_to_flags(opt: &mount::MountOptions) -> MsFlags {
let mut flags = MsFlags::empty();
for opt in opt.iter() {
match opt {
mount::MountOption::Rw => {}
mount::MountOption::NoExec => flags |= MsFlags::MS_NOEXEC,
mount::MountOption::NoSuid => flags |= MsFlags::MS_NOSUID,
mount::MountOption::NoDev => flags |= MsFlags::MS_NODEV,
mount::MountOption::Rec => flags |= MsFlags::MS_REC,
}
}
flags
}
impl PathExt for Path {
fn join_strip<T: AsRef<Path>>(&self, w: T) -> PathBuf {
self.join(match w.as_ref().strip_prefix("/") {
Ok(stripped) => stripped,
Err(_) => w.as_ref(),
})
}
}