adds recovery (#731)

* should work

* works for real

* fix build
This commit is contained in:
Aiden McClelland
2021-10-27 15:50:53 -06:00
parent d0e8211b24
commit 1e7492e915
27 changed files with 694 additions and 598 deletions

View File

@@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize};
use tracing::instrument;
use self::util::DiskInfo;
use crate::context::RpcContext;
use crate::disk::util::{BackupMountGuard, TmpMountGuard};
use crate::s9pk::manifest::PackageId;
use crate::util::{display_serializable, IoFormat, Version};
@@ -17,7 +16,7 @@ use crate::Error;
pub mod main;
pub mod util;
#[command(subcommands(list))]
#[command(subcommands(list, backup_info))]
pub fn disk() -> Result<(), Error> {
Ok(())
}
@@ -139,14 +138,13 @@ fn display_backup_info(info: BackupInfo, matches: &ArgMatches<'_>) {
}
#[command(rename = "backup-info", display(display_backup_info))]
#[instrument(skip(ctx, password))]
#[instrument(skip(password))]
pub async fn backup_info(
#[context] ctx: RpcContext,
#[arg] logicalname: PathBuf,
#[arg] password: String,
) -> Result<BackupInfo, Error> {
let guard =
BackupMountGuard::mount(TmpMountGuard::mount(logicalname).await?, &password).await?;
BackupMountGuard::mount(TmpMountGuard::mount(logicalname, None).await?, &password).await?;
let res = guard.metadata.clone();

View File

@@ -1,16 +1,19 @@
use std::collections::{BTreeMap, BTreeSet};
use std::os::unix::prelude::OsStrExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Weak};
use color_eyre::eyre::{self, eyre};
use digest::Digest;
use futures::TryStreamExt;
use indexmap::IndexSet;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::process::Command;
use tokio::sync::Mutex;
use tracing::instrument;
use super::BackupInfo;
@@ -18,7 +21,7 @@ use crate::auth::check_password;
use crate::middleware::encrypt::{decrypt_slice, encrypt_slice};
use crate::s9pk::manifest::PackageId;
use crate::util::io::from_yaml_async_reader;
use crate::util::{AtomicFile, FileLock, GeneralGuard, Invoke, IoFormat, Version};
use crate::util::{AtomicFile, FileLock, Invoke, IoFormat, Version};
use crate::volume::BACKUP_DIR;
use crate::{Error, ResultExt as _};
@@ -158,7 +161,10 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
.filter_map(|c| c.get(1))
.map(|d| Path::new("/dev").join(d.as_str()))
.collect(),
Err(e) => BTreeSet::new(),
Err(e) => {
tracing::warn!("`zpool status` returned error: {}", e);
BTreeSet::new()
}
};
let disks = tokio_stream::wrappers::ReadDirStream::new(
tokio::fs::read_dir(DISK_PATH)
@@ -241,71 +247,67 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
.unwrap_or_default();
let mut used = None;
let tmp_mountpoint =
Path::new(TMP_MOUNTPOINT).join(&part.strip_prefix("/").unwrap_or(&part));
if let Err(e) = mount(&part, &tmp_mountpoint).await {
tracing::warn!("Could not collect usage information: {}", e.source)
} else {
let mount_guard = GeneralGuard::new(|| {
let path = tmp_mountpoint.clone();
tokio::spawn(unmount(path))
});
used = get_used(&tmp_mountpoint)
.await
.map_err(|e| {
tracing::warn!(
"Could not get usage of {}: {}",
part.display(),
e.source
)
})
.ok();
let backup_unencrypted_metadata_path =
tmp_mountpoint.join("EmbassyBackups/unencrypted-metadata.cbor");
if tokio::fs::metadata(&backup_unencrypted_metadata_path)
.await
.is_ok()
{
embassy_os = match (|| async {
IoFormat::Cbor.from_slice(
&tokio::fs::read(&backup_unencrypted_metadata_path)
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
backup_unencrypted_metadata_path.display().to_string(),
)
})?,
)
})()
.await
match TmpMountGuard::mount(&part, None).await {
Err(e) => tracing::warn!("Could not collect usage information: {}", e.source),
Ok(mount_guard) => {
used = get_used(&mount_guard)
.await
.map_err(|e| {
tracing::warn!(
"Could not get usage of {}: {}",
part.display(),
e.source
)
})
.ok();
let backup_unencrypted_metadata_path = mount_guard
.as_ref()
.join("EmbassyBackups/unencrypted-metadata.cbor");
if tokio::fs::metadata(&backup_unencrypted_metadata_path)
.await
.is_ok()
{
Ok(a) => Some(a),
Err(e) => {
tracing::error!(
"Error fetching unencrypted backup metadata: {}",
e
);
None
}
};
} else if label.as_deref() == Some("rootfs") {
let version_path =
tmp_mountpoint.join("root").join("appmgr").join("version");
if tokio::fs::metadata(&version_path).await.is_ok() {
embassy_os = Some(EmbassyOsRecoveryInfo {
version: from_yaml_async_reader(File::open(&version_path).await?)
embassy_os = match (|| async {
IoFormat::Cbor.from_slice(
&tokio::fs::read(&backup_unencrypted_metadata_path)
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
backup_unencrypted_metadata_path
.display()
.to_string(),
)
})?,
)
})()
.await
{
Ok(a) => Some(a),
Err(e) => {
tracing::error!(
"Error fetching unencrypted backup metadata: {}",
e
);
None
}
};
} else if label.as_deref() == Some("rootfs") {
let version_path = mount_guard.as_ref().join("root/appmgr/version");
if tokio::fs::metadata(&version_path).await.is_ok() {
embassy_os = Some(EmbassyOsRecoveryInfo {
version: from_yaml_async_reader(
File::open(&version_path).await?,
)
.await?,
full: true,
password_hash: None,
wrapped_key: None,
});
full: true,
password_hash: None,
wrapped_key: None,
});
}
}
mount_guard.unmount().await?;
}
mount_guard
.drop()
.await
.with_kind(crate::ErrorKind::Unknown)??;
}
partitions.push(PartitionInfo {
@@ -361,11 +363,11 @@ pub async fn mount(
Ok(())
}
#[instrument(skip(src, dst, password))]
#[instrument(skip(src, dst, key))]
pub async fn mount_ecryptfs<P0: AsRef<Path>, P1: AsRef<Path>>(
src: P0,
dst: P1,
password: &str,
key: &str,
) -> Result<(), Error> {
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(dst.as_ref())
@@ -383,7 +385,7 @@ pub async fn mount_ecryptfs<P0: AsRef<Path>, P1: AsRef<Path>>(
.arg(src.as_ref())
.arg(dst.as_ref())
.arg("-o")
.arg(format!("key=passphrase,passwd={},ecryptfs_cipher=aes,ecryptfs_key_bytes=32,ecryptfs_passthrough=n,ecryptfs_enable_filename_crypto=y", password))
.arg(format!("key=passphrase,passwd={},ecryptfs_cipher=aes,ecryptfs_key_bytes=32,ecryptfs_passthrough=n,ecryptfs_enable_filename_crypto=y", key))
.stdin(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
@@ -422,6 +424,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
if is_mountpoint.success() {
unmount(dst.as_ref()).await?;
}
tokio::fs::create_dir_all(&src).await?;
tokio::fs::create_dir_all(&dst).await?;
let mut mount_cmd = tokio::process::Command::new("mount");
mount_cmd.arg("--bind");
@@ -432,17 +435,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
.arg(src.as_ref())
.arg(dst.as_ref())
.invoke(crate::ErrorKind::Filesystem)
.await
.map_err(|e| {
Error::new(
e.source.wrap_err(format!(
"Binding {} to {}",
src.as_ref().display(),
dst.as_ref().display(),
)),
e.kind,
)
})?;
.await?;
Ok(())
}
@@ -450,6 +443,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
pub async fn unmount<P: AsRef<Path>>(mountpoint: P) -> Result<(), Error> {
tracing::debug!("Unmounting {}.", mountpoint.as_ref().display());
let umount_output = tokio::process::Command::new("umount")
.arg("-l")
.arg(mountpoint.as_ref())
.output()
.await?;
@@ -472,10 +466,11 @@ pub async fn unmount<P: AsRef<Path>>(mountpoint: P) -> Result<(), Error> {
}
#[async_trait::async_trait]
pub trait GenericMountGuard: AsRef<Path> {
pub trait GenericMountGuard: AsRef<Path> + std::fmt::Debug + Send + Sync + 'static {
async fn unmount(mut self) -> Result<(), Error>;
}
#[derive(Debug)]
pub struct MountGuard {
mountpoint: PathBuf,
mounted: bool,
@@ -484,9 +479,14 @@ impl MountGuard {
pub async fn mount(
logicalname: impl AsRef<Path>,
mountpoint: impl AsRef<Path>,
encryption_key: Option<&str>,
) -> Result<Self, Error> {
let mountpoint = mountpoint.as_ref().to_owned();
mount(logicalname, &mountpoint).await?;
if let Some(key) = encryption_key {
mount_ecryptfs(logicalname, &mountpoint, key).await?;
} else {
mount(logicalname, &mountpoint).await?;
}
Ok(MountGuard {
mountpoint,
mounted: true,
@@ -538,27 +538,45 @@ async fn tmp_mountpoint(source: impl AsRef<Path>) -> Result<PathBuf, Error> {
)))
}
lazy_static! {
static ref TMP_MOUNTS: Mutex<BTreeMap<PathBuf, Weak<MountGuard>>> = Mutex::new(BTreeMap::new());
}
#[derive(Debug)]
pub struct TmpMountGuard {
guard: MountGuard,
lock: FileLock,
guard: Arc<MountGuard>,
}
impl TmpMountGuard {
pub async fn mount(logicalname: impl AsRef<Path>) -> Result<Self, Error> {
#[instrument(skip(logicalname, encryption_key))]
pub async fn mount(
logicalname: impl AsRef<Path>,
encryption_key: Option<&str>,
) -> Result<Self, Error> {
let mountpoint = tmp_mountpoint(&logicalname).await?;
let lock = FileLock::new(mountpoint.with_extension("lock")).await?;
let guard = MountGuard::mount(logicalname, &mountpoint).await?;
Ok(TmpMountGuard { guard, lock })
let mut tmp_mounts = TMP_MOUNTS.lock().await;
if !tmp_mounts.contains_key(&mountpoint) {
tmp_mounts.insert(mountpoint.clone(), Weak::new());
}
let weak_slot = tmp_mounts.get_mut(&mountpoint).unwrap();
if let Some(guard) = weak_slot.upgrade() {
Ok(TmpMountGuard { guard })
} else {
let guard =
Arc::new(MountGuard::mount(logicalname, &mountpoint, encryption_key).await?);
*weak_slot = Arc::downgrade(&guard);
Ok(TmpMountGuard { guard })
}
}
pub async fn unmount(self) -> Result<(), Error> {
let TmpMountGuard { guard, lock } = self;
guard.unmount().await?;
lock.unlock().await?;
if let Ok(guard) = Arc::try_unwrap(self.guard) {
guard.unmount().await?;
}
Ok(())
}
}
impl AsRef<Path> for TmpMountGuard {
fn as_ref(&self) -> &Path {
self.guard.as_ref()
(&*self.guard).as_ref()
}
}
#[async_trait::async_trait]
@@ -570,11 +588,10 @@ impl GenericMountGuard for TmpMountGuard {
pub struct BackupMountGuard<G: GenericMountGuard> {
backup_disk_mount_guard: Option<G>,
encrypted_guard: Option<TmpMountGuard>,
enc_key: String,
pub unencrypted_metadata: EmbassyOsRecoveryInfo,
pub metadata: BackupInfo,
mountpoint: PathBuf,
mounted: bool,
}
impl<G: GenericMountGuard> BackupMountGuard<G> {
fn backup_disk_path(&self) -> &Path {
@@ -585,12 +602,12 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
}
}
#[instrument(skip(password))]
pub async fn mount(backup_disk_mount_guard: G, password: &str) -> Result<Self, Error> {
let mountpoint = tmp_mountpoint(&backup_disk_mount_guard).await?;
let backup_disk_path = backup_disk_mount_guard.as_ref();
let unencrypted_metadata_path =
backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor");
let unencrypted_metadata: EmbassyOsRecoveryInfo =
let mut unencrypted_metadata: EmbassyOsRecoveryInfo =
if tokio::fs::metadata(&unencrypted_metadata_path)
.await
.is_ok()
@@ -613,7 +630,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
unencrypted_metadata.wrapped_key.as_ref(),
) {
let wrapped_key =
base32::decode(base32::Alphabet::RFC4648 { padding: false }, wrapped_key)
base32::decode(base32::Alphabet::RFC4648 { padding: true }, wrapped_key)
.ok_or_else(|| {
Error::new(
eyre!("failed to decode wrapped key"),
@@ -629,6 +646,23 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
)
};
if unencrypted_metadata.password_hash.is_none() {
unencrypted_metadata.password_hash = Some(
argon2::hash_encoded(
password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::default(),
)
.with_kind(crate::ErrorKind::PasswordHashGeneration)?,
);
}
if unencrypted_metadata.wrapped_key.is_none() {
unencrypted_metadata.wrapped_key = Some(base32::encode(
base32::Alphabet::RFC4648 { padding: true },
&encrypt_slice(&enc_key, password),
));
}
let crypt_path = backup_disk_path.join("EmbassyBackups/crypt");
if tokio::fs::metadata(&crypt_path).await.is_err() {
tokio::fs::create_dir_all(&crypt_path).await.with_ctx(|_| {
@@ -638,38 +672,26 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
)
})?;
}
mount_ecryptfs(&crypt_path, &mountpoint, &enc_key).await?;
let metadata = match async {
let metadata_path = mountpoint.join("metadata.cbor");
let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() {
IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(
|_| {
(
crate::ErrorKind::Filesystem,
metadata_path.display().to_string(),
)
},
)?)?
} else {
Default::default()
};
Ok(metadata)
}
.await
{
Ok(a) => a,
Err(e) => {
unmount(&mountpoint).await?;
return Err(e);
}
let encrypted_guard = TmpMountGuard::mount(&crypt_path, Some(&enc_key)).await?;
let metadata_path = encrypted_guard.as_ref().join("metadata.cbor");
let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() {
IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
metadata_path.display().to_string(),
)
})?)?
} else {
Default::default()
};
Ok(Self {
backup_disk_mount_guard: Some(backup_disk_mount_guard),
encrypted_guard: Some(encrypted_guard),
enc_key,
unencrypted_metadata,
metadata,
mountpoint,
mounted: true,
})
}
@@ -689,22 +711,23 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
Ok(())
}
#[instrument(skip(self))]
pub async fn mount_package_backup(
&self,
id: &PackageId,
) -> Result<PackageBackupMountGuard, Error> {
let lock = FileLock::new(Path::new(BACKUP_DIR).join(format!("{}.lock", id))).await?;
let lock = FileLock::new(Path::new(BACKUP_DIR).join(format!("{}.lock", id)), false).await?;
let mountpoint = Path::new(BACKUP_DIR).join(id);
bind(self.mountpoint.join(id), &mountpoint, false).await?;
bind(self.as_ref().join(id), &mountpoint, false).await?;
Ok(PackageBackupMountGuard {
mountpoint,
lock,
mounted: true,
mountpoint: Some(mountpoint),
lock: Some(lock),
})
}
#[instrument(skip(self))]
pub async fn save(&self) -> Result<(), Error> {
let metadata_path = self.mountpoint.join("metadata.cbor");
let metadata_path = self.as_ref().join("metadata.cbor");
let backup_disk_path = self.backup_disk_path();
let mut file = AtomicFile::new(&metadata_path).await?;
file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?)
@@ -719,10 +742,10 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
Ok(())
}
#[instrument(skip(self))]
pub async fn unmount(mut self) -> Result<(), Error> {
if self.mounted {
unmount(&self.mountpoint).await?;
self.mounted = false;
if let Some(guard) = self.encrypted_guard.take() {
guard.unmount().await?;
}
if let Some(guard) = self.backup_disk_mount_guard.take() {
guard.unmount().await?;
@@ -730,6 +753,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
Ok(())
}
#[instrument(skip(self))]
pub async fn save_and_unmount(self) -> Result<(), Error> {
self.save().await?;
self.unmount().await?;
@@ -738,42 +762,63 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
}
impl<G: GenericMountGuard> AsRef<Path> for BackupMountGuard<G> {
fn as_ref(&self) -> &Path {
&self.mountpoint
if let Some(guard) = &self.encrypted_guard {
guard.as_ref()
} else {
unreachable!()
}
}
}
impl<G: GenericMountGuard> Drop for BackupMountGuard<G> {
fn drop(&mut self) {
if self.mounted {
let mountpoint = std::mem::take(&mut self.mountpoint);
tokio::spawn(async move { unmount(mountpoint).await.unwrap() });
}
let first = self.encrypted_guard.take();
let second = self.backup_disk_mount_guard.take();
tokio::spawn(async move {
if let Some(guard) = first {
guard.unmount().await.unwrap();
}
if let Some(guard) = second {
guard.unmount().await.unwrap();
}
});
}
}
pub struct PackageBackupMountGuard {
mountpoint: PathBuf,
lock: FileLock,
mounted: bool,
mountpoint: Option<PathBuf>,
lock: Option<FileLock>,
}
impl PackageBackupMountGuard {
pub async fn unmount(mut self) -> Result<(), Error> {
if self.mounted {
unmount(&self.mountpoint).await?;
self.mounted = false;
if let Some(mountpoint) = self.mountpoint.take() {
unmount(&mountpoint).await?;
}
if let Some(lock) = self.lock.take() {
lock.unlock().await?;
}
Ok(())
}
}
impl AsRef<Path> for PackageBackupMountGuard {
fn as_ref(&self) -> &Path {
&self.mountpoint
if let Some(mountpoint) = &self.mountpoint {
mountpoint
} else {
unreachable!()
}
}
}
impl Drop for PackageBackupMountGuard {
fn drop(&mut self) {
if self.mounted {
let mountpoint = std::mem::take(&mut self.mountpoint);
tokio::spawn(async move { unmount(mountpoint).await.unwrap() });
}
let mountpoint = self.mountpoint.take();
let lock = self.lock.take();
tokio::spawn(async move {
if let Some(mountpoint) = mountpoint {
unmount(&mountpoint).await.unwrap();
}
if let Some(lock) = lock {
lock.unlock().await.unwrap();
}
});
}
}