forked from brl/citadel-tools
refactor of RealmFS into several components
This commit is contained in:
parent
fcbf63db8e
commit
b7d4f1e570
@ -1,224 +0,0 @@
|
|||||||
|
|
||||||
use std::path::{Path,PathBuf};
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use crate::{ImageHeader,MetaInfo,Mount,Result,util,verity};
|
|
||||||
|
|
||||||
const BASE_PATH: &'static str = "/storage/realms/realmfs-images";
|
|
||||||
const RUN_DIRECTORY: &str = "/run/citadel/images";
|
|
||||||
const MAX_REALMFS_NAME_LEN: usize = 40;
|
|
||||||
|
|
||||||
|
|
||||||
pub struct RealmFS {
|
|
||||||
path: PathBuf,
|
|
||||||
mountpoint: PathBuf,
|
|
||||||
header: ImageHeader,
|
|
||||||
metainfo: MetaInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RealmFS {
|
|
||||||
|
|
||||||
/// Locate a RealmFS image by name in the default location using the standard name convention
|
|
||||||
pub fn load_by_name(name: &str) -> Result<RealmFS> {
|
|
||||||
if !util::is_valid_name(name, MAX_REALMFS_NAME_LEN) {
|
|
||||||
bail!("Invalid realmfs name '{}'", name);
|
|
||||||
}
|
|
||||||
let path = Path::new(BASE_PATH).join(format!("{}-realmfs.img", name));
|
|
||||||
if !path.exists() {
|
|
||||||
bail!("No image found at {}", path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
RealmFS::load_from_path(path, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_image_exists(name: &str) -> bool {
|
|
||||||
if !util::is_valid_name(name, MAX_REALMFS_NAME_LEN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let path = Path::new(BASE_PATH).join(format!("{}-realmfs.img", name));
|
|
||||||
path.exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load RealmFS image from an exact path.
|
|
||||||
pub fn load_from_path<P: AsRef<Path>>(path: P, name: &str) -> Result<RealmFS> {
|
|
||||||
let path = path.as_ref().to_owned();
|
|
||||||
let header = ImageHeader::from_file(&path)?;
|
|
||||||
if !header.is_magic_valid() {
|
|
||||||
bail!("Image file {} does not have a valid header", path.display());
|
|
||||||
}
|
|
||||||
let metainfo = header.metainfo()?;
|
|
||||||
let mountpoint = PathBuf::from(format!("{}/{}-realmfs.mountpoint", RUN_DIRECTORY, name));
|
|
||||||
|
|
||||||
Ok(RealmFS{
|
|
||||||
path,
|
|
||||||
mountpoint,
|
|
||||||
header,
|
|
||||||
metainfo,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mount_rw(&mut self) -> Result<()> {
|
|
||||||
// XXX fail if already verity mounted?
|
|
||||||
// XXX strip dm-verity tree if present?
|
|
||||||
// XXX just remount if not verity mounted, but currently ro mounted?
|
|
||||||
self.mount(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mount_ro(&mut self) -> Result<()> {
|
|
||||||
self.mount(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mount(&mut self, read_only: bool) -> Result<()> {
|
|
||||||
let flags = if read_only {
|
|
||||||
Some("-oro")
|
|
||||||
} else {
|
|
||||||
Some("-orw")
|
|
||||||
};
|
|
||||||
if !self.mountpoint.exists() {
|
|
||||||
fs::create_dir_all(self.mountpoint())?;
|
|
||||||
}
|
|
||||||
let loopdev = self.create_loopdev()?;
|
|
||||||
util::mount(&loopdev.to_string_lossy(), self.mountpoint(), flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mount_verity(&self) -> Result<()> {
|
|
||||||
if self.is_mounted() {
|
|
||||||
bail!("RealmFS image is already mounted");
|
|
||||||
}
|
|
||||||
if !self.is_sealed() {
|
|
||||||
bail!("Cannot verity mount RealmFS image because it's not sealed");
|
|
||||||
}
|
|
||||||
if !self.mountpoint.exists() {
|
|
||||||
fs::create_dir_all(self.mountpoint())?;
|
|
||||||
}
|
|
||||||
let dev = self.setup_verity_device()?;
|
|
||||||
util::mount(&dev.to_string_lossy(), &self.mountpoint, Some("-oro"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_verity_device(&self) -> Result<PathBuf> {
|
|
||||||
|
|
||||||
// TODO verify signature
|
|
||||||
|
|
||||||
if !self.header.has_flag(ImageHeader::FLAG_HASH_TREE) {
|
|
||||||
self.generate_verity()?;
|
|
||||||
}
|
|
||||||
verity::setup_image_device(&self.path, &self.metainfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_loopdev(&self) -> Result<PathBuf> {
|
|
||||||
let args = format!("--offset 4096 -f --show {}", self.path.display());
|
|
||||||
let output = util::exec_cmdline_with_output("/sbin/losetup", args)?;
|
|
||||||
Ok(PathBuf::from(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_mounted(&self) -> bool {
|
|
||||||
match Mount::is_target_mounted(self.mountpoint()) {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error reading /proc/mounts: {}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fork(&self, new_name: &str) -> Result<RealmFS> {
|
|
||||||
if !util::is_valid_name(new_name, MAX_REALMFS_NAME_LEN) {
|
|
||||||
bail!("Invalid realmfs name '{}'", new_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// during install the images have a different base directory
|
|
||||||
let mut new_path = self.path.clone();
|
|
||||||
new_path.pop();
|
|
||||||
new_path.push(format!("{}-realmfs.img", new_name));
|
|
||||||
|
|
||||||
if new_path.exists() {
|
|
||||||
bail!("RealmFS image for name {} already exists", new_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = format!("--reflink=auto {} {}", self.path.display(), new_path.display());
|
|
||||||
util::exec_cmdline("/usr/bin/cp", args)?;
|
|
||||||
|
|
||||||
let header = ImageHeader::new();
|
|
||||||
header.set_metainfo_bytes(&self.generate_fork_metainfo(new_name));
|
|
||||||
header.write_header_to(&new_path)?;
|
|
||||||
|
|
||||||
let realmfs = RealmFS::load_from_path(&new_path, new_name)?;
|
|
||||||
|
|
||||||
// forking unseals since presumably the image is being forked to modify it
|
|
||||||
realmfs.truncate_verity()?;
|
|
||||||
Ok(realmfs)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn generate_fork_metainfo(&self, name: &str) -> Vec<u8> {
|
|
||||||
let mut v = Vec::new();
|
|
||||||
writeln!(v, "image-type = \"realmfs\"").unwrap();
|
|
||||||
writeln!(v, "realmfs-name = \"{}\"", name).unwrap();
|
|
||||||
writeln!(v, "nblocks = {}", self.metainfo.nblocks()).unwrap();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove verity tree from image file by truncating file to the number of blocks in metainfo
|
|
||||||
fn truncate_verity(&self) -> Result<()> {
|
|
||||||
let verity_flag = self.header.has_flag(ImageHeader::FLAG_HASH_TREE);
|
|
||||||
if verity_flag {
|
|
||||||
self.header.clear_flag(ImageHeader::FLAG_HASH_TREE);
|
|
||||||
self.header.write_header_to(&self.path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let meta = self.path.metadata()?;
|
|
||||||
let expected = (self.metainfo.nblocks() + 1) * 4096;
|
|
||||||
let actual = meta.len() as usize;
|
|
||||||
|
|
||||||
if actual > expected {
|
|
||||||
if !verity_flag {
|
|
||||||
warn!("RealmFS length was greater than length indicated by metainfo but FLAG_HASH_TREE not set");
|
|
||||||
}
|
|
||||||
let f = fs::OpenOptions::new().write(true).open(&self.path)?;
|
|
||||||
f.set_len(expected as u64)?;
|
|
||||||
} else if actual < expected {
|
|
||||||
bail!("RealmFS image {} is shorter than length indicated by metainfo", self.path.display());
|
|
||||||
}
|
|
||||||
if verity_flag {
|
|
||||||
warn!("FLAG_HASH_TREE was set but RealmFS image file length matched metainfo length");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_sealed(&self) -> bool {
|
|
||||||
!self.metainfo.verity_root().is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_verity(&self) -> Result<()> {
|
|
||||||
self.truncate_verity()?;
|
|
||||||
verity::generate_image_hashtree(&self.path, &self.metainfo)?;
|
|
||||||
self.header.set_flag(ImageHeader::FLAG_HASH_TREE);
|
|
||||||
self.header.write_header_to(&self.path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_overlay(&self, basedir: &Path) -> Result<PathBuf> {
|
|
||||||
if !self.is_mounted() {
|
|
||||||
bail!("Cannot create overlay until realmfs is mounted");
|
|
||||||
}
|
|
||||||
let workdir = basedir.join("workdir");
|
|
||||||
let upperdir = basedir.join("upperdir");
|
|
||||||
let mountpoint = basedir.join("mountpoint");
|
|
||||||
fs::create_dir_all(&workdir)?;
|
|
||||||
fs::create_dir_all(&upperdir)?;
|
|
||||||
fs::create_dir_all(&mountpoint)?;
|
|
||||||
let args = format!("-t overlay realmfs-overlay -olowerdir={},upperdir={},workdir={} {}",
|
|
||||||
self.mountpoint.display(),
|
|
||||||
upperdir.display(),
|
|
||||||
workdir.display(),
|
|
||||||
mountpoint.display());
|
|
||||||
|
|
||||||
util::exec_cmdline("/usr/bin/mount", args)?;
|
|
||||||
Ok(mountpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mountpoint(&self) -> &Path {
|
|
||||||
&self.mountpoint
|
|
||||||
}
|
|
||||||
}
|
|
365
libcitadel/src/realmfs/activator.rs
Normal file
365
libcitadel/src/realmfs/activator.rs
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::{RealmFS, Result, ImageHeader, CommandLine, PublicKey, LoopDevice};
|
||||||
|
use crate::realmfs::mountpoint::Mountpoint;
|
||||||
|
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use crate::verity::Verity;
|
||||||
|
|
||||||
|
/// Holds the activation status for a RealmFS and provides a thread-safe
|
||||||
|
/// interface to it.
|
||||||
|
///
|
||||||
|
/// If `state` is `None` then the RealmFS is not currently activated.
|
||||||
|
///
|
||||||
|
pub struct ActivationState {
|
||||||
|
state: RwLock<Option<Arc<Activation>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActivationState {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let state = RwLock::new(None);
|
||||||
|
ActivationState { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load an unknown activation state for `realmfs` by examining
|
||||||
|
/// the state of the system to determine if the RealmFS is activated.
|
||||||
|
pub fn load(&self, realmfs: &RealmFS) {
|
||||||
|
let activation = if realmfs.is_sealed() {
|
||||||
|
let header = realmfs.header();
|
||||||
|
let activator = VerityActivator::new(realmfs, header);
|
||||||
|
activator.activation()
|
||||||
|
} else {
|
||||||
|
let activator = LoopActivator::new(realmfs);
|
||||||
|
activator.activation()
|
||||||
|
};
|
||||||
|
*self.state_mut() = activation.map(|a| Arc::new(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If currently activated return the corresponding `Activation` instance
|
||||||
|
/// otherwise return `None`
|
||||||
|
pub fn get(&self) -> Option<Arc<Activation>> {
|
||||||
|
self.state().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if currently activated.
|
||||||
|
pub fn is_activated(&self) -> bool {
|
||||||
|
self.state().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activate `realmfs` or if already activated return current `Activation`.
|
||||||
|
pub fn activate(&self, realmfs: &RealmFS) -> Result<Arc<Activation>> {
|
||||||
|
let header = realmfs.header();
|
||||||
|
let mut lock = self.state_mut();
|
||||||
|
if let Some(ref activation) = *lock {
|
||||||
|
return Ok(activation.clone());
|
||||||
|
} else {
|
||||||
|
let activation = self._activate(realmfs, header)?;
|
||||||
|
let activation = Arc::new(activation);
|
||||||
|
*lock = Some(activation.clone());
|
||||||
|
Ok(activation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _activate(&self, realmfs: &RealmFS, header: &ImageHeader) -> Result<Activation> {
|
||||||
|
if realmfs.is_sealed() {
|
||||||
|
let activator = VerityActivator::new(realmfs, header);
|
||||||
|
activator.activate()
|
||||||
|
} else {
|
||||||
|
let activator = LoopActivator::new(realmfs);
|
||||||
|
activator.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivate `Activation` only if not in use.
|
||||||
|
///
|
||||||
|
/// Returns `true` if state changes from activated to not-activated.
|
||||||
|
///
|
||||||
|
pub fn deactivate(&self, active_set: &HashSet<Mountpoint>) -> Result<bool> {
|
||||||
|
let mut lock = self.state_mut();
|
||||||
|
if let Some(ref activation) = *lock {
|
||||||
|
if activation.deactivate(active_set)? {
|
||||||
|
*lock = None;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if an `Activation` exists and is currently in-use by some `Realm`
|
||||||
|
pub fn is_in_use(&self, active_set: &HashSet<Mountpoint>) -> bool {
|
||||||
|
self.state()
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| a.in_use(active_set))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> RwLockReadGuard<Option<Arc<Activation>>> {
|
||||||
|
self.state.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&self) -> RwLockWriteGuard<Option<Arc<Activation>>>{
|
||||||
|
self.state.write().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a RealmFS in an activated state. The activation can be one of:
|
||||||
|
///
|
||||||
|
/// `Activation::Loop` if the RealmFS is unsealed
|
||||||
|
/// `Activation::Verity` if the RealmFS is sealed
|
||||||
|
///
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Activation {
|
||||||
|
///
|
||||||
|
/// A RealmFS in the unsealed state is activated by creating a /dev/loop
|
||||||
|
/// device and mounting it twice as both a read-only and read-write tree.
|
||||||
|
///
|
||||||
|
Loop {
|
||||||
|
ro_mountpoint: Mountpoint,
|
||||||
|
rw_mountpoint: Mountpoint,
|
||||||
|
device: LoopDevice,
|
||||||
|
},
|
||||||
|
///
|
||||||
|
/// A RealmFS in the sealed state is activated by configuring a dm-verity
|
||||||
|
/// device and mounting it.
|
||||||
|
/// `mountpoint` is the filesystem location at which the device is mounted.
|
||||||
|
/// `device` is a path to a device in /dev/mapper/
|
||||||
|
///
|
||||||
|
Verity {
|
||||||
|
mountpoint: Mountpoint,
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Activation {
|
||||||
|
|
||||||
|
fn new_loop(ro_mountpoint: Mountpoint, rw_mountpoint: Mountpoint, device: LoopDevice) -> Self {
|
||||||
|
Activation::Loop { ro_mountpoint, rw_mountpoint, device }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_verity(mountpoint: Mountpoint, device: String) -> Self {
|
||||||
|
Activation::Verity{ mountpoint, device }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an entry read from RealmFS:RUN_DIRECTORY into an `Activation` instance.
|
||||||
|
///
|
||||||
|
/// Return an `Activation` corresponding to `mountpoint` if valid activation exists.
|
||||||
|
///
|
||||||
|
pub fn for_mountpoint(mountpoint: &Mountpoint) -> Option<Self> {
|
||||||
|
if mountpoint.tag() == "rw" || mountpoint.tag() == "ro" {
|
||||||
|
LoopDevice::find_mounted_loop(mountpoint.path()).map(|loopdev| {
|
||||||
|
let (ro,rw) = Mountpoint::new_loop_pair(mountpoint.realmfs());
|
||||||
|
Activation::new_loop(ro, rw, loopdev)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let device = Verity::device_name_for_mountpoint(mountpoint);
|
||||||
|
if Path::new("/dev/mapper").join(&device).exists() {
|
||||||
|
Some(Activation::new_verity(mountpoint.clone(), device))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivate `Activation` only if not in use.
|
||||||
|
///
|
||||||
|
/// Returns `true` if state changes from activated to not-activated.
|
||||||
|
///
|
||||||
|
pub fn deactivate(&self, active_set: &HashSet<Mountpoint>) -> Result<bool> {
|
||||||
|
if !self.in_use(active_set) {
|
||||||
|
self._deactivate()?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _deactivate(&self) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Activation::Loop { ro_mountpoint, rw_mountpoint, device } => {
|
||||||
|
ro_mountpoint.deactivate()?;
|
||||||
|
rw_mountpoint.deactivate()?;
|
||||||
|
info!("Removing loop device {}", device);
|
||||||
|
device.detach()
|
||||||
|
},
|
||||||
|
Activation::Verity { mountpoint, device } => {
|
||||||
|
mountpoint.deactivate()?;
|
||||||
|
Verity::close_device(&device)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if `mp` is a `Mountpoint` belonging to this `Activation`.
|
||||||
|
pub fn is_mountpoint(&self, mp: &Mountpoint) -> bool {
|
||||||
|
match self {
|
||||||
|
Activation::Loop { ro_mountpoint, rw_mountpoint, ..} => {
|
||||||
|
mp == ro_mountpoint || mp == rw_mountpoint
|
||||||
|
},
|
||||||
|
Activation::Verity { mountpoint, .. } => {
|
||||||
|
mp == mountpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return read-only `Mountpoint` for this `Activation`
|
||||||
|
pub fn mountpoint(&self) -> &Mountpoint {
|
||||||
|
match self {
|
||||||
|
Activation::Loop { ro_mountpoint, ..} => &ro_mountpoint,
|
||||||
|
Activation::Verity { mountpoint, ..} => &mountpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return read-write `Mountpoint` if present for this `Activation` type.
|
||||||
|
pub fn mountpoint_rw(&self) -> Option<&Mountpoint> {
|
||||||
|
match self {
|
||||||
|
Activation::Loop { rw_mountpoint, ..} => Some(&rw_mountpoint),
|
||||||
|
Activation::Verity { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn device(&self) -> &str{
|
||||||
|
match self {
|
||||||
|
Activation::Loop { device, ..} => device.device_str(),
|
||||||
|
Activation::Verity { device, ..} => &device,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if `Activation` is currently in-use by some `Realm`
|
||||||
|
///
|
||||||
|
/// `active_set` is a set of mountpoints needed to determine if an activation is
|
||||||
|
/// in use. This set is obtained by calling `active_mountpoints()` on a `RealmManager`
|
||||||
|
/// instance.
|
||||||
|
///
|
||||||
|
pub fn in_use(&self, active_set: &HashSet<Mountpoint>) -> bool {
|
||||||
|
match self {
|
||||||
|
Activation::Loop {ro_mountpoint: ro, rw_mountpoint: rw, ..} => {
|
||||||
|
active_set.contains(ro) || active_set.contains(rw)
|
||||||
|
},
|
||||||
|
Activation::Verity { mountpoint, ..} => {
|
||||||
|
active_set.contains(mountpoint)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct VerityActivator<'a> {
|
||||||
|
realmfs: &'a RealmFS,
|
||||||
|
header: &'a ImageHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl <'a> VerityActivator <'a> {
|
||||||
|
fn new(realmfs: &'a RealmFS, header: &'a ImageHeader) -> Self {
|
||||||
|
VerityActivator { realmfs, header }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if `self.realmfs` is already activated by searching for verity mountpoint and
|
||||||
|
// device name. If found return an `Activation::Verity`
|
||||||
|
fn activation(&self) -> Option<Activation> {
|
||||||
|
let mountpoint = self.mountpoint();
|
||||||
|
if mountpoint.exists() {
|
||||||
|
let devname = Verity::device_name(&self.realmfs.metainfo());
|
||||||
|
Some(Activation::new_verity(self.mountpoint(), devname))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform a verity activation of `self.realmfs` and return an `Activation::Verity`
|
||||||
|
fn activate(&self) -> Result<Activation> {
|
||||||
|
info!("Starting verity activation for {}", self.realmfs.name());
|
||||||
|
let mountpoint = self.mountpoint();
|
||||||
|
if !mountpoint.exists() {
|
||||||
|
mountpoint.create_dir()?;
|
||||||
|
}
|
||||||
|
let device_name = self.setup_verity_device()?;
|
||||||
|
info!("verity device created..");
|
||||||
|
cmd!("/usr/bin/mount", "-oro /dev/mapper/{} {}", device_name, mountpoint)?;
|
||||||
|
|
||||||
|
Ok(Activation::new_verity(mountpoint, device_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mountpoint(&self) -> Mountpoint {
|
||||||
|
Mountpoint::new(self.realmfs.name(), &self.realmfs.metainfo().verity_tag())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_verity_device(&self) -> Result<String> {
|
||||||
|
if !CommandLine::nosignatures() {
|
||||||
|
self.verify_signature()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.header.has_flag(ImageHeader::FLAG_HASH_TREE) {
|
||||||
|
self.generate_verity()?;
|
||||||
|
}
|
||||||
|
Verity::new(self.realmfs.path()).setup(&self.header.metainfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_verity(&self) -> Result<()> {
|
||||||
|
info!("Generating verity hash tree");
|
||||||
|
Verity::new(self.realmfs.path()).generate_image_hashtree(&self.header.metainfo())?;
|
||||||
|
info!("Writing header...");
|
||||||
|
self.header.set_flag(ImageHeader::FLAG_HASH_TREE);
|
||||||
|
self.header.write_header_to(self.realmfs.path())?;
|
||||||
|
info!("Done generating verity hash tree");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_signature(&self) -> Result<()> {
|
||||||
|
let pubkey = self.public_key()?;
|
||||||
|
if !self.realmfs.header().verify_signature(pubkey) {
|
||||||
|
bail!("header signature verification failed on realmfs image '{}'", self.realmfs.name());
|
||||||
|
}
|
||||||
|
info!("header signature verified on realmfs image '{}'", self.realmfs.name());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_key(&self) -> Result<PublicKey> {
|
||||||
|
let pubkey = if self.realmfs.metainfo().channel() == RealmFS::USER_KEYNAME {
|
||||||
|
self.realmfs.sealing_keys()?.public_key()
|
||||||
|
} else {
|
||||||
|
match self.realmfs.header().public_key()? {
|
||||||
|
Some(pubkey) => pubkey,
|
||||||
|
None => bail!("No public key available for channel {}", self.realmfs.metainfo().channel()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoopActivator<'a> {
|
||||||
|
realmfs: &'a RealmFS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <'a> LoopActivator<'a> {
|
||||||
|
fn new(realmfs: &'a RealmFS) -> Self {
|
||||||
|
LoopActivator{ realmfs }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if `self.realmfs` is presently activated by searching for mountpoints. If
|
||||||
|
// loop activation mountpoints are present return an `Activation::Loop`
|
||||||
|
fn activation(&self) -> Option<Activation> {
|
||||||
|
let (ro,rw) = Mountpoint::new_loop_pair(self.realmfs.name());
|
||||||
|
if ro.exists() && rw.exists() {
|
||||||
|
Activation::for_mountpoint(&ro)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform a loop activation of `self.realmfs` and return an `Activation::Loop`
|
||||||
|
fn activate(&self) -> Result<Activation> {
|
||||||
|
|
||||||
|
let (ro,rw) = Mountpoint::new_loop_pair(self.realmfs.name());
|
||||||
|
ro.create_dir()?;
|
||||||
|
rw.create_dir()?;
|
||||||
|
|
||||||
|
let loopdev = LoopDevice::create(self.realmfs.path(), Some(4096), false)?;
|
||||||
|
|
||||||
|
loopdev.mount_pair(rw.path(), ro.path())?;
|
||||||
|
|
||||||
|
Ok(Activation::new_loop(ro, rw, loopdev))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
10
libcitadel/src/realmfs/mod.rs
Normal file
10
libcitadel/src/realmfs/mod.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
pub(crate) mod resizer;
|
||||||
|
mod activator;
|
||||||
|
mod mountpoint;
|
||||||
|
mod update;
|
||||||
|
pub(crate) mod realmfs_set;
|
||||||
|
mod realmfs;
|
||||||
|
|
||||||
|
pub use self::realmfs::RealmFS;
|
||||||
|
pub use self::mountpoint::Mountpoint;
|
||||||
|
pub use self::activator::Activation;
|
129
libcitadel/src/realmfs/mountpoint.rs
Normal file
129
libcitadel/src/realmfs/mountpoint.rs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
use std::fs::{self, DirEntry};
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
|
||||||
|
use crate::{Result, RealmFS};
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
|
||||||
|
/// A RealmFS activation mountpoint
|
||||||
|
#[derive(Clone,Eq,PartialEq,Hash,Debug)]
|
||||||
|
pub struct Mountpoint(PathBuf);
|
||||||
|
|
||||||
|
impl Mountpoint {
|
||||||
|
const UMOUNT: &'static str = "/usr/bin/umount";
|
||||||
|
|
||||||
|
/// Read `RealmFS::RUN_DIRECTORY` to collect all current mountpoints
|
||||||
|
/// and return them.
|
||||||
|
pub fn all_mountpoints() -> Result<Vec<Mountpoint>> {
|
||||||
|
let all = fs::read_dir(RealmFS::RUN_DIRECTORY)?
|
||||||
|
.flat_map(|e| e.ok())
|
||||||
|
.map(Into::into)
|
||||||
|
.filter(Mountpoint::is_valid)
|
||||||
|
.collect();
|
||||||
|
Ok(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a read-only/read-write mountpoint pair.
|
||||||
|
pub fn new_loop_pair(realmfs: &str) -> (Self,Self) {
|
||||||
|
let ro = Self::new(realmfs, "ro");
|
||||||
|
let rw = Self::new(realmfs, "rw");
|
||||||
|
(ro, rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a new `Mountpoint` from the provided realmfs `name` and `tag`.
|
||||||
|
///
|
||||||
|
/// The directory name of the mountpoint will have the structure:
|
||||||
|
///
|
||||||
|
/// realmfs-$name-$tag.mountpoint
|
||||||
|
///
|
||||||
|
pub fn new(name: &str, tag: &str) -> Self {
|
||||||
|
let filename = format!("realmfs-{}-{}.mountpoint", name, tag);
|
||||||
|
Mountpoint(Path::new(RealmFS::RUN_DIRECTORY).join(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&self) -> bool {
|
||||||
|
self.0.exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_dir(&self) -> Result<()> {
|
||||||
|
fs::create_dir_all(self.path())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivate this mountpoint by unmounting it and removing the directory.
|
||||||
|
pub fn deactivate(&self) -> Result<()> {
|
||||||
|
if self.exists() {
|
||||||
|
info!("Unmounting {} and removing directory", self);
|
||||||
|
cmd!(Self::UMOUNT, "{}", self)?;
|
||||||
|
fs::remove_dir(self.path())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full `&Path` of mountpoint.
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
self.0.as_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of RealmFS extracted from structure of directory filename.
|
||||||
|
pub fn realmfs(&self) -> &str {
|
||||||
|
self.field(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tag field extracted from structure of directory filename.
|
||||||
|
pub fn tag(&self) -> &str {
|
||||||
|
self.field(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field(&self, n: usize) -> &str {
|
||||||
|
Self::filename_fields(self.path())
|
||||||
|
.and_then(|mut fields| fields.nth(n))
|
||||||
|
.expect(&format!("Failed to access field {} of mountpoint {}", n, self))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this instance is a `&Path` in `RealmFS::RUN_DIRECTORY` and
|
||||||
|
/// the filename has the expected structure.
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.path().starts_with(RealmFS::RUN_DIRECTORY) && self.has_valid_extention() &&
|
||||||
|
Self::filename_fields(self.path()).map(|it| it.count() == 3).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_valid_extention(&self) -> bool {
|
||||||
|
self.path().extension().map(|e| e == "mountpoint").unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filename_fields(path: &Path) -> Option<impl Iterator<Item=&str>> {
|
||||||
|
Self::filename(path).map(|name| name.split("-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filename(path: &Path) -> Option<&str> {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.map(|s| s.trim_end_matches(".mountpoint"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Mountpoint {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0.to_str().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Path> for Mountpoint {
|
||||||
|
fn from(p: &Path) -> Self {
|
||||||
|
Mountpoint(p.to_path_buf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for Mountpoint {
|
||||||
|
fn from(p: PathBuf) -> Self {
|
||||||
|
Mountpoint(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DirEntry> for Mountpoint {
|
||||||
|
fn from(entry: DirEntry) -> Self {
|
||||||
|
Mountpoint(entry.path())
|
||||||
|
}
|
||||||
|
}
|
604
libcitadel/src/realmfs/realmfs.rs
Normal file
604
libcitadel/src/realmfs/realmfs.rs
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
use std::path::{Path,PathBuf};
|
||||||
|
|
||||||
|
use sodiumoxide::randombytes::randombytes;
|
||||||
|
use hex;
|
||||||
|
|
||||||
|
use crate::{CommandLine, ImageHeader, MetaInfo, Result, KeyRing, KeyPair, Signature, util, RealmManager};
|
||||||
|
|
||||||
|
use super::resizer::{ImageResizer,ResizeSize};
|
||||||
|
use super::update::Update;
|
||||||
|
use crate::realmfs::resizer::Superblock;
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use super::activator::Activation;
|
||||||
|
use super::mountpoint::Mountpoint;
|
||||||
|
use crate::realmfs::activator::ActivationState;
|
||||||
|
use crate::verity::Verity;
|
||||||
|
|
||||||
|
// Maximum length of a RealmFS name
|
||||||
|
const MAX_REALMFS_NAME_LEN: usize = 40;
|
||||||
|
|
||||||
|
// The maximum number of backup copies the rotate() method will create
|
||||||
|
const NUM_BACKUPS: usize = 2;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Representation of a RealmFS disk image file.
|
||||||
|
///
|
||||||
|
/// RealmFS images contain the root filesystem for one or more realms. A single RealmFS
|
||||||
|
/// image may be shared by multiple running realm instances.
|
||||||
|
///
|
||||||
|
/// A RealmFS image can be in a state where it includes all the metadata needed to mount the
|
||||||
|
/// image with dm-verity to securely enforce read-only access to the image. An image in this state
|
||||||
|
/// is called 'sealed' and it may be signed either with regular channel keys or with a special
|
||||||
|
/// key generated upon installation and stored in the kernel keyring.
|
||||||
|
///
|
||||||
|
/// An image which is not sealed is called 'unsealed'. In this state, the image can be mounted into
|
||||||
|
/// a realm with write access, but only one realm can write to the image. All other realms
|
||||||
|
/// use read-only views of the image.
|
||||||
|
///
|
||||||
|
/// RealmFS images are normally stored in the directory `BASE_PATH` (/storage/realms/realmfs-images),
|
||||||
|
/// and images stored in this directory can be loaded by name rather than needing the exact path
|
||||||
|
/// to the image.
|
||||||
|
///
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RealmFS {
|
||||||
|
// RealmFS name
|
||||||
|
name: Arc<String>,
|
||||||
|
// path to RealmFS image file
|
||||||
|
path: Arc<PathBuf>,
|
||||||
|
// current RealmFS image file header
|
||||||
|
header: Arc<ImageHeader>,
|
||||||
|
|
||||||
|
activation_state: Arc<ActivationState>,
|
||||||
|
|
||||||
|
manager: Weak<RealmManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmFS {
|
||||||
|
// Directory where RealmFS images are stored
|
||||||
|
pub const BASE_PATH: &'static str = "/storage/realms/realmfs-images";
|
||||||
|
|
||||||
|
// Directory where RealmFS mountpoints are created
|
||||||
|
pub const RUN_DIRECTORY: &'static str = "/run/citadel/realmfs";
|
||||||
|
|
||||||
|
// Name used to retrieve key by 'description' from kernel key storage
|
||||||
|
pub const USER_KEYNAME: &'static str = "realmfs-user";
|
||||||
|
|
||||||
|
/// Locate a RealmFS image by name in the default location using the standard name convention
|
||||||
|
pub fn load_by_name(name: &str) -> Result<RealmFS> {
|
||||||
|
RealmFS::validate_name(name)?;
|
||||||
|
let path = RealmFS::image_path(name);
|
||||||
|
if !path.exists() {
|
||||||
|
bail!("No image found at {}", path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
RealmFS::load_from_path(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load RealmFS image from an exact path.
|
||||||
|
pub fn load_from_path(path: impl AsRef<Path>) -> Result<RealmFS> {
|
||||||
|
Self::_load_from_path(path.as_ref(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _load_from_path(path: &Path, load_activation: bool) -> Result<RealmFS> {
|
||||||
|
let path = Arc::new(path.to_owned());
|
||||||
|
let header = RealmFS::load_realmfs_header(&path)?;
|
||||||
|
let name = header.metainfo().realmfs_name()
|
||||||
|
.expect("RealmFS does not have a name")
|
||||||
|
.to_owned();
|
||||||
|
let name = Arc::new(name);
|
||||||
|
let header = Arc::new(header);
|
||||||
|
let manager = Weak::new();
|
||||||
|
|
||||||
|
let activation_state = Arc::new(ActivationState::new());
|
||||||
|
|
||||||
|
let realmfs = RealmFS {
|
||||||
|
name, path, header, activation_state, manager
|
||||||
|
};
|
||||||
|
|
||||||
|
if load_activation {
|
||||||
|
realmfs.load_activation();
|
||||||
|
}
|
||||||
|
Ok(realmfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_manager(&mut self, manager: Arc<RealmManager>) {
|
||||||
|
self.manager = Arc::downgrade(&manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_activation(&self) {
|
||||||
|
self.activation_state.load(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manager(&self) -> Arc<RealmManager> {
|
||||||
|
if let Some(manager) = self.manager.upgrade() {
|
||||||
|
manager
|
||||||
|
} else {
|
||||||
|
panic!("No manager set on realmfs {}", self.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_manager<F>(&self, f: F)
|
||||||
|
where F: FnOnce(Arc<RealmManager>)
|
||||||
|
{
|
||||||
|
if let Some(manager) = self.manager.upgrade() {
|
||||||
|
f(manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid_realmfs_image(path: impl AsRef<Path>) -> bool {
|
||||||
|
RealmFS::load_realmfs_header(path.as_ref()).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_realmfs_header(path: &Path) -> Result<ImageHeader> {
|
||||||
|
let header = ImageHeader::from_file(path)?;
|
||||||
|
if !header.is_magic_valid() {
|
||||||
|
bail!("Image file {} does not have a valid header", path.display());
|
||||||
|
}
|
||||||
|
let metainfo = header.metainfo();
|
||||||
|
if metainfo.image_type() != "realmfs" {
|
||||||
|
bail!("Image file {} is not a realmfs image", path.display());
|
||||||
|
}
|
||||||
|
match metainfo.realmfs_name() {
|
||||||
|
Some(name) => RealmFS::validate_name(name)?,
|
||||||
|
None => bail!("RealmFS image file {} does not have a 'realmfs-name' field", path.display()),
|
||||||
|
};
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return an Error result if name is not valid.
|
||||||
|
fn validate_name(name: &str) -> Result<()> {
|
||||||
|
if RealmFS::is_valid_name(name) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format_err!("Invalid realm name '{}'", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if `name` is a valid name for a RealmFS.
|
||||||
|
///
|
||||||
|
/// Valid names:
|
||||||
|
/// * Are 40 characters or less in length
|
||||||
|
/// * Have an alphabetic ascii letter as first character
|
||||||
|
/// * Contain only alphanumeric ascii characters or '-' (dash)
|
||||||
|
///
|
||||||
|
pub fn is_valid_name(name: &str) -> bool {
|
||||||
|
util::is_valid_name(name, MAX_REALMFS_NAME_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn named_image_exists(name: &str) -> bool {
|
||||||
|
if !util::is_valid_name(name, MAX_REALMFS_NAME_LEN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RealmFS::is_valid_realmfs_image(RealmFS::image_path(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn image_path(name: &str) -> PathBuf {
|
||||||
|
Path::new(RealmFS::BASE_PATH).join(format!("{}-realmfs.img", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the `Path` to this RealmFS image file.
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
self.path.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a new `PathBuf` based on the path of the current image by appending
|
||||||
|
/// the string `ext` as an extension to the filename. If the current filename
|
||||||
|
/// ends with '.img' then the specified extension is appended to this as '.img.ext'
|
||||||
|
/// otherwise it replaces any existing extension.
|
||||||
|
fn path_with_extension(&self, ext: &str) -> PathBuf {
|
||||||
|
if self.path.extension() == Some(OsStr::new("img")) {
|
||||||
|
self.path.with_extension(format!("img.{}", ext))
|
||||||
|
} else {
|
||||||
|
self.path.with_extension(ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a new `PathBuf` based on the path of the current image by replacing
|
||||||
|
/// the image filename with the specified name.
|
||||||
|
pub fn path_with_filename(&self, filename: impl AsRef<str>) -> PathBuf {
|
||||||
|
let mut path = (*self.path).clone();
|
||||||
|
path.pop();
|
||||||
|
path.push(filename.as_ref());
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the 'realmfs-name' metainfo field of this image.
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notes(&self) -> Option<String> {
|
||||||
|
let path = self.path_with_extension("notes");
|
||||||
|
if path.exists() {
|
||||||
|
return fs::read_to_string(path).ok();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_notes(&self, notes: impl AsRef<str>) -> Result<()> {
|
||||||
|
let path = self.path_with_extension("notes");
|
||||||
|
let notes = notes.as_ref();
|
||||||
|
if path.exists() && notes.is_empty() {
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
} else {
|
||||||
|
fs::write(path, notes)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `MetaInfo` from image header of this RealmFS.
|
||||||
|
pub fn metainfo(&self) -> Arc<MetaInfo> {
|
||||||
|
self.header().metainfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(&self) -> &ImageHeader {
|
||||||
|
match self.header.reload_if_stale(self.path()) {
|
||||||
|
Ok(true) => self.load_activation(),
|
||||||
|
Err(e) => warn!("error reloading stale image header: {}", e),
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_user_realmfs(&self) -> bool {
|
||||||
|
!self.is_sealed() || self.metainfo().channel() == Self::USER_KEYNAME
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this RealmFS is 'activated'.
|
||||||
|
///
|
||||||
|
/// A RealmFS is activated if the device for the image has been created and mounted.
|
||||||
|
/// Sealed images create dm-verity devices in /dev/mapper and unsealed images create
|
||||||
|
/// /dev/loop devices.
|
||||||
|
pub fn is_activated(&self) -> bool {
|
||||||
|
self.activation_state.is_activated()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If this RealmFS is activated return `Activation` instance
|
||||||
|
pub fn activation(&self) -> Option<Arc<Activation>> {
|
||||||
|
self.activation_state.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if RealmFS is activated and some Realm is currently using
|
||||||
|
/// it. A RealmFS which is in use cannot be deactivated.
|
||||||
|
pub fn is_in_use(&self) -> bool {
|
||||||
|
let active = self.manager().active_mountpoints();
|
||||||
|
self.activation_state.is_in_use(&active)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activate this RealmFS image if not yet activated.
|
||||||
|
pub fn activate(&self) -> Result<Arc<Activation>> {
|
||||||
|
if CommandLine::sealed() && !self.is_sealed() && !self.is_update_copy() {
|
||||||
|
bail!("Cannot activate unsealed realmfs '{}' because citadel.sealed is enabled", self.name());
|
||||||
|
}
|
||||||
|
self.activation_state.activate(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivate this RealmFS image if currently activated, but not in use.
|
||||||
|
/// Return `true` if deactivation occurs.
|
||||||
|
pub fn deactivate(&self) -> Result<bool> {
|
||||||
|
let active = self.manager().active_mountpoints();
|
||||||
|
self.activation_state.deactivate(&active)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fork(&self, new_name: &str) -> Result<RealmFS> {
|
||||||
|
self._fork(new_name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an unsealed copy of this RealmFS image with a new image name.
|
||||||
|
///
|
||||||
|
pub fn fork_unsealed(&self, new_name: &str) -> Result<RealmFS> {
|
||||||
|
RealmFS::validate_name(new_name)?;
|
||||||
|
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
|
||||||
|
|
||||||
|
let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name));
|
||||||
|
|
||||||
|
if new_path.exists() {
|
||||||
|
bail!("RealmFS image for name {} already exists", new_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_realmfs = self.copy_image(&new_path, new_name, false)?;
|
||||||
|
self.with_manager(|m| m.realmfs_added(&new_realmfs));
|
||||||
|
Ok(new_realmfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _fork(&self, new_name: &str, sealed_fork: bool) -> Result<RealmFS> {
|
||||||
|
RealmFS::validate_name(new_name)?;
|
||||||
|
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
|
||||||
|
let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name));
|
||||||
|
if new_path.exists() {
|
||||||
|
bail!("RealmFS image for name {} already exists", new_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_realmfs = self.copy_image(&new_path, new_name, sealed_fork)?;
|
||||||
|
|
||||||
|
self.with_manager(|m| m.realmfs_added(&new_realmfs));
|
||||||
|
Ok(new_realmfs)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&self) -> Update {
|
||||||
|
Update::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_update_copy(&self) -> bool {
|
||||||
|
self.path().extension() == Some(OsStr::new("update"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_copy(&self) -> Result<RealmFS> {
|
||||||
|
let path = self.path_with_extension("update");
|
||||||
|
let name = self.name().to_string() + "-update";
|
||||||
|
self.copy_image(&path, &name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_image(&self, path: &Path, name: &str, sealed_copy: bool) -> Result<RealmFS> {
|
||||||
|
if path.exists() {
|
||||||
|
bail!("Cannot create sealed copy because target path '{}' already exists", path.display());
|
||||||
|
}
|
||||||
|
cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), path.display())?;
|
||||||
|
let mut realmfs = RealmFS::_load_from_path(path, false)?;
|
||||||
|
self.with_manager(|m| realmfs.set_manager(m));
|
||||||
|
realmfs.name = Arc::new(name.to_owned());
|
||||||
|
|
||||||
|
let result = if sealed_copy {
|
||||||
|
realmfs.write_sealed_copy_header()
|
||||||
|
} else {
|
||||||
|
realmfs.unseal()
|
||||||
|
};
|
||||||
|
|
||||||
|
result.map_err(|e|
|
||||||
|
if let Err(e) = fs::remove_file(path) {
|
||||||
|
format_err!("failed to remove {} after realmfs fork/copy failed with: {}", path.display(), e)
|
||||||
|
} else { e })?;
|
||||||
|
|
||||||
|
Ok(realmfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_sealed_copy_header(&self) -> Result<()> {
|
||||||
|
let keys = match self.sealing_keys() {
|
||||||
|
Ok(keys) => keys,
|
||||||
|
Err(err) => bail!("Cannot seal realmfs image, no sealing keys available: {}", err),
|
||||||
|
};
|
||||||
|
let metainfo = self.metainfo();
|
||||||
|
let metainfo_bytes = self.generate_sealed_metainfo(self.name(), metainfo.verity_salt(), metainfo.verity_root());
|
||||||
|
let sig = keys.sign(&metainfo_bytes);
|
||||||
|
self.write_new_metainfo(metainfo_bytes, Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to unsealed RealmFS image by removing dm-verity metadata and hash tree
|
||||||
|
pub fn unseal(&self) -> Result<()> {
|
||||||
|
let bytes = RealmFS::generate_unsealed_metainfo(self.name(), self.metainfo().nblocks(), None);
|
||||||
|
self.write_new_metainfo(bytes, None)?;
|
||||||
|
if self.has_verity_tree() {
|
||||||
|
self.truncate_verity()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_owner_realm(&self, owner_realm: &str) -> Result<()> {
|
||||||
|
if self.is_sealed() {
|
||||||
|
bail!("Cannot set owner realm because RealmFS is sealed");
|
||||||
|
}
|
||||||
|
if let Some(activation) = self.activation() {
|
||||||
|
let rw_mountpoint = activation.mountpoint_rw()
|
||||||
|
.ok_or(format_err!("unsealed activation expected"))?;
|
||||||
|
if self.manager().active_mountpoints().contains(rw_mountpoint) {
|
||||||
|
bail!("Cannot set owner realm because RW mountpoint is in use (by current owner?)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nblocks = self.metainfo().nblocks();
|
||||||
|
self.update_unsealed_metainfo(self.name(), nblocks, Some(owner_realm.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_unsealed_metainfo(&self, name: &str, nblocks: usize, owner_realm: Option<String>) -> Result<()> {
|
||||||
|
if self.is_sealed() {
|
||||||
|
bail!("Cannot update metainfo on sealed realmfs image");
|
||||||
|
}
|
||||||
|
let metainfo_bytes = RealmFS::generate_unsealed_metainfo(name, nblocks, owner_realm);
|
||||||
|
self.write_new_metainfo(metainfo_bytes, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_new_metainfo(&self, bytes: Vec<u8>, sig: Option<Signature>) -> Result<()> {
|
||||||
|
self.header.set_metainfo_bytes(&bytes)?;
|
||||||
|
if let Some(sig) = sig {
|
||||||
|
self.header.set_signature(sig.to_bytes())?;
|
||||||
|
}
|
||||||
|
self.header.write_header_to(self.path())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_unsealed_metainfo(name: &str, nblocks: usize, owner_realm: Option<String>) -> Vec<u8> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
writeln!(v, "image-type = \"realmfs\"").unwrap();
|
||||||
|
writeln!(v, "realmfs-name = \"{}\"", name).unwrap();
|
||||||
|
writeln!(v, "nblocks = {}", nblocks).unwrap();
|
||||||
|
if let Some(owner) = owner_realm {
|
||||||
|
writeln!(v, "realmfs-owner = \"{}\"", owner).unwrap();
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_sealed_metainfo(&self, name: &str, verity_salt: &str, verity_root: &str) -> Vec<u8> {
|
||||||
|
let mut v = RealmFS::generate_unsealed_metainfo(name, self.metainfo().nblocks(), None);
|
||||||
|
writeln!(v, "channel = \"{}\"", Self::USER_KEYNAME).unwrap();
|
||||||
|
writeln!(v, "verity-salt = \"{}\"", verity_salt).unwrap();
|
||||||
|
writeln!(v, "verity-root = \"{}\"", verity_root).unwrap();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove verity tree from image file by truncating file to the number of blocks in metainfo
|
||||||
|
fn truncate_verity(&self) -> Result<()> {
|
||||||
|
let file_nblocks = self.file_nblocks()?;
|
||||||
|
let expected = self.metainfo_nblocks();
|
||||||
|
|
||||||
|
if self.has_verity_tree() {
|
||||||
|
let f = fs::OpenOptions::new().write(true).open(self.path())?;
|
||||||
|
let lock = self.header();
|
||||||
|
lock.clear_flag(ImageHeader::FLAG_HASH_TREE);
|
||||||
|
lock.write_header(&f)?;
|
||||||
|
debug!("Removing appended dm-verity hash tree by truncating image from {} blocks to {} blocks", file_nblocks, expected);
|
||||||
|
f.set_len((expected * 4096) as u64)?;
|
||||||
|
} else if file_nblocks > expected {
|
||||||
|
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the length in blocks of the actual image file on disk
|
||||||
|
fn file_nblocks(&self) -> Result<usize> {
|
||||||
|
let meta = self.path.metadata()?;
|
||||||
|
let len = meta.len() as usize;
|
||||||
|
if len % 4096 != 0 {
|
||||||
|
bail!("realmfs image file '{}' has size which is not a multiple of block size", self.path.display());
|
||||||
|
}
|
||||||
|
let nblocks = len / 4096;
|
||||||
|
if nblocks < (self.metainfo().nblocks() + 1) {
|
||||||
|
bail!("realmfs image file '{}' has shorter length than nblocks field of image header", self.path.display());
|
||||||
|
}
|
||||||
|
Ok(nblocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_verity_tree(&self) -> bool {
|
||||||
|
self.header().has_flag(ImageHeader::FLAG_HASH_TREE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_sealed(&self) -> bool {
|
||||||
|
!self.metainfo().verity_root().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seal(&self, new_name: Option<&str>) -> Result<()> {
|
||||||
|
if self.is_sealed() {
|
||||||
|
info!("RealmFS {} is already sealed. Doing nothing.", self.name());
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys = match self.sealing_keys() {
|
||||||
|
Ok(keys) => keys,
|
||||||
|
Err(err) => bail!("Cannot seal realmfs image, no sealing keys available: {}", err),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.is_activated() {
|
||||||
|
bail!("Cannot seal RealmFS because it is currently activated");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.has_verity_tree() {
|
||||||
|
warn!("unsealed RealmFS already has a verity hash tree, removing it");
|
||||||
|
self.truncate_verity()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp = self.path_with_extension("sealing");
|
||||||
|
if tmp.exists() {
|
||||||
|
info!("Temporary copy of realmfs image {} already exists, removing it.", self.name());
|
||||||
|
fs::remove_file(&tmp)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Creating temporary copy of realmfs image");
|
||||||
|
cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), tmp.display())?;
|
||||||
|
|
||||||
|
let name = new_name.unwrap_or_else(|| self.name());
|
||||||
|
|
||||||
|
let mut realmfs = RealmFS::load_from_path(&tmp)?;
|
||||||
|
realmfs.set_manager(self.manager());
|
||||||
|
|
||||||
|
let finish = || {
|
||||||
|
realmfs.generate_sealing_verity(&keys, name)?;
|
||||||
|
verbose!("Rename {} to {}", self.path().display(), self.path_with_extension("old").display());
|
||||||
|
fs::rename(self.path(), self.path_with_extension("old"))?;
|
||||||
|
verbose!("Rename {} to {}", realmfs.path().display(), self.path().display());
|
||||||
|
fs::rename(realmfs.path(), self.path())?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = finish() {
|
||||||
|
if tmp.exists() {
|
||||||
|
let _ = fs::remove_file(tmp);
|
||||||
|
}
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_sealing_verity(&self, keys: &KeyPair, name: &str) -> Result<()> {
|
||||||
|
info!("Generating verity hash tree for sealed realmfs ({})", self.path().display());
|
||||||
|
let salt = hex::encode(randombytes(32));
|
||||||
|
let output = Verity::new(self.path()).generate_image_hashtree_with_salt(&self.metainfo(), &salt)?;
|
||||||
|
let root_hash = output.root_hash()
|
||||||
|
.ok_or(format_err!("no root hash returned from verity format operation"))?;
|
||||||
|
info!("root hash is {}", output.root_hash().unwrap());
|
||||||
|
|
||||||
|
info!("Signing new image with user realmfs keys");
|
||||||
|
let metainfo_bytes = self.generate_sealed_metainfo(name, &salt, &root_hash);
|
||||||
|
let sig = keys.sign(&metainfo_bytes);
|
||||||
|
|
||||||
|
self.header().set_flag(ImageHeader::FLAG_HASH_TREE);
|
||||||
|
self.write_new_metainfo(metainfo_bytes, Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_sealing_keys(&self) -> bool {
|
||||||
|
self.sealing_keys().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sealing_keys(&self) -> Result<KeyPair> {
|
||||||
|
KeyRing::get_kernel_keypair(Self::USER_KEYNAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate(&self, new_file: &Path) -> Result<()> {
|
||||||
|
let backup = |n: usize| Path::new(RealmFS::BASE_PATH).join(format!("{}-realmfs.img.{}", self.name(), n));
|
||||||
|
|
||||||
|
for i in (1..NUM_BACKUPS).rev() {
|
||||||
|
let from = backup(i - 1);
|
||||||
|
if from.exists() {
|
||||||
|
fs::rename(from, backup(i))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs::rename(self.path(), backup(0))?;
|
||||||
|
fs::rename(new_file, self.path())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auto_resize_size(&self) -> Option<ResizeSize> {
|
||||||
|
ImageResizer::auto_resize_size(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> {
|
||||||
|
info!("Resizing to {} blocks", size.nblocks());
|
||||||
|
ImageResizer::new(self).grow_to(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
|
||||||
|
ImageResizer::new(self).grow_by(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_size_blocks(&self) -> Result<usize> {
|
||||||
|
let sb = Superblock::load(self.path(), 4096)?;
|
||||||
|
Ok(sb.free_block_count() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocated_size_blocks(&self) -> Result<usize> {
|
||||||
|
let meta = self.path().metadata()?;
|
||||||
|
Ok(meta.blocks() as usize / 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of image file in blocks (including header block) based on metainfo `nblocks` field.
|
||||||
|
pub fn metainfo_nblocks(&self) -> usize {
|
||||||
|
self.metainfo().nblocks() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if mountpoint belongs to current `Activation` state of
|
||||||
|
/// this `RealmFS`
|
||||||
|
pub fn release_mountpoint(&self, mountpoint: &Mountpoint) -> bool {
|
||||||
|
let is_ours = self.activation()
|
||||||
|
.map(|a| a.is_mountpoint(mountpoint))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if is_ours {
|
||||||
|
if let Err(e) = self.deactivate() {
|
||||||
|
warn!("error deactivating mountpoint: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_ours
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
70
libcitadel/src/realmfs/realmfs_set.rs
Normal file
70
libcitadel/src/realmfs/realmfs_set.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::{RealmFS, RealmManager, Result};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub struct RealmFSSet {
|
||||||
|
realmfs_map: HashMap<String, RealmFS>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmFSSet {
|
||||||
|
|
||||||
|
pub fn load() -> Result<Self> {
|
||||||
|
let mut realmfs_map = HashMap::new();
|
||||||
|
for realmfs in Self::load_all()? {
|
||||||
|
let name = realmfs.name().to_string();
|
||||||
|
realmfs_map.insert(name, realmfs);
|
||||||
|
}
|
||||||
|
Ok( RealmFSSet { realmfs_map })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_all() -> Result<Vec<RealmFS>> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for entry in fs::read_dir(RealmFS::BASE_PATH)? {
|
||||||
|
if let Some(realmfs) = Self::entry_to_realmfs(entry?) {
|
||||||
|
v.push(realmfs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_to_realmfs(entry: fs::DirEntry) -> Option<RealmFS> {
|
||||||
|
if let Ok(filename) = entry.file_name().into_string() {
|
||||||
|
if filename.ends_with("-realmfs.img") {
|
||||||
|
let name = filename.trim_end_matches("-realmfs.img");
|
||||||
|
if RealmFS::is_valid_name(name) && RealmFS::named_image_exists(name) {
|
||||||
|
return RealmFS::load_by_name(name).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_manager(&mut self, manager: Arc<RealmManager>) {
|
||||||
|
self.realmfs_map.iter_mut().for_each(|(_,v)| v.set_manager(manager.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn by_name(&self, name: &str) -> Option<RealmFS> {
|
||||||
|
self.realmfs_map.get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, realmfs: &RealmFS) {
|
||||||
|
if !self.realmfs_map.contains_key(realmfs.name()) {
|
||||||
|
self.realmfs_map.insert(realmfs.name().to_string(), realmfs.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, name: &str) -> Option<RealmFS> {
|
||||||
|
self.realmfs_map.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name_exists(&self, name: &str) -> bool {
|
||||||
|
self.realmfs_map.contains_key(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_list(&self) -> Vec<RealmFS> {
|
||||||
|
let mut v = self.realmfs_map.values().cloned().collect::<Vec<RealmFS>>();
|
||||||
|
v.sort_unstable_by(|a,b| a.name().cmp(&b.name()));
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
183
libcitadel/src/realmfs/resizer.rs
Normal file
183
libcitadel/src/realmfs/resizer.rs
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
use std::fs::{File,OpenOptions};
|
||||||
|
use std::io::{Read,Seek,SeekFrom};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{ByteOrder,LittleEndian};
|
||||||
|
|
||||||
|
use crate::{RealmFS,Result,LoopDevice};
|
||||||
|
|
||||||
|
const BLOCK_SIZE: usize = 4096;
|
||||||
|
const BLOCKS_PER_MEG: usize = (1024 * 1024) / BLOCK_SIZE;
|
||||||
|
const BLOCKS_PER_GIG: usize = 1024 * BLOCKS_PER_MEG;
|
||||||
|
|
||||||
|
const RESIZE2FS: &str = "resize2fs";
|
||||||
|
|
||||||
|
// If less than 1gb remaining space
|
||||||
|
const AUTO_RESIZE_MINIMUM_FREE: ResizeSize = ResizeSize(1 * BLOCKS_PER_GIG);
|
||||||
|
// ... add 4gb to size of image
|
||||||
|
const AUTO_RESIZE_INCREASE_SIZE: ResizeSize = ResizeSize(4 * BLOCKS_PER_GIG);
|
||||||
|
|
||||||
|
pub struct ImageResizer<'a> {
|
||||||
|
image: &'a RealmFS,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResizeSize(usize);
|
||||||
|
|
||||||
|
impl ResizeSize {
|
||||||
|
|
||||||
|
pub fn gigs(n: usize) -> ResizeSize {
|
||||||
|
ResizeSize(BLOCKS_PER_GIG * n)
|
||||||
|
|
||||||
|
}
|
||||||
|
pub fn megs(n: usize) -> ResizeSize {
|
||||||
|
ResizeSize(BLOCKS_PER_MEG * n)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blocks(n: usize) -> ResizeSize {
|
||||||
|
ResizeSize(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nblocks(&self) -> usize {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size_in_gb(&self) -> usize {
|
||||||
|
self.0 / BLOCKS_PER_GIG
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size_in_mb(&self) -> usize {
|
||||||
|
self.0 / BLOCKS_PER_MEG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <'a> ImageResizer<'a> {
|
||||||
|
|
||||||
|
pub fn new(image: &'a RealmFS) -> ImageResizer<'a> {
|
||||||
|
ImageResizer { image }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grow_to(&mut self, size: ResizeSize) -> Result<()> {
|
||||||
|
let target_nblocks = size.nblocks();
|
||||||
|
let current_nblocks = self.image.metainfo_nblocks();
|
||||||
|
if current_nblocks >= target_nblocks {
|
||||||
|
info!("RealmFS image is already larger than requested size, doing nothing");
|
||||||
|
} else {
|
||||||
|
let size = ResizeSize::blocks(target_nblocks - current_nblocks);
|
||||||
|
self.grow_by(size)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grow_by(&mut self, size: ResizeSize) -> Result<()> {
|
||||||
|
let nblocks = size.nblocks();
|
||||||
|
let new_nblocks = self.image.metainfo_nblocks() + nblocks;
|
||||||
|
if self.image.is_sealed() {
|
||||||
|
bail!("Cannot resize sealed image '{}'. unseal first", self.image.name());
|
||||||
|
}
|
||||||
|
self.resize(new_nblocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(&self, new_nblocks: usize) -> Result<()> {
|
||||||
|
if new_nblocks < self.image.metainfo_nblocks() {
|
||||||
|
bail!("Cannot shrink image")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_nblocks - self.image.metainfo_nblocks()) > ResizeSize::gigs(8).nblocks() {
|
||||||
|
bail!("Can only increase size of RealmFS image by a maximum of 8gb at one time");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageResizer::resize_image_file(self.image.path(), new_nblocks)?;
|
||||||
|
|
||||||
|
if let Some(open_loop) = self.notify_open_loops()? {
|
||||||
|
info!("Running resize2fs {:?}", open_loop);
|
||||||
|
cmd!(RESIZE2FS, "{}", open_loop.device().display())?;
|
||||||
|
} else {
|
||||||
|
LoopDevice::with_loop(self.image.path(), Some(4096), false, |loopdev| {
|
||||||
|
info!("Running resize2fs {:?}", loopdev);
|
||||||
|
cmd!(RESIZE2FS, "{}", loopdev.device().display())?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let owner = self.image.metainfo().realmfs_owner().map(|s| s.to_owned());
|
||||||
|
self.image.update_unsealed_metainfo(self.image.name(), new_nblocks - 1, owner)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_image_file(file: &Path, nblocks: usize) -> Result<()> {
|
||||||
|
let len = nblocks * BLOCK_SIZE;
|
||||||
|
info!("Resizing image file to {}", len);
|
||||||
|
OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.open(file)?
|
||||||
|
.set_len(len as u64)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_open_loops(&self) -> Result<Option<LoopDevice>> {
|
||||||
|
let mut open_loop = None;
|
||||||
|
for loopdev in LoopDevice::find_devices_for(self.image.path())? {
|
||||||
|
loopdev.resize()
|
||||||
|
.unwrap_or_else(|err| warn!("Error running losetup -c {:?}: {}", loopdev, err));
|
||||||
|
open_loop = Some(loopdev);
|
||||||
|
}
|
||||||
|
Ok(open_loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// If the RealmFS needs to be resized to a larger size, returns the
|
||||||
|
/// recommended size. Pass this value to `ImageResizer.grow_to()` to
|
||||||
|
/// complete the resize.
|
||||||
|
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
|
||||||
|
let sb = match Superblock::load(realmfs.path(), 4096) {
|
||||||
|
Ok(sb) => sb,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error reading superblock from {}: {}", realmfs.path().display(), e);
|
||||||
|
return None;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sb.free_block_count();
|
||||||
|
let free_blocks = sb.free_block_count() as usize;
|
||||||
|
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||||
|
let mask = AUTO_RESIZE_INCREASE_SIZE.nblocks() - 1;
|
||||||
|
let grow_blocks = (free_blocks + mask) & !mask;
|
||||||
|
Some(ResizeSize::blocks(grow_blocks))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPERBLOCK_SIZE: usize = 1024;
|
||||||
|
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
|
||||||
|
impl Superblock {
|
||||||
|
fn new() -> Superblock {
|
||||||
|
Superblock([0u8; SUPERBLOCK_SIZE])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(path: impl AsRef<Path>, offset: u64) -> Result<Superblock> {
|
||||||
|
let mut sb = Superblock::new();
|
||||||
|
let mut file = File::open(path.as_ref())?;
|
||||||
|
file.seek(SeekFrom::Start(1024 + offset))?;
|
||||||
|
file.read_exact(&mut sb.0)?;
|
||||||
|
Ok(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_block_count(&self) -> u64 {
|
||||||
|
self.split_u64(0x0C, 0x158)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32(&self, offset: usize) -> u32 {
|
||||||
|
LittleEndian::read_u32(self.at(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_u64(&self, offset_lo: usize, offset_hi: usize) -> u64 {
|
||||||
|
let lo = self.u32(offset_lo) as u64;
|
||||||
|
let hi = self.u32(offset_hi) as u64;
|
||||||
|
(hi << 32) | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
fn at(&self, offset: usize) -> &[u8] {
|
||||||
|
&self.0[offset..]
|
||||||
|
}
|
||||||
|
}
|
145
libcitadel/src/realmfs/update.rs
Normal file
145
libcitadel/src/realmfs/update.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::{Result, RealmFS };
|
||||||
|
use crate::realmfs::Mountpoint;
|
||||||
|
use crate::realm::BridgeAllocator;
|
||||||
|
use crate::ResizeSize;
|
||||||
|
|
||||||
|
enum UpdateType {
|
||||||
|
NotSetup,
|
||||||
|
Sealed(RealmFS),
|
||||||
|
Unsealed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Update<'a> {
|
||||||
|
realmfs: &'a RealmFS,
|
||||||
|
network_allocated: bool,
|
||||||
|
update_type: UpdateType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <'a> Update<'a> {
|
||||||
|
pub fn new(realmfs: &'a RealmFS) -> Self {
|
||||||
|
Update { realmfs, network_allocated: false, update_type: UpdateType::NotSetup }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup(&mut self) -> Result<()> {
|
||||||
|
self.update_type = self.create_update_type()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auto_resize_size(&self) -> Option<ResizeSize> {
|
||||||
|
self.target_image().auto_resize_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_resize(&self, size: ResizeSize) -> Result<()> {
|
||||||
|
self.target_image().resize_grow_to(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_image(&self) -> &RealmFS {
|
||||||
|
if let UpdateType::Sealed(ref image) = self.update_type {
|
||||||
|
image
|
||||||
|
} else {
|
||||||
|
&self.realmfs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_update_type(&self) -> Result<UpdateType> {
|
||||||
|
if self.realmfs.is_sealed() {
|
||||||
|
let update_image = self.realmfs.update_copy()?;
|
||||||
|
Ok(UpdateType::Sealed(update_image))
|
||||||
|
} else {
|
||||||
|
Ok(UpdateType::Unsealed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_update_shell(&mut self) -> Result<()> {
|
||||||
|
self.run_update_shell("/usr/libexec/configure-host0.sh && exec /bin/bash")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mountpoint(&self) -> Result<Mountpoint> {
|
||||||
|
let target = self.target_image();
|
||||||
|
let activation = target.activate()
|
||||||
|
.map_err(|e| format_err!("failed to activate update image: {}", e))?;
|
||||||
|
|
||||||
|
activation.mountpoint_rw().cloned()
|
||||||
|
.ok_or(format_err!("Update image activation does not have a writeable mountpoint"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_update_shell(&mut self, command: &str) -> Result<()> {
|
||||||
|
|
||||||
|
let mountpoint = self.mountpoint().map_err(|e| {
|
||||||
|
let _ = self.cleanup();
|
||||||
|
format_err!("Could not run update shell: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut alloc = BridgeAllocator::default_bridge()?;
|
||||||
|
let addr = alloc.allocate_address_for(&self.name())?;
|
||||||
|
let gw = alloc.gateway();
|
||||||
|
self.network_allocated = true;
|
||||||
|
Command::new("/usr/bin/systemd-nspawn")
|
||||||
|
.arg(format!("--setenv=IFCONFIG_IP={}", addr))
|
||||||
|
.arg(format!("--setenv=IFCONFIG_GW={}", gw))
|
||||||
|
.arg("--quiet")
|
||||||
|
.arg(format!("--machine={}", self.name()))
|
||||||
|
.arg(format!("--directory={}", mountpoint))
|
||||||
|
.arg(format!("--network-zone=clear"))
|
||||||
|
.arg("/bin/bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(command)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| {
|
||||||
|
let _ = self.cleanup();
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
self.deactivate_update()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivate_update(&self) -> Result<()> {
|
||||||
|
match self.update_type {
|
||||||
|
UpdateType::Sealed(ref update_image) => update_image.deactivate()?,
|
||||||
|
UpdateType::Unsealed => self.realmfs.deactivate()?,
|
||||||
|
UpdateType::NotSetup => return Ok(()),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn apply_update(&mut self) -> Result<()> {
|
||||||
|
match self.update_type {
|
||||||
|
UpdateType::Sealed(ref update_image) => {
|
||||||
|
update_image.seal(Some(self.realmfs.name()))?;
|
||||||
|
fs::rename(update_image.path(), self.realmfs.path())?;
|
||||||
|
self.cleanup()
|
||||||
|
},
|
||||||
|
UpdateType::Unsealed => self.cleanup(),
|
||||||
|
UpdateType::NotSetup => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("{}-update", self.realmfs.name())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self) -> Result<()> {
|
||||||
|
match self.update_type {
|
||||||
|
UpdateType::Sealed(ref update_image) => {
|
||||||
|
update_image.deactivate()?;
|
||||||
|
if update_image.path().exists() {
|
||||||
|
fs::remove_file(update_image.path())?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpdateType::Unsealed => {
|
||||||
|
self.realmfs.deactivate()?;
|
||||||
|
}
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
self.update_type = UpdateType::NotSetup;
|
||||||
|
|
||||||
|
if self.network_allocated {
|
||||||
|
BridgeAllocator::default_bridge()?
|
||||||
|
.free_allocation_for(&self.name())?;
|
||||||
|
self.network_allocated = false;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user