forked from brl/citadel-tools
Compare commits
5 Commits
error_hand
...
upgrade_to
Author | SHA1 | Date | |
---|---|---|---|
44d408f84a | |||
904707a7c3 | |||
![]() |
24f786cf75 | ||
![]() |
2dc8bf2922 | ||
2a16bd4c41 |
2398
Cargo.lock
generated
2398
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software" ]
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
2648
citadel-installer-ui/Cargo.lock
generated
2648
citadel-installer-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,14 +8,19 @@ homepage = "https://subgraph.com"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
rpassword = "4.0"
|
||||
clap = "2.33"
|
||||
rpassword = "7.3"
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
lazy_static = "1.4"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
toml = "0.5"
|
||||
toml = "0.8"
|
||||
hex = "0.4"
|
||||
byteorder = "1"
|
||||
dbus = "0.8.4"
|
||||
pwhash = "0.3.1"
|
||||
pwhash = "1.0"
|
||||
tempfile = "3"
|
||||
ed25519-dalek = {version = "2.1", features = ["pem"]}
|
||||
anyhow = "1.0"
|
||||
reqwest = {version = "0.12", features = ["blocking"]}
|
||||
glob = "0.3"
|
||||
serde_cbor = "0.11"
|
||||
|
@@ -8,7 +8,7 @@ use std::path::Path;
|
||||
|
||||
mod live;
|
||||
mod disks;
|
||||
mod rootfs;
|
||||
pub mod rootfs;
|
||||
|
||||
pub fn main(args: Vec<String>) {
|
||||
if CommandLine::debug() {
|
||||
|
@@ -94,7 +94,7 @@ fn choose_revert_partition(best: Option<Partition>) -> Option<Partition> {
|
||||
best
|
||||
}
|
||||
|
||||
fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
|
||||
pub fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
|
||||
let mut partitions = Partition::rootfs_partitions()?;
|
||||
|
||||
if scan {
|
||||
@@ -136,8 +136,11 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
|
||||
}
|
||||
|
||||
// Compare versions and channels
|
||||
let a_v = a.metainfo().version();
|
||||
let b_v = b.metainfo().version();
|
||||
let bind_a = a.metainfo();
|
||||
let bind_b = b.metainfo();
|
||||
|
||||
let a_v = bind_a.version();
|
||||
let b_v = bind_b.version();
|
||||
|
||||
// Compare versions only if channels match
|
||||
if a.metainfo().channel() == b.metainfo().channel() {
|
||||
|
293
citadel-tool/src/fetch/fetch.rs
Normal file
293
citadel-tool/src/fetch/fetch.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use crate::{update, Path};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::ArgMatches;
|
||||
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
|
||||
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
|
||||
use libcitadel::{updates, updates::CitadelVersionStruct};
|
||||
use libcitadel::{OsRelease, ResourceImage};
|
||||
|
||||
use std::io::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
|
||||
|
||||
pub fn check() -> Result<()> {
|
||||
let current_version = get_current_os_config()?;
|
||||
|
||||
let server_citadel_version = fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let components_to_upgrade =
|
||||
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||
|
||||
if components_to_upgrade.len() == 1 {
|
||||
println!(
|
||||
"We found the following component to upgrade: {}",
|
||||
components_to_upgrade[0]
|
||||
);
|
||||
} else if components_to_upgrade.len() > 1 {
|
||||
println!("We found the following components to upgrade:");
|
||||
|
||||
for component in components_to_upgrade {
|
||||
println!("{}", component);
|
||||
}
|
||||
} else {
|
||||
println!("Your system is up to date!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn download(sub_matches: &ArgMatches) -> Result<()> {
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let mut path = "";
|
||||
if sub_matches.get_flag("rootfs") {
|
||||
path = &server_citadel_version.component_version[0].file_path;
|
||||
} else if sub_matches.get_flag("kernel") {
|
||||
path = &server_citadel_version.component_version[1].file_path;
|
||||
} else if sub_matches.get_flag("extra") {
|
||||
path = &server_citadel_version.component_version[2].file_path;
|
||||
}
|
||||
|
||||
download_file(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_remote() -> Result<()> {
|
||||
let server_citadel_version = fetch_and_verify_version_cbor(&get_current_os_config()?)?;
|
||||
|
||||
println!("Server offers:\n{server_citadel_version}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn upgrade() -> Result<()> {
|
||||
// First get access to the current citadel's parameters
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
// What do we need to upgrade?
|
||||
let components_to_upgrade =
|
||||
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||
|
||||
if components_to_upgrade.len() == 1 {
|
||||
println!("We found a component to upgrade!");
|
||||
let allow_download = prompt_user_for_permission_to_download(&components_to_upgrade[0])?;
|
||||
|
||||
if allow_download {
|
||||
let save_path = download_file(&components_to_upgrade[0].file_path)?;
|
||||
|
||||
// run citadel-update to upgrade
|
||||
println!("Installing image");
|
||||
update::install_image(&save_path, 0)?;
|
||||
println!("Image installed correctly");
|
||||
} else {
|
||||
println!("Ok! Maybe later");
|
||||
}
|
||||
} else if components_to_upgrade.len() > 1 {
|
||||
println!("We found some components to upgrade!");
|
||||
for component in components_to_upgrade {
|
||||
let allow_download = prompt_user_for_permission_to_download(&component)?;
|
||||
|
||||
if allow_download {
|
||||
let save_path = download_file(&component.file_path)?;
|
||||
|
||||
println!("Installing image");
|
||||
update::install_image(&save_path, 0)?;
|
||||
println!("Image installed correctly");
|
||||
} else {
|
||||
println!("Ok! Maybe later");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Your system is up to date!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reinstall(sub_matches: &ArgMatches) -> Result<()> {
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let mut path = "";
|
||||
if sub_matches.get_flag("rootfs") {
|
||||
path = &server_citadel_version.component_version[0].file_path;
|
||||
} else if sub_matches.get_flag("kernel") {
|
||||
path = &server_citadel_version.component_version[1].file_path;
|
||||
} else if sub_matches.get_flag("extra") {
|
||||
path = &server_citadel_version.component_version[2].file_path;
|
||||
}
|
||||
|
||||
let save_path = download_file(path)?;
|
||||
update::install_image(&save_path, 0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a vec of ComponentVersion structs of the components which can be upgraded
|
||||
fn compare_citadel_versions(
|
||||
current: &CitadelVersionStruct,
|
||||
offered: &CitadelVersionStruct,
|
||||
) -> Result<Vec<updates::AvailableComponentVersion>> {
|
||||
let mut update_vec: Vec<updates::AvailableComponentVersion> = Vec::new();
|
||||
|
||||
// safety checks
|
||||
if current.channel != offered.channel {
|
||||
panic!("Error: channels do not match");
|
||||
} else if current.client != offered.client {
|
||||
panic!("Error: clients do not match");
|
||||
} else if current.publisher != offered.publisher {
|
||||
panic!("Error: publishers do not match");
|
||||
}
|
||||
|
||||
for i in 0..current.component_version.len() {
|
||||
if current.component_version[i] < offered.component_version[i] {
|
||||
update_vec.push(offered.component_version[i].clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(update_vec)
|
||||
}
|
||||
|
||||
// We need to get the version of the rootfs, kernel and extra images currently installed
|
||||
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
|
||||
let client = OsRelease::citadel_client().context("Failed to find client of current system")?;
|
||||
let channel = OsRelease::citadel_channel().context("Failed to find channel of current system")?;
|
||||
let publisher = OsRelease::citadel_publisher().context("Failed to find publisher of current system")?;
|
||||
|
||||
let metainfo;
|
||||
// choose best partion to boot from as the partition to compare versions with
|
||||
let rootfs_version = match crate::boot::rootfs::choose_boot_partiton(false, false) {
|
||||
Ok(part) => {metainfo = part.header().metainfo();
|
||||
metainfo.version() }
|
||||
Err(e) => bail!("Rootfs version not found. Error: {e}"),
|
||||
};
|
||||
|
||||
// Get highest values of image versions
|
||||
let kernel_resource = ResourceImage::find("kernel")?.metainfo();
|
||||
let kernel_version = kernel_resource.version();
|
||||
|
||||
let extra_resource = ResourceImage::find("extra")?.metainfo();
|
||||
let extra_version = extra_resource.version();
|
||||
|
||||
let mut component_version = Vec::new();
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Rootfs,
|
||||
version: rootfs_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Kernel,
|
||||
version: kernel_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Extra,
|
||||
version: extra_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
|
||||
let current_version_struct = updates::CitadelVersionStruct {
|
||||
client: client.to_owned(),
|
||||
channel: channel.to_owned(),
|
||||
component_version,
|
||||
publisher: publisher.to_owned(),
|
||||
};
|
||||
|
||||
Ok(current_version_struct)
|
||||
}
|
||||
|
||||
fn fetch_and_verify_version_cbor(
|
||||
current_citadel_version: &updates::CitadelVersionStruct,
|
||||
) -> Result<updates::CitadelVersionStruct> {
|
||||
let url = format!(
|
||||
"https://{}/{}/{}/version.cbor",
|
||||
UPDATE_SERVER_HOSTNAME, current_citadel_version.client, current_citadel_version.channel
|
||||
);
|
||||
|
||||
let version_file_bytes = reqwest::blocking::get(&url)?
|
||||
.bytes()
|
||||
.context(format!("Failed to get version_file_bytes from {url}"))?;
|
||||
|
||||
let crypto_container: updates::CryptoContainerFile =
|
||||
serde_cbor::from_slice(&version_file_bytes)
|
||||
.context(format!("Failed to parse version.cbor from {}", url))?;
|
||||
|
||||
// find update server public key kept in the rootfs
|
||||
let mut file = std::fs::File::open(UPDATE_SERVER_KEY_PATH).context(format!(
|
||||
"Failed to open update_server_key file from {}",
|
||||
UPDATE_SERVER_KEY_PATH
|
||||
))?;
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let public_key = VerifyingKey::from_public_key_pem(&contents)
|
||||
.context("Failed to parse public key from file.")?;
|
||||
|
||||
let signature = ed25519_dalek::Signature::from_str(&crypto_container.signature)?;
|
||||
|
||||
// verify signature
|
||||
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)
|
||||
.context("We failed to verify the signature update release file. Please make sure the key at /etc/citade/update_server_key.pub matches the one publicly linked to your update provider.")?;
|
||||
|
||||
// construct the struct
|
||||
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||
serde_cbor::from_slice(&crypto_container.serialized_citadel_version)?;
|
||||
|
||||
Ok(citadel_version_struct)
|
||||
}
|
||||
|
||||
fn prompt_user_for_permission_to_download(
|
||||
component: &updates::AvailableComponentVersion,
|
||||
) -> Result<bool> {
|
||||
println!(
|
||||
"Would you like to download and install the new version of the {} image with version {}? (y/n)",
|
||||
component.component, component.version
|
||||
);
|
||||
|
||||
loop {
|
||||
let stdin = std::io::stdin();
|
||||
let mut user_input = String::new();
|
||||
|
||||
stdin.read_line(&mut user_input)?;
|
||||
|
||||
if user_input.trim() == "y" {
|
||||
return Ok(true);
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn download_file(path: &str) -> Result<std::path::PathBuf> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let url = format!("https://{UPDATE_SERVER_HOSTNAME}/{path}");
|
||||
println!("Downloading from {url}");
|
||||
|
||||
let component_download_response = client.get(&url).send()?;
|
||||
|
||||
if !component_download_response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"Failed to download image from {}. Server returned error {}",
|
||||
path,
|
||||
component_download_response.status()
|
||||
);
|
||||
}
|
||||
|
||||
let path = Path::new(path);
|
||||
let path = format!("/tmp/{}", path.file_name().unwrap().to_str().unwrap());
|
||||
|
||||
let mut content = std::io::Cursor::new(component_download_response.bytes()?);
|
||||
let mut file =
|
||||
std::fs::File::create(&path).context(format!("Failed to create file at {path}"))?;
|
||||
std::io::copy(&mut content, &mut file)?;
|
||||
|
||||
println!("Saved file to {path}");
|
||||
|
||||
Ok(std::path::PathBuf::from(path))
|
||||
}
|
55
citadel-tool/src/fetch/mod.rs
Normal file
55
citadel-tool/src/fetch/mod.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use clap::{arg, command, ArgAction, Command};
|
||||
use std::process::exit;
|
||||
use libcitadel::util;
|
||||
|
||||
mod fetch;
|
||||
|
||||
pub fn main() {
|
||||
if !util::is_euid_root() {
|
||||
println!("Please run this program as root");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let matches = command!()
|
||||
.subcommand_required(true)
|
||||
.subcommand(Command::new("check").about("Check for updates from remote server"))
|
||||
.subcommand(
|
||||
Command::new("download")
|
||||
.about("Download a specific file from the server")
|
||||
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
|
||||
.arg_required_else_help(true),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("read-remote")
|
||||
.about("Read the remote server and print information on versions offered"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("upgrade")
|
||||
.about("Download and install all components found on the server to be more recent than currently installed on system")
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("reinstall")
|
||||
.about("Download and install a specific component even if the server's component version is not greater than currently installed")
|
||||
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
|
||||
.arg_required_else_help(true),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
Some(("check", _sub_matches)) => fetch::check(),
|
||||
Some(("download", sub_matches)) => fetch::download(sub_matches),
|
||||
Some(("read-remote", _sub_matches)) => fetch::read_remote(),
|
||||
Some(("upgrade", _sub_matches)) => fetch::upgrade(),
|
||||
Some(("reinstall", sub_matches)) => fetch::reinstall(sub_matches),
|
||||
_ => unreachable!("Please pass a subcommand"),
|
||||
};
|
||||
|
||||
if let Err(ref e) = result {
|
||||
println!("Error: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
@@ -1,89 +1,97 @@
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
use clap::{App,Arg,SubCommand,ArgMatches};
|
||||
use clap::AppSettings::*;
|
||||
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
use clap::{Arg,ArgMatches};
|
||||
use clap::{command, ArgAction, Command};
|
||||
use hex;
|
||||
|
||||
pub fn main(args: Vec<String>) {
|
||||
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
|
||||
let app = App::new("citadel-image")
|
||||
pub fn main() {
|
||||
let matches = command!()
|
||||
.about("Citadel update image builder")
|
||||
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
|
||||
.arg_required_else_help(true)
|
||||
.disable_help_subcommand(true)
|
||||
.subcommand(
|
||||
Command::new("metainfo")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("info")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("generate-verity")
|
||||
.about("Generate dm-verity hash tree for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("verify")
|
||||
.about("Verify dm-verity hash tree for an image file")
|
||||
.arg(
|
||||
Arg::new("option")
|
||||
.long("option")
|
||||
.required(true)
|
||||
.help("Path to image file"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("install-rootfs")
|
||||
.about("Install rootfs image file to a partition")
|
||||
.arg(
|
||||
Arg::new("choose")
|
||||
.long("just-choose")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Don't install anything, just show which partition would be chosen"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("skip-sha")
|
||||
.long("skip-sha")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Skip verification of header sha256 value"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-prefer")
|
||||
.long("no-prefer")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Don't set PREFER_BOOT flag"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.required_unless_present("choose")
|
||||
.help("Path to image file"),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("genkeys").about("Generate a pair of keys"))
|
||||
.subcommand(
|
||||
Command::new("decompress")
|
||||
.about("Decompress a compressed image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("bless")
|
||||
.about("Mark currently mounted rootfs partition as successfully booted"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("verify-shasum")
|
||||
.about("Verify the sha256 sum of the image")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
.subcommand(SubCommand::with_name("metainfo")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("info")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("generate-verity")
|
||||
.about("Generate dm-verity hash tree for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("verify")
|
||||
.about("Verify dm-verity hash tree for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("install-rootfs")
|
||||
.about("Install rootfs image file to a partition")
|
||||
.arg(Arg::with_name("choose")
|
||||
.long("just-choose")
|
||||
.help("Don't install anything, just show which partition would be chosen"))
|
||||
.arg(Arg::with_name("skip-sha")
|
||||
.long("skip-sha")
|
||||
.help("Skip verification of header sha256 value"))
|
||||
.arg(Arg::with_name("no-prefer")
|
||||
.long("no-prefer")
|
||||
.help("Don't set PREFER_BOOT flag"))
|
||||
.arg(Arg::with_name("path")
|
||||
.required_unless("choose")
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("genkeys")
|
||||
.about("Generate a pair of keys"))
|
||||
|
||||
.subcommand(SubCommand::with_name("decompress")
|
||||
.about("Decompress a compressed image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("bless")
|
||||
.about("Mark currently mounted rootfs partition as successfully booted"))
|
||||
|
||||
.subcommand(SubCommand::with_name("verify-shasum")
|
||||
.about("Verify the sha256 sum of the image")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")));
|
||||
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
let matches = app.get_matches_from(args);
|
||||
let result = match matches.subcommand() {
|
||||
("metainfo", Some(m)) => metainfo(m),
|
||||
("info", Some(m)) => info(m),
|
||||
("generate-verity", Some(m)) => generate_verity(m),
|
||||
("verify", Some(m)) => verify(m),
|
||||
("sign-image", Some(m)) => sign_image(m),
|
||||
("genkeys", Some(_)) => genkeys(),
|
||||
("decompress", Some(m)) => decompress(m),
|
||||
("verify-shasum", Some(m)) => verify_shasum(m),
|
||||
("install-rootfs", Some(m)) => install_rootfs(m),
|
||||
("install", Some(m)) => install_image(m),
|
||||
("bless", Some(_)) => bless(),
|
||||
Some(("metainfo", sub_m)) => metainfo(sub_m),
|
||||
Some(("info", sub_m)) => info(sub_m),
|
||||
Some(("generate-verity", sub_m)) => generate_verity(sub_m),
|
||||
Some(("verify", sub_m)) => verify(sub_m),
|
||||
Some(("sign-image", sub_m)) => sign_image(sub_m),
|
||||
Some(("genkeys", _)) => genkeys(),
|
||||
Some(("decompress", sub_m)) => decompress(sub_m),
|
||||
Some(("verify-shasum", sub_m)) => verify_shasum(sub_m),
|
||||
Some(("install-rootfs", sub_m)) => install_rootfs(sub_m),
|
||||
Some(("install", sub_m)) => install_image(sub_m),
|
||||
Some(("bless", _)) => bless(),
|
||||
_ => Ok(()),
|
||||
};
|
||||
|
||||
@@ -159,7 +167,9 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
let path = arg_matches.value_of("path").expect("path argument missing");
|
||||
let path = arg_matches.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
bail!("Cannot load image {}: File does not exist", path);
|
||||
}
|
||||
@@ -171,14 +181,14 @@ fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
}
|
||||
|
||||
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if arg_matches.is_present("choose") {
|
||||
if arg_matches.get_flag("choose") {
|
||||
let _ = choose_install_partition(true)?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
|
||||
if !arg_matches.is_present("skip-sha") {
|
||||
if !arg_matches.get_flag("skip-sha") {
|
||||
info!("Verifying sha256 hash of image");
|
||||
let shasum = img.generate_shasum()?;
|
||||
if shasum != img.metainfo().shasum() {
|
||||
@@ -188,7 +198,7 @@ fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
let partition = choose_install_partition(true)?;
|
||||
|
||||
if !arg_matches.is_present("no-prefer") {
|
||||
if !arg_matches.get_flag("no-prefer") {
|
||||
clear_prefer_boot()?;
|
||||
img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
|
||||
}
|
||||
@@ -212,7 +222,9 @@ fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let source = arg_matches.value_of("path").expect("path argument missing");
|
||||
let source = arg_matches.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
let _hdr = img.header();
|
||||
let metainfo = img.metainfo();
|
||||
@@ -238,9 +250,9 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if kernel_version.chars().any(|c| c == '/') {
|
||||
bail!("Kernel version field has / char");
|
||||
}
|
||||
format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
|
||||
format!("citadel-kernel-{}-{}.img", kernel_version, metainfo.version())
|
||||
} else {
|
||||
format!("citadel-extra-{:03}.img", metainfo.version())
|
||||
format!("citadel-extra-{}.img", metainfo.version())
|
||||
};
|
||||
|
||||
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
|
||||
|
@@ -125,7 +125,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
loop {
|
||||
println!("{}", prompt);
|
||||
println!();
|
||||
let passphrase = rpassword::read_password_from_tty(Some(" Passphrase : "))?;
|
||||
let passphrase = rpassword::prompt_password(" Passphrase : ")?;
|
||||
if passphrase.is_empty() {
|
||||
println!("Passphrase cannot be empty");
|
||||
continue;
|
||||
@@ -133,7 +133,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
if passphrase == "q" || passphrase == "Q" {
|
||||
return Ok(None);
|
||||
}
|
||||
let confirm = rpassword::read_password_from_tty(Some(" Confirm : "))?;
|
||||
let confirm = rpassword::prompt_password(" Confirm : ")?;
|
||||
if confirm == "q" || confirm == "Q" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ mod mkimage;
|
||||
mod realmfs;
|
||||
mod sync;
|
||||
mod update;
|
||||
mod fetch;
|
||||
|
||||
fn main() {
|
||||
let exe = match env::current_exe() {
|
||||
@@ -34,11 +35,13 @@ fn main() {
|
||||
} else if exe == Path::new("/usr/libexec/citadel-install-backend") {
|
||||
install_backend::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-image") {
|
||||
image::main(args);
|
||||
image::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
|
||||
realmfs::main(args);
|
||||
realmfs::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||
update::main(args);
|
||||
} else if exe == Path::new("/usr/bin/citadel-fetch") {
|
||||
fetch::main();
|
||||
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
||||
sync::main(args);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
||||
@@ -57,9 +60,10 @@ fn dispatch_command(args: Vec<String>) {
|
||||
match command.as_str() {
|
||||
"boot" => boot::main(rebuild_args("citadel-boot", args)),
|
||||
"install" => install::main(rebuild_args("citadel-install", args)),
|
||||
"image" => image::main(rebuild_args("citadel-image", args)),
|
||||
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
|
||||
"image" => image::main(),
|
||||
"realmfs" => realmfs::main(),
|
||||
"update" => update::main(rebuild_args("citadel-update", args)),
|
||||
"fetch" => update::main(rebuild_args("citadel-fetch", args)),
|
||||
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
|
||||
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
|
||||
"run" => do_citadel_run(rebuild_args("citadel-run", args)),
|
||||
|
@@ -38,15 +38,15 @@ impl UpdateBuilder {
|
||||
}
|
||||
|
||||
fn target_filename(&self) -> String {
|
||||
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||
format!("citadel-{}-{}-{}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||
}
|
||||
|
||||
fn build_filename(config: &BuildConfig) -> String {
|
||||
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
|
||||
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version())
|
||||
}
|
||||
|
||||
fn verity_filename(&self) -> String {
|
||||
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
|
||||
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version())
|
||||
}
|
||||
|
||||
pub fn build(&mut self) -> Result<()> {
|
||||
@@ -154,7 +154,7 @@ impl UpdateBuilder {
|
||||
bail!("failed to compress {:?}: {}", self.image(), err);
|
||||
}
|
||||
// Rename back to original image_data filename
|
||||
util::rename(self.image().with_extension("xz"), self.image())?;
|
||||
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -217,7 +217,7 @@ impl UpdateBuilder {
|
||||
writeln!(v, "realmfs-name = \"{}\"", name)?;
|
||||
}
|
||||
writeln!(v, "channel = \"{}\"", self.config.channel())?;
|
||||
writeln!(v, "version = {}", self.config.version())?;
|
||||
writeln!(v, "version = \"{}\"", self.config.version())?;
|
||||
writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?;
|
||||
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
|
||||
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;
|
||||
|
@@ -9,7 +9,7 @@ pub struct BuildConfig {
|
||||
#[serde(rename = "image-type")]
|
||||
image_type: String,
|
||||
channel: String,
|
||||
version: usize,
|
||||
version: String,
|
||||
timestamp: String,
|
||||
source: String,
|
||||
#[serde(default)]
|
||||
@@ -102,8 +102,8 @@ impl BuildConfig {
|
||||
self.realmfs_name.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn version(&self) -> usize {
|
||||
self.version
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn channel(&self) -> &str {
|
||||
|
@@ -1,28 +1,24 @@
|
||||
use clap::App;
|
||||
use clap::ArgMatches;
|
||||
use clap::{command, Command};
|
||||
use clap::{Arg, ArgMatches};
|
||||
|
||||
use libcitadel::{Result,RealmFS,Logger,LogLevel};
|
||||
use libcitadel::util::is_euid_root;
|
||||
use clap::SubCommand;
|
||||
use clap::AppSettings::*;
|
||||
use clap::Arg;
|
||||
use libcitadel::ResizeSize;
|
||||
use std::process::exit;
|
||||
|
||||
pub fn main(args: Vec<String>) {
|
||||
pub fn main() {
|
||||
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
let app = App::new("citadel-realmfs")
|
||||
.about("Citadel realmfs image tool")
|
||||
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs])
|
||||
|
||||
.subcommand(SubCommand::with_name("resize")
|
||||
let matches = command!()
|
||||
.about("citadel-realmfs")
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(Command::new("resize")
|
||||
.about("Resize an existing RealmFS image. If the image is currently sealed, it will also be unsealed.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to resize")
|
||||
.required(true))
|
||||
.arg(Arg::with_name("size")
|
||||
.arg(Arg::new("size")
|
||||
.help("Size to increase RealmFS image to (or by if prefixed with '+')")
|
||||
.long_help("\
|
||||
The size can be followed by a 'g' or 'm' character \
|
||||
@@ -35,53 +31,53 @@ is the final absolute size of the image.")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.subcommand(SubCommand::with_name("fork")
|
||||
.subcommand(Command::new("fork")
|
||||
.about("Create a new RealmFS image as an unsealed copy of an existing image")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to fork")
|
||||
.required(true))
|
||||
|
||||
.arg(Arg::with_name("forkname")
|
||||
.arg(Arg::new("forkname")
|
||||
.help("Name of new image to create")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("autoresize")
|
||||
.subcommand(Command::new("autoresize")
|
||||
.about("Increase size of RealmFS image if not enough free space remains")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("update")
|
||||
.subcommand(Command::new("update")
|
||||
.about("Open an update shell on the image")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("activate")
|
||||
.subcommand(Command::new("activate")
|
||||
.about("Activate a RealmFS by creating a block device for the image and mounting it.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to activate")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("deactivate")
|
||||
.subcommand(Command::new("deactivate")
|
||||
.about("Deactivate a RealmFS by unmounting it and removing block device created during activation.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to deactivate")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Name of or path to RealmFS image to display information about")
|
||||
.required(true));
|
||||
.required(true))
|
||||
.get_matches();
|
||||
|
||||
let matches = app.get_matches_from(args);
|
||||
let result = match matches.subcommand() {
|
||||
("resize", Some(m)) => resize(m),
|
||||
("autoresize", Some(m)) => autoresize(m),
|
||||
("fork", Some(m)) => fork(m),
|
||||
("update", Some(m)) => update(m),
|
||||
("activate", Some(m)) => activate(m),
|
||||
("deactivate", Some(m)) => deactivate(m),
|
||||
Some(("resize", m)) => resize(m),
|
||||
Some(("autoresize", m)) => autoresize(m),
|
||||
Some(("fork", m)) => fork(m),
|
||||
Some(("update", m)) => update(m),
|
||||
Some(("activate", m)) => activate(m),
|
||||
Some(("deactivate", m)) => deactivate(m),
|
||||
_ => image_info(&matches),
|
||||
};
|
||||
|
||||
@@ -92,7 +88,7 @@ is the final absolute size of the image.")
|
||||
}
|
||||
|
||||
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
|
||||
let image = match arg_matches.value_of("image") {
|
||||
let image = match arg_matches.get_one::<String>("image") {
|
||||
Some(s) => s,
|
||||
None => bail!("Image argument required."),
|
||||
};
|
||||
@@ -136,10 +132,9 @@ fn parse_resize_size(s: &str) -> Result<ResizeSize> {
|
||||
fn resize(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
info!("image is {}", img.path().display());
|
||||
let size_arg = match arg_matches.value_of("size") {
|
||||
let size_arg = match arg_matches.get_one::<String>("size") {
|
||||
Some(size) => size,
|
||||
None => "No size argument",
|
||||
|
||||
};
|
||||
info!("Size is {}", size_arg);
|
||||
let mode_add = size_arg.starts_with('+');
|
||||
@@ -165,7 +160,7 @@ fn autoresize(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
fn fork(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let forkname = match arg_matches.value_of("forkname") {
|
||||
let forkname = match arg_matches.get_one::<String>("forkname") {
|
||||
Some(name) => name,
|
||||
None => bail!("No fork name argument"),
|
||||
};
|
||||
@@ -190,7 +185,7 @@ fn update(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
fn activate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let img_arg = arg_matches.value_of("image").unwrap();
|
||||
let img_arg = arg_matches.get_one::<String>("image").unwrap();
|
||||
|
||||
if img.is_activated() {
|
||||
info!("RealmFS image {} is already activated", img_arg);
|
||||
@@ -203,7 +198,7 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
fn deactivate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let img_arg = arg_matches.value_of("image").unwrap();
|
||||
let img_arg = arg_matches.get_one::<String>("image").unwrap();
|
||||
if !img.is_activated() {
|
||||
info!("RealmFS image {} is not activated", img_arg);
|
||||
} else if img.is_in_use() {
|
||||
|
@@ -99,7 +99,7 @@ impl DesktopFileSync {
|
||||
}
|
||||
|
||||
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<()> {
|
||||
let mut directory = Realms::current_realm_symlink().join(directory.as_ref());
|
||||
let mut directory = self.realm.run_path().join(directory.as_ref());
|
||||
directory.push("share/applications");
|
||||
if directory.exists() {
|
||||
util::read_directory(&directory, |dent| {
|
||||
@@ -126,7 +126,11 @@ impl DesktopFileSync {
|
||||
}
|
||||
|
||||
fn remove_missing_target_files(&mut self) -> Result<()> {
|
||||
let sources = self.source_filenames();
|
||||
let mut sources = self.source_filenames();
|
||||
// If flatpak is enabled, don't remove the generated GNOME Software desktop file
|
||||
if self.realm.config().flatpak() {
|
||||
sources.insert(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
|
||||
}
|
||||
let prefix = format!("realm-{}.", self.realm.name());
|
||||
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
|
||||
if let Some(filename) = dent.file_name().to_str() {
|
||||
@@ -182,7 +186,9 @@ impl DesktopFileSync {
|
||||
|
||||
fn sync_item(&self, item: &DesktopItem) -> Result<()> {
|
||||
let mut dfp = DesktopFileParser::parse_from_path(&item.path, "/usr/libexec/citadel-run ")?;
|
||||
if dfp.is_showable() {
|
||||
// When use-flatpak is enabled a gnome-software desktop file will be generated
|
||||
let flatpak_gs_hide = dfp.filename() == "org.gnome.Software.desktop" && self.realm.config().flatpak();
|
||||
if dfp.is_showable() && !flatpak_gs_hide {
|
||||
self.sync_item_icon(&mut dfp);
|
||||
dfp.write_to_dir(Self::CITADEL_APPLICATIONS, Some(&self.realm))?;
|
||||
} else {
|
||||
|
@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use libcitadel::{Result, util, Realm};
|
||||
use std::cell::{RefCell, Cell};
|
||||
use std::fs;
|
||||
use crate::sync::desktop_file::DesktopFile;
|
||||
use crate::sync::REALM_BASE_PATHS;
|
||||
|
||||
|
@@ -14,7 +14,7 @@ fn has_arg(args: &[String], arg: &str) -> bool {
|
||||
|
||||
pub const REALM_BASE_PATHS:&[&str] = &[
|
||||
"rootfs/usr",
|
||||
"rootfs/var/lib/flatpak/exports",
|
||||
"flatpak/exports",
|
||||
"home/.local",
|
||||
"home/.local/share/flatpak/exports"
|
||||
];
|
||||
|
@@ -93,7 +93,7 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
if !path.exists() || path.file_name().is_none() {
|
||||
bail!("file path {} does not exist", path.display());
|
||||
}
|
||||
@@ -140,7 +140,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_extra_image(image: &ResourceImage) -> Result<()> {
|
||||
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
|
||||
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
|
||||
install_image_file(image, filename.as_str())?;
|
||||
remove_old_extra_images(image)?;
|
||||
Ok(())
|
||||
@@ -186,7 +186,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
|
||||
info!("kernel version is {}", kernel_version);
|
||||
install_kernel_file(image, &kernel_version)?;
|
||||
|
||||
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
|
||||
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version);
|
||||
install_image_file(image, &filename)?;
|
||||
|
||||
let all_versions = all_boot_kernel_versions()?;
|
||||
|
8
launch-gnome-software/Cargo.toml
Normal file
8
launch-gnome-software/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "launch-gnome-software"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
anyhow = "1.0"
|
67
launch-gnome-software/src/main.rs
Normal file
67
launch-gnome-software/src/main.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::env;
|
||||
|
||||
use libcitadel::{Logger, LogLevel, Realm, Realms, util};
|
||||
use libcitadel::flatpak::GnomeSoftwareLauncher;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
|
||||
fn realm_arg() -> Option<String> {
|
||||
let mut args = env::args();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--realm" {
|
||||
return args.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn choose_realm() -> Result<Realm> {
|
||||
let mut realms = Realms::load()?;
|
||||
if let Some(realm_name) = realm_arg() {
|
||||
match realms.by_name(&realm_name) {
|
||||
None => bail!("realm '{}' not found", realm_name),
|
||||
Some(realm) => return Ok(realm),
|
||||
}
|
||||
}
|
||||
let realm = match realms.current() {
|
||||
Some(realm) => realm,
|
||||
None => bail!("no current realm"),
|
||||
};
|
||||
|
||||
Ok(realm)
|
||||
}
|
||||
|
||||
fn has_arg(arg: &str) -> bool {
|
||||
env::args()
|
||||
.skip(1)
|
||||
.any(|s| s == arg)
|
||||
}
|
||||
|
||||
fn launch() -> Result<()> {
|
||||
let realm = choose_realm()?;
|
||||
if !util::is_euid_root() {
|
||||
bail!("Must be run with root euid");
|
||||
}
|
||||
let mut launcher = GnomeSoftwareLauncher::new(realm)?;
|
||||
|
||||
if has_arg("--quit") {
|
||||
launcher.quit()?;
|
||||
} else {
|
||||
if has_arg("--shell") {
|
||||
launcher.set_run_shell();
|
||||
}
|
||||
launcher.launch()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if has_arg("--verbose") {
|
||||
Logger::set_log_level(LogLevel::Verbose);
|
||||
}
|
||||
|
||||
if let Err(err) = launch() {
|
||||
eprintln!("Error: {}", err);
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ nix = "0.17.0"
|
||||
toml = "0.5"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "=1.0.1"
|
||||
lazy_static = "1.4"
|
||||
sodiumoxide = "0.2"
|
||||
hex = "0.4"
|
||||
@@ -19,6 +20,8 @@ walkdir = "2"
|
||||
dbus = "0.6"
|
||||
posix-acl = "1.0.0"
|
||||
procfs = "0.12.0"
|
||||
semver = "1.0"
|
||||
clap = "4.5"
|
||||
|
||||
[dependencies.inotify]
|
||||
version = "0.8"
|
||||
|
@@ -84,8 +84,8 @@ impl OsRelease {
|
||||
OsRelease::get_value("CITADEL_IMAGE_PUBKEY")
|
||||
}
|
||||
|
||||
pub fn citadel_rootfs_version() -> Option<usize> {
|
||||
OsRelease::get_int_value("CITADEL_ROOTFS_VERSION")
|
||||
pub fn citadel_rootfs_version() -> Option<&'static str> {
|
||||
OsRelease::get_value("CITADEL_ROOTFS_VERSION")
|
||||
}
|
||||
|
||||
pub fn citadel_kernel_version() -> Option<&'static str> {
|
||||
@@ -96,6 +96,14 @@ impl OsRelease {
|
||||
OsRelease::get_value("CITADEL_KERNEL_ID")
|
||||
}
|
||||
|
||||
pub fn citadel_client() -> Option<&'static str> {
|
||||
OsRelease::get_value("CITADEL_CLIENT")
|
||||
}
|
||||
|
||||
pub fn citadel_publisher() -> Option<&'static str> {
|
||||
OsRelease::get_value("CITADEL_PUBLISHER")
|
||||
}
|
||||
|
||||
fn _get_value(&self, key: &str) -> Option<&str> {
|
||||
self.vars.get(key).map(|v| v.as_str())
|
||||
}
|
||||
|
221
libcitadel/src/flatpak/bubblewrap.rs
Normal file
221
libcitadel/src/flatpak/bubblewrap.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, io};
|
||||
use std::fs::File;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{Logger, LogLevel, Result, verbose};
|
||||
|
||||
const BWRAP_PATH: &str = "/usr/libexec/flatpak-bwrap";
|
||||
|
||||
pub struct BubbleWrap {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl BubbleWrap {
|
||||
|
||||
pub fn new() -> Self {
|
||||
BubbleWrap {
|
||||
command: Command::new(BWRAP_PATH),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
|
||||
self.command.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.add_arg(arg.as_ref());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ro_bind(&mut self, path_list: &[&str]) -> &mut Self {
|
||||
for &path in path_list {
|
||||
self.add_args(&["--ro-bind", path, path]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ro_bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
|
||||
self.add_args(&["--ro-bind", src, dest])
|
||||
}
|
||||
|
||||
pub fn bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
|
||||
self.add_args(&["--bind", src, dest])
|
||||
}
|
||||
|
||||
pub fn create_dirs(&mut self, dir_list: &[&str]) -> &mut Self {
|
||||
for &dir in dir_list {
|
||||
self.add_args(&["--dir", dir]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create_symlinks(&mut self, links: &[(&str, &str)]) -> &mut Self {
|
||||
for (src,dest) in links {
|
||||
self.add_args(&["--symlink", src, dest]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount_dev(&mut self) -> &mut Self {
|
||||
self.add_args(&["--dev", "/dev"])
|
||||
}
|
||||
|
||||
pub fn mount_proc(&mut self) -> &mut Self {
|
||||
self.add_args(&["--proc", "/proc"])
|
||||
}
|
||||
|
||||
pub fn dev_bind(&mut self, path: &str) -> &mut Self {
|
||||
self.add_args(&["--dev-bind", path, path])
|
||||
}
|
||||
|
||||
pub fn clear_env(&mut self) -> &mut Self {
|
||||
self.add_arg("--clearenv")
|
||||
}
|
||||
|
||||
pub fn set_env_list(&mut self, env_list: &[&str]) -> &mut Self {
|
||||
for line in env_list {
|
||||
if let Some((k,v)) = line.split_once("=") {
|
||||
self.add_args(&["--setenv", k, v]);
|
||||
} else {
|
||||
eprintln!("Warning: environment variable '{}' does not have = delimiter. Ignoring", line);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unshare_all(&mut self) -> &mut Self {
|
||||
self.add_arg("--unshare-all")
|
||||
}
|
||||
|
||||
pub fn share_net(&mut self) -> &mut Self {
|
||||
self.add_arg("--share-net")
|
||||
}
|
||||
|
||||
pub fn log_command(&self) {
|
||||
let mut buffer = String::new();
|
||||
verbose!("{}", BWRAP_PATH);
|
||||
for arg in self.command.get_args() {
|
||||
if let Some(s) = arg.to_str() {
|
||||
if s.starts_with("-") {
|
||||
if !buffer.is_empty() {
|
||||
verbose!(" {}", buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
buffer.push(' ');
|
||||
}
|
||||
buffer.push_str(s);
|
||||
}
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
verbose!(" {}", buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_file(&mut self, status_file: &File) -> &mut Self {
|
||||
// Rust sets O_CLOEXEC when opening files so we create
|
||||
// a new descriptor that will remain open across exec()
|
||||
let dup_fd = unsafe {
|
||||
libc::dup(status_file.as_raw_fd())
|
||||
};
|
||||
if dup_fd == -1 {
|
||||
warn!("Failed to dup() status file descriptor: {}", io::Error::last_os_error());
|
||||
warn!("Skipping --json-status-fd argument");
|
||||
self
|
||||
} else {
|
||||
self.add_arg("--json-status-fd")
|
||||
.add_arg(dup_fd.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch<S: AsRef<OsStr>>(&mut self, cmd: &[S]) -> Result<()> {
|
||||
if Logger::is_log_level(LogLevel::Verbose) {
|
||||
self.log_command();
|
||||
let s = cmd.iter().map(|s| format!("{} ", s.as_ref().to_str().unwrap())).collect::<String>();
|
||||
verbose!(" {}", s)
|
||||
}
|
||||
self.add_args(cmd);
|
||||
let err = self.command.exec();
|
||||
bail!("failed to exec bubblewrap: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub struct BubbleWrapRunningStatus {
|
||||
pub child_pid: u64,
|
||||
pub cgroup_namespace: u64,
|
||||
pub ipc_namespace: u64,
|
||||
pub mnt_namespace: u64,
|
||||
pub pid_namespace: u64,
|
||||
pub uts_namespace: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub struct BubbleWrapExitStatus {
|
||||
pub exit_code: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BubbleWrapStatus {
|
||||
running: BubbleWrapRunningStatus,
|
||||
exit: Option<BubbleWrapExitStatus>,
|
||||
}
|
||||
|
||||
impl BubbleWrapStatus {
|
||||
pub fn parse_file(path: impl AsRef<Path>) -> Result<Option<Self>> {
|
||||
if !path.as_ref().exists() {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(path)
|
||||
.map_err(context!("error reading status file"))?;
|
||||
|
||||
let mut lines = s.lines();
|
||||
let running = match lines.next() {
|
||||
None => return Ok(None),
|
||||
Some(s) => serde_json::from_str::<BubbleWrapRunningStatus>(s)
|
||||
.map_err(context!("failed to parse status line ({})", s))?
|
||||
};
|
||||
|
||||
let exit = match lines.next() {
|
||||
None => None,
|
||||
Some(s) => Some(serde_json::from_str::<BubbleWrapExitStatus>(s)
|
||||
.map_err(context!("failed to parse exit line ({})", s))?)
|
||||
};
|
||||
|
||||
Ok(Some(BubbleWrapStatus {
|
||||
running,
|
||||
exit
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.exit.is_none()
|
||||
}
|
||||
|
||||
pub fn running_status(&self) -> &BubbleWrapRunningStatus {
|
||||
&self.running
|
||||
}
|
||||
|
||||
pub fn child_pid(&self) -> u64 {
|
||||
self.running.child_pid
|
||||
}
|
||||
|
||||
pub fn pid_namespace(&self) -> u64 {
|
||||
self.running.pid_namespace
|
||||
}
|
||||
|
||||
pub fn exit_status(&self) -> Option<&BubbleWrapExitStatus> {
|
||||
self.exit.as_ref()
|
||||
}
|
||||
}
|
259
libcitadel/src/flatpak/launcher.rs
Normal file
259
libcitadel/src/flatpak/launcher.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use std::{fs, io};
|
||||
use std::fs::File;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::prelude::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{Realm, Result, util};
|
||||
use crate::flatpak::{BubbleWrap, BubbleWrapStatus, SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
|
||||
use crate::flatpak::netns::NetNS;
|
||||
|
||||
|
||||
const FLATPAK_PATH: &str = "/usr/bin/flatpak";
|
||||
|
||||
const ENVIRONMENT: &[&str; 7] = &[
|
||||
"HOME=/home/citadel",
|
||||
"USER=citadel",
|
||||
"XDG_RUNTIME_DIR=/run/user/1000",
|
||||
"XDG_DATA_DIRS=/home/citadel/.local/share/flatpak/exports/share:/usr/share",
|
||||
"TERM=xterm-256color",
|
||||
"GTK_A11Y=none",
|
||||
"FLATPAK_USER_DIR=/home/citadel/realm-flatpak",
|
||||
];
|
||||
|
||||
const FLATHUB_URL: &str = "https://dl.flathub.org/repo/flathub.flatpakrepo";
|
||||
|
||||
pub struct GnomeSoftwareLauncher {
|
||||
realm: Realm,
|
||||
status: Option<BubbleWrapStatus>,
|
||||
netns: NetNS,
|
||||
run_shell: bool,
|
||||
}
|
||||
impl GnomeSoftwareLauncher {
|
||||
|
||||
pub fn new(realm: Realm) -> Result<Self> {
|
||||
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
|
||||
let status = sandbox_status.realm_status(&realm);
|
||||
let netns = NetNS::new(NetNS::GS_NETNS_NAME);
|
||||
Ok(GnomeSoftwareLauncher {
|
||||
realm,
|
||||
status,
|
||||
netns,
|
||||
run_shell: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_run_shell(&mut self) {
|
||||
self.run_shell = true;
|
||||
}
|
||||
|
||||
fn ensure_flatpak_dir(&self) -> Result<()> {
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
if !flatpak_user_dir.exists() {
|
||||
if let Err(err) = fs::create_dir(&flatpak_user_dir) {
|
||||
bail!("failed to create realm flatpak directory ({}): {}", flatpak_user_dir.display(), err);
|
||||
}
|
||||
util::chown_user(&flatpak_user_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flathub(&self) -> Result<()> {
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
|
||||
match Command::new(FLATPAK_PATH)
|
||||
.env("FLATPAK_USER_DIR", flatpak_user_dir)
|
||||
.arg("remote-add")
|
||||
.arg("--user")
|
||||
.arg("--if-not-exists")
|
||||
.arg("flathub")
|
||||
.arg(FLATHUB_URL)
|
||||
.status() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
|
||||
} else {
|
||||
bail!("failed to add flathub repo")
|
||||
}
|
||||
},
|
||||
Err(err) => bail!("error running flatpak command: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_tmp_directory(path: &Path) -> io::Result<Option<String>> {
|
||||
for entry in fs::read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_socket() {
|
||||
if let Some(filename) = entry.path().file_name() {
|
||||
if let Some(filename) = filename.to_str() {
|
||||
if filename.starts_with("dbus-") {
|
||||
return Ok(Some(format!("/tmp/{}", filename)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn find_dbus_socket(&self) -> Result<String> {
|
||||
let pid = self.running_pid()?;
|
||||
let tmp_dir = PathBuf::from(format!("/proc/{}/root/tmp", pid));
|
||||
if !tmp_dir.is_dir() {
|
||||
bail!("no /tmp directory found for process pid={}", pid);
|
||||
}
|
||||
|
||||
if let Some(s) = Self::scan_tmp_directory(&tmp_dir)
|
||||
.map_err(context!("error reading directory {}", tmp_dir.display()))? {
|
||||
Ok(s)
|
||||
|
||||
} else {
|
||||
bail!("no dbus socket found in /tmp directory for process pid={}", pid);
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_sandbox(&self, status_file: &File) -> Result<()> {
|
||||
self.ensure_flatpak_dir()?;
|
||||
if let Err(err) = self.netns.nsenter() {
|
||||
bail!("Failed to enter 'gnome-software' network namespace: {}", err);
|
||||
}
|
||||
verbose!("Entered network namespace ({})", NetNS::GS_NETNS_NAME);
|
||||
|
||||
if let Err(err) = util::drop_privileges(1000, 1000) {
|
||||
bail!("Failed to drop privileges to uid = gid = 1000: {}", err);
|
||||
}
|
||||
verbose!("Dropped privileges (uid=1000, gid=1000)");
|
||||
|
||||
self.add_flathub()?;
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
let flatpak_user_dir = flatpak_user_dir.to_str().unwrap();
|
||||
|
||||
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
|
||||
verbose!("Running command in sandbox: {}", cmd);
|
||||
|
||||
BubbleWrap::new()
|
||||
.ro_bind(&[
|
||||
"/usr/bin",
|
||||
"/usr/lib",
|
||||
"/usr/libexec",
|
||||
"/usr/share/dbus-1",
|
||||
"/usr/share/icons",
|
||||
"/usr/share/mime",
|
||||
"/usr/share/X11",
|
||||
"/usr/share/glib-2.0",
|
||||
"/usr/share/xml",
|
||||
"/usr/share/drirc.d",
|
||||
"/usr/share/fontconfig",
|
||||
"/usr/share/fonts",
|
||||
"/usr/share/zoneinfo",
|
||||
"/usr/share/swcatalog",
|
||||
|
||||
"/etc/passwd",
|
||||
"/etc/machine-id",
|
||||
"/etc/nsswitch.conf",
|
||||
"/etc/fonts",
|
||||
"/etc/ssl",
|
||||
"/sys/dev/char", "/sys/devices",
|
||||
"/run/user/1000/wayland-0",
|
||||
])
|
||||
.ro_bind_to("/run/NetworkManager/resolv.conf", "/etc/resolv.conf")
|
||||
.bind_to(flatpak_user_dir, "/home/citadel/realm-flatpak")
|
||||
.create_symlinks(&[
|
||||
("usr/lib", "/lib64"),
|
||||
("usr/bin", "/bin"),
|
||||
("/tmp", "/var/tmp"),
|
||||
])
|
||||
.create_dirs(&[
|
||||
"/var/lib/flatpak",
|
||||
"/home/citadel",
|
||||
"/tmp",
|
||||
"/sys/block", "/sys/bus", "/sys/class",
|
||||
])
|
||||
|
||||
|
||||
.mount_dev()
|
||||
.dev_bind("/dev/dri")
|
||||
.mount_proc()
|
||||
.unshare_all()
|
||||
.share_net()
|
||||
|
||||
.clear_env()
|
||||
.set_env_list(ENVIRONMENT)
|
||||
|
||||
.status_file(status_file)
|
||||
|
||||
.launch(&["dbus-run-session", "--", cmd])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_realm_status_file(&self) -> Result<File> {
|
||||
let path = Path::new(SANDBOX_STATUS_FILE_DIRECTORY).join(self.realm.name());
|
||||
File::create(&path)
|
||||
.map_err(context!("failed to open sandbox status file {}", path.display()))
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> Result<()> {
|
||||
self.netns.ensure_exists()?;
|
||||
if self.is_running() {
|
||||
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
|
||||
self.launch_in_running_sandbox(&[cmd])?;
|
||||
} else {
|
||||
let status_file = self.new_realm_status_file()?;
|
||||
self.ensure_flatpak_dir()?;
|
||||
self.launch_sandbox(&status_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&self) -> Result<()> {
|
||||
if self.is_running() {
|
||||
self.launch_in_running_sandbox(&["/usr/bin/gnome-software", "--quit"])?;
|
||||
} else {
|
||||
warn!("No running sandbox found for realm {}", self.realm.name());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.status.as_ref()
|
||||
.map(|s| s.is_running())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn running_pid(&self) -> Result<u64> {
|
||||
self.status.as_ref()
|
||||
.map(|s| s.child_pid())
|
||||
.ok_or(format_err!("no sandbox status available for realm '{}',", self.realm.name()))
|
||||
}
|
||||
|
||||
fn dbus_session_address(&self) -> Result<String> {
|
||||
let dbus_socket = Self::find_dbus_socket(&self)?;
|
||||
Ok(format!("unix:path={}", dbus_socket))
|
||||
}
|
||||
|
||||
fn launch_in_running_sandbox(&self, command: &[&str]) -> Result<()> {
|
||||
let dbus_address = self.dbus_session_address()?;
|
||||
|
||||
let pid = self.running_pid()?.to_string();
|
||||
let mut env = ENVIRONMENT.iter()
|
||||
.map(|s| s.split_once('=').unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
env.push(("DBUS_SESSION_BUS_ADDRESS", dbus_address.as_str()));
|
||||
|
||||
let err = Command::new("/usr/bin/nsenter")
|
||||
.env_clear()
|
||||
.envs( env )
|
||||
.args(&[
|
||||
"--all",
|
||||
"--target", pid.as_str(),
|
||||
"--setuid", "1000",
|
||||
"--setgid", "1000",
|
||||
])
|
||||
.args(command)
|
||||
.exec();
|
||||
|
||||
Err(format_err!("failed to execute nsenter: {}", err))
|
||||
}
|
||||
}
|
16
libcitadel/src/flatpak/mod.rs
Normal file
16
libcitadel/src/flatpak/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
pub(crate) mod setup;
|
||||
pub(crate) mod status;
|
||||
|
||||
pub(crate) mod bubblewrap;
|
||||
|
||||
pub(crate) mod launcher;
|
||||
|
||||
pub(crate) mod netns;
|
||||
|
||||
pub use status::SandboxStatus;
|
||||
|
||||
pub use bubblewrap::{BubbleWrap,BubbleWrapStatus,BubbleWrapRunningStatus,BubbleWrapExitStatus};
|
||||
pub use launcher::GnomeSoftwareLauncher;
|
||||
|
||||
pub const SANDBOX_STATUS_FILE_DIRECTORY: &str = "/run/citadel/realms/gs-sandbox-status";
|
132
libcitadel/src/flatpak/netns.rs
Normal file
132
libcitadel/src/flatpak/netns.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{util,Result};
|
||||
|
||||
|
||||
const BRIDGE_NAME: &str = "vz-clear";
|
||||
const VETH0: &str = "gs-veth0";
|
||||
const VETH1: &str = "gs-veth1";
|
||||
const IP_ADDRESS: &str = "172.17.0.222/24";
|
||||
const GW_ADDRESS: &str = "172.17.0.1";
|
||||
|
||||
|
||||
pub struct NetNS {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl NetNS {
|
||||
pub const GS_NETNS_NAME: &'static str = "gnome-software";
|
||||
pub fn new(name: &str) -> Self {
|
||||
NetNS {
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create(&self) -> crate::Result<()> {
|
||||
Ip::new().link_add_veth(VETH0, VETH1).run()?;
|
||||
Ip::new().link_set_netns(VETH0, &self.name).run()?;
|
||||
Ip::new().link_set_master(VETH1, BRIDGE_NAME).run()?;
|
||||
Ip::new().link_set_dev_up(VETH1).run()?;
|
||||
|
||||
Ip::new().ip_netns_exec_ip(&self.name).addr_add(IP_ADDRESS, VETH0).run()?;
|
||||
Ip::new().ip_netns_exec_ip(&self.name).link_set_dev_up(VETH0).run()?;
|
||||
Ip::new().ip_netns_exec_ip(&self.name).route_add_default(GW_ADDRESS).run()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn ensure_exists(&self) -> Result<()> {
|
||||
if Path::new(&format!("/run/netns/{}", self.name)).exists() {
|
||||
verbose!("Network namespace ({}) exists", self.name);
|
||||
return Ok(())
|
||||
}
|
||||
verbose!("Setting up network namespace ({})", self.name);
|
||||
|
||||
Ip::new().netns_add(&self.name).run()
|
||||
.map_err(context!("Failed to add network namespace '{}'", self.name))?;
|
||||
|
||||
if let Err(err) = self.create() {
|
||||
Ip::new().netns_delete(&self.name).run()?;
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nsenter(&self) -> Result<()> {
|
||||
util::nsenter_netns(&self.name)
|
||||
}
|
||||
}
|
||||
const IP_PATH: &str = "/usr/sbin/ip";
|
||||
struct Ip {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl Ip {
|
||||
|
||||
fn new() -> Self {
|
||||
let mut command = Command::new(IP_PATH);
|
||||
command.env_clear();
|
||||
Ip { command }
|
||||
}
|
||||
|
||||
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.command.arg(arg);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn netns_add(&mut self, name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "add", name])
|
||||
}
|
||||
|
||||
pub fn netns_delete(&mut self, name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "delete", name])
|
||||
}
|
||||
|
||||
pub fn link_add_veth(&mut self, name: &str, peer_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "add", name, "type", "veth", "peer", "name", peer_name])
|
||||
}
|
||||
|
||||
pub fn link_set_netns(&mut self, iface: &str, netns_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", iface, "netns", netns_name])
|
||||
}
|
||||
|
||||
pub fn link_set_master(&mut self, iface: &str, bridge_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", iface, "master", bridge_name])
|
||||
}
|
||||
|
||||
pub fn link_set_dev_up(&mut self, iface: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", "dev", iface, "up"])
|
||||
}
|
||||
|
||||
pub fn ip_netns_exec_ip(&mut self, netns_name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "exec", netns_name, IP_PATH])
|
||||
}
|
||||
|
||||
pub fn addr_add(&mut self, ip_address: &str, dev: &str) -> &mut Self {
|
||||
self.add_args(&["addr", "add", ip_address, "dev", dev])
|
||||
}
|
||||
|
||||
pub fn route_add_default(&mut self, gateway: &str) -> &mut Self {
|
||||
self.add_args(&["route", "add", "default", "via", gateway])
|
||||
}
|
||||
|
||||
fn run(&mut self) -> crate::Result<()> {
|
||||
verbose!("{:?}", self.command);
|
||||
match self.command.status() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("IP command ({:?}) did not succeeed.", self.command);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("error running ip command ({:?}): {}", self.command, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
libcitadel/src/flatpak/setup.rs
Normal file
58
libcitadel/src/flatpak/setup.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::path::Path;
|
||||
use crate::{Realm, Result, util};
|
||||
|
||||
const GNOME_SOFTWARE_DESKTOP_TEMPLATE: &str = "\
|
||||
[Desktop Entry]
|
||||
Name=Software
|
||||
Comment=Add, remove or update software on this computer
|
||||
Icon=org.gnome.Software
|
||||
Exec=/usr/libexec/launch-gnome-software --realm $REALM_NAME
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=GNOME;GTK;System;PackageManager;
|
||||
Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store;
|
||||
StartupNotify=true
|
||||
";
|
||||
|
||||
const APPLICATION_DIRECTORY: &str = "/home/citadel/.local/share/applications";
|
||||
|
||||
pub struct FlatpakSetup<'a> {
|
||||
realm: &'a Realm,
|
||||
}
|
||||
|
||||
impl <'a> FlatpakSetup<'a> {
|
||||
|
||||
pub fn new(realm: &'a Realm) -> Self {
|
||||
Self { realm }
|
||||
}
|
||||
|
||||
pub fn setup(&self) -> Result<()> {
|
||||
self.write_desktop_file()?;
|
||||
self.ensure_flatpak_directory()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn write_desktop_file(&self) -> Result<()> {
|
||||
let appdir = Path::new(APPLICATION_DIRECTORY);
|
||||
if !appdir.exists() {
|
||||
util::create_dir(appdir)?;
|
||||
if let Some(parent) = appdir.parent().and_then(|p| p.parent()) {
|
||||
util::chown_tree(parent, (1000,1000), true)?;
|
||||
}
|
||||
}
|
||||
let path = appdir.join(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
|
||||
util::write_file(path, GNOME_SOFTWARE_DESKTOP_TEMPLATE.replace("$REALM_NAME", self.realm.name()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_flatpak_directory(&self) -> Result<()> {
|
||||
let path = self.realm.base_path_file("flatpak");
|
||||
if !path.exists() {
|
||||
util::create_dir(&path)?;
|
||||
util::chown_user(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
144
libcitadel/src/flatpak/status.rs
Normal file
144
libcitadel/src/flatpak/status.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
use crate::flatpak::bubblewrap::BubbleWrapStatus;
|
||||
use crate::{Realm, Result, util};
|
||||
|
||||
|
||||
/// Utility function to read modified time from a path.
|
||||
fn modified_time(path: &Path) -> Result<SystemTime> {
|
||||
path.metadata().and_then(|meta| meta.modified())
|
||||
.map_err(context!("failed to read modified time from '{}'", path.display()))
|
||||
}
|
||||
|
||||
/// Utility function to detect if current modified time of a path
|
||||
/// matches an earlier recorded modified time.
|
||||
fn modified_changed(path: &Path, old_modified: SystemTime) -> bool {
|
||||
if !path.exists() {
|
||||
// Path existed at some earlier point, so something changed
|
||||
return true;
|
||||
}
|
||||
|
||||
match modified_time(path) {
|
||||
Ok(modified) => old_modified != modified,
|
||||
Err(err) => {
|
||||
// Print a warning but assume change
|
||||
warn!("{}", err);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the content of single entry in a sandbox status directory.
|
||||
///
|
||||
/// The path to the status file as well as the last modified time are
|
||||
/// recorded so that changes in status of a sandbox can be detected.
|
||||
struct StatusEntry {
|
||||
status: BubbleWrapStatus,
|
||||
path: PathBuf,
|
||||
modified: SystemTime,
|
||||
}
|
||||
|
||||
|
||||
impl StatusEntry {
|
||||
|
||||
fn load_timestamp_and_status(path: &Path) -> Result<Option<(SystemTime, BubbleWrapStatus)>> {
|
||||
if path.exists() {
|
||||
let modified = modified_time(path)?;
|
||||
if let Some(status) = BubbleWrapStatus::parse_file(path)? {
|
||||
return Ok(Some((modified, status)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn load(base_dir: &Path, name: &str) -> Result<Option<Self>> {
|
||||
let path = base_dir.join(name);
|
||||
let result = StatusEntry::load_timestamp_and_status(&path)?
|
||||
.map(|(modified, status)| StatusEntry { status, path, modified });
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn is_modified(&self) -> bool {
|
||||
modified_changed(&self.path, self.modified)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Holds information about entries in a sandbox status directory.
|
||||
///
|
||||
/// Bubblewrap accepts a command line argument that asks for status
|
||||
/// information to be written as a json structure to a file descriptor.
|
||||
///
|
||||
pub struct SandboxStatus {
|
||||
base_dir: PathBuf,
|
||||
base_modified: SystemTime,
|
||||
entries: HashMap<String, StatusEntry>,
|
||||
}
|
||||
|
||||
impl SandboxStatus {
|
||||
|
||||
pub fn need_reload(&self) -> bool {
|
||||
if modified_changed(&self.base_dir, self.base_modified) {
|
||||
return true;
|
||||
}
|
||||
self.entries.values().any(|entry| entry.is_modified())
|
||||
}
|
||||
|
||||
fn process_dir_entry(&mut self, dir_entry: PathBuf) -> Result<()> {
|
||||
fn realm_name_for_path(path: &Path) -> Option<&str> {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| Realm::is_valid_name(name))
|
||||
|
||||
}
|
||||
if dir_entry.is_file() {
|
||||
if let Some(name) = realm_name_for_path(&dir_entry) {
|
||||
if let Some(entry) = StatusEntry::load(&self.base_dir, name)? {
|
||||
self.entries.insert(name.to_string(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reload(&mut self) -> Result<()> {
|
||||
self.entries.clear();
|
||||
self.base_modified = modified_time(&self.base_dir)?;
|
||||
let base_dir = self.base_dir.clone();
|
||||
util::read_directory(&base_dir, |entry| {
|
||||
self.process_dir_entry(entry.path())
|
||||
})
|
||||
}
|
||||
|
||||
fn new(base_dir: &Path) -> Result<Self> {
|
||||
let base_dir = base_dir.to_owned();
|
||||
let base_modified = modified_time(&base_dir)?;
|
||||
Ok(SandboxStatus {
|
||||
base_dir,
|
||||
base_modified,
|
||||
entries: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(directory: impl AsRef<Path>) -> Result<SandboxStatus> {
|
||||
let base_dir = directory.as_ref();
|
||||
if !base_dir.exists() {
|
||||
util::create_dir(base_dir)?;
|
||||
}
|
||||
let mut status = SandboxStatus::new(base_dir)?;
|
||||
status.reload()?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn realm_status(&self, realm: &Realm) -> Option<BubbleWrapStatus> {
|
||||
self.entries.get(realm.name()).map(|entry| entry.status.clone())
|
||||
}
|
||||
|
||||
pub fn new_realm_status_file(&self, realm: &Realm) -> Result<File> {
|
||||
let path = self.base_dir.join(realm.name());
|
||||
File::create(&path)
|
||||
.map_err(context!("failed to open sandbox status file {}", path.display()))
|
||||
}
|
||||
}
|
@@ -453,7 +453,7 @@ pub struct MetaInfo {
|
||||
realmfs_owner: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
version: u32,
|
||||
version: String,
|
||||
|
||||
#[serde(default)]
|
||||
timestamp: String,
|
||||
@@ -508,8 +508,8 @@ impl MetaInfo {
|
||||
Self::str_ref(&self.realmfs_owner)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> u32 {
|
||||
self.version
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> &str {
|
||||
|
@@ -20,6 +20,9 @@ pub mod symlink;
|
||||
mod realm;
|
||||
pub mod terminal;
|
||||
mod system;
|
||||
pub mod updates;
|
||||
|
||||
pub mod flatpak;
|
||||
|
||||
pub use crate::config::OsRelease;
|
||||
pub use crate::blockdev::BlockDev;
|
||||
|
@@ -62,6 +62,11 @@ impl Logger {
|
||||
logger.level = level;
|
||||
}
|
||||
|
||||
pub fn is_log_level(level: LogLevel) -> bool {
|
||||
let logger = LOGGER.lock().unwrap();
|
||||
logger.level >= level
|
||||
}
|
||||
|
||||
pub fn set_log_output(output: Box<dyn LogOutput>) {
|
||||
let mut logger = LOGGER.lock().unwrap();
|
||||
logger.output = output;
|
||||
|
@@ -77,6 +77,9 @@ pub struct RealmConfig {
|
||||
#[serde(rename="use-fuse")]
|
||||
pub use_fuse: Option<bool>,
|
||||
|
||||
#[serde(rename="use-flatpak")]
|
||||
pub use_flatpak: Option<bool>,
|
||||
|
||||
#[serde(rename="use-gpu")]
|
||||
pub use_gpu: Option<bool>,
|
||||
|
||||
@@ -201,6 +204,7 @@ impl RealmConfig {
|
||||
wayland_socket: Some("wayland-0".to_string()),
|
||||
use_kvm: Some(false),
|
||||
use_fuse: Some(false),
|
||||
use_flatpak: Some(false),
|
||||
use_gpu: Some(false),
|
||||
use_gpu_card0: Some(false),
|
||||
use_network: Some(true),
|
||||
@@ -233,6 +237,7 @@ impl RealmConfig {
|
||||
wayland_socket: None,
|
||||
use_kvm: None,
|
||||
use_fuse: None,
|
||||
use_flatpak: None,
|
||||
use_gpu: None,
|
||||
use_gpu_card0: None,
|
||||
use_network: None,
|
||||
@@ -267,6 +272,14 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_fuse)
|
||||
}
|
||||
|
||||
/// If `true` flatpak directory will be mounted into realm
|
||||
/// and a desktop file will be created to launch gnome-software
|
||||
///
|
||||
pub fn flatpak(&self) -> bool {
|
||||
self.bool_value(|c| c.use_flatpak)
|
||||
}
|
||||
|
||||
|
||||
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
|
||||
///
|
||||
/// This enables hardware graphics acceleration in realm.
|
||||
|
@@ -60,7 +60,7 @@ impl <'a> RealmLauncher <'a> {
|
||||
if config.kvm() {
|
||||
self.add_device("/dev/kvm");
|
||||
}
|
||||
if config.fuse() {
|
||||
if config.fuse() || config.flatpak() {
|
||||
self.add_device("/dev/fuse");
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ impl <'a> RealmLauncher <'a> {
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/{}:/run/user/host/wayland-0", config.wayland_socket())?;
|
||||
}
|
||||
|
||||
if config.flatpak() {
|
||||
writeln!(s, "BindReadOnly={}:/var/lib/flatpak", self.realm.base_path_file("flatpak").display())?;
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts() {
|
||||
if Self::is_valid_bind_item(bind) {
|
||||
writeln!(s, "Bind={}", bind)?;
|
||||
|
@@ -4,6 +4,8 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use posix_acl::{ACL_EXECUTE, ACL_READ, PosixACL, Qualifier};
|
||||
|
||||
use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util};
|
||||
use crate::flatpak::GnomeSoftwareLauncher;
|
||||
use crate::flatpak::setup::FlatpakSetup;
|
||||
use crate::realm::pidmapper::{PidLookupResult, PidMapper};
|
||||
use crate::realmfs::realmfs_set::RealmFSSet;
|
||||
|
||||
@@ -21,6 +23,7 @@ struct Inner {
|
||||
events: RealmEventListener,
|
||||
realms: Realms,
|
||||
realmfs_set: RealmFSSet,
|
||||
pid_mapper: PidMapper,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
@@ -28,7 +31,8 @@ impl Inner {
|
||||
let events = RealmEventListener::new();
|
||||
let realms = Realms::load()?;
|
||||
let realmfs_set = RealmFSSet::load()?;
|
||||
Ok(Inner { events, realms, realmfs_set })
|
||||
let pid_mapper = PidMapper::new()?;
|
||||
Ok(Inner { events, realms, realmfs_set, pid_mapper })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +234,10 @@ impl RealmManager {
|
||||
self.ensure_run_media_directory()?;
|
||||
}
|
||||
|
||||
if realm.config().flatpak() {
|
||||
FlatpakSetup::new(realm).setup()?;
|
||||
}
|
||||
|
||||
self.systemd.start_realm(realm, &rootfs)?;
|
||||
|
||||
self.create_realm_namefile(realm)?;
|
||||
@@ -268,6 +276,15 @@ impl RealmManager {
|
||||
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false)
|
||||
}
|
||||
|
||||
fn stop_gnome_software_sandbox(&self, realm: &Realm) -> Result<()> {
|
||||
let launcher = GnomeSoftwareLauncher::new(realm.clone())?;
|
||||
if launcher.is_running() {
|
||||
info!("Stopping GNOME Software sandbox for {}", realm.name());
|
||||
launcher.quit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
|
||||
if !realm.is_active() {
|
||||
info!("ignoring stop request on realm '{}' which is not running", realm.name());
|
||||
@@ -276,6 +293,12 @@ impl RealmManager {
|
||||
|
||||
info!("Stopping realm {}", realm.name());
|
||||
|
||||
if realm.config().flatpak() {
|
||||
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
|
||||
warn!("Error stopping GNOME Software sandbox: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
realm.set_active(false);
|
||||
self.systemd.stop_realm(realm)?;
|
||||
realm.cleanup_rootfs();
|
||||
@@ -335,8 +358,8 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult {
|
||||
let mapper = PidMapper::new(self.active_realms(false));
|
||||
mapper.lookup_pid(pid as libc::pid_t)
|
||||
let realms = self.realm_list();
|
||||
self.inner_mut().pid_mapper.lookup_pid(pid as libc::pid_t, realms)
|
||||
}
|
||||
|
||||
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use std::ffi::OsStr;
|
||||
use procfs::process::Process;
|
||||
use crate::Realm;
|
||||
use crate::{Result, Realm};
|
||||
use crate::flatpak::{SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
|
||||
|
||||
pub enum PidLookupResult {
|
||||
Unknown,
|
||||
@@ -9,14 +10,15 @@ pub enum PidLookupResult {
|
||||
}
|
||||
|
||||
pub struct PidMapper {
|
||||
active_realms: Vec<Realm>,
|
||||
sandbox_status: SandboxStatus,
|
||||
my_pid_ns_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl PidMapper {
|
||||
pub fn new(active_realms: Vec<Realm>) -> Self {
|
||||
pub fn new() -> Result<Self> {
|
||||
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
|
||||
let my_pid_ns_id = Self::self_pid_namespace_id();
|
||||
PidMapper { active_realms, my_pid_ns_id }
|
||||
Ok(PidMapper { sandbox_status, my_pid_ns_id })
|
||||
}
|
||||
|
||||
fn read_process(pid: libc::pid_t) -> Option<Process> {
|
||||
@@ -72,7 +74,30 @@ impl PidMapper {
|
||||
Self::read_process(ppid)
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&self, pid: libc::pid_t) -> PidLookupResult {
|
||||
fn refresh_sandbox_status(&mut self) -> Result<()> {
|
||||
if self.sandbox_status.need_reload() {
|
||||
self.sandbox_status.reload()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn search_sandbox_realms(&mut self, pid_ns: u64, realms: &[Realm]) -> Option<Realm> {
|
||||
if let Err(err) = self.refresh_sandbox_status() {
|
||||
warn!("error reloading sandbox status directory: {}", err);
|
||||
return None;
|
||||
}
|
||||
|
||||
for r in realms {
|
||||
if let Some(status) = self.sandbox_status.realm_status(r) {
|
||||
if status.pid_namespace() == pid_ns {
|
||||
return Some(r.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&mut self, pid: libc::pid_t, realms: Vec<Realm>) -> PidLookupResult {
|
||||
const MAX_PARENT_SEARCH: i32 = 8;
|
||||
let mut n = 0;
|
||||
|
||||
@@ -92,13 +117,17 @@ impl PidMapper {
|
||||
return PidLookupResult::Citadel;
|
||||
}
|
||||
|
||||
if let Some(realm) = self.active_realms.iter()
|
||||
.find(|r| r.has_pid_ns(pid_ns_id))
|
||||
if let Some(realm) = realms.iter()
|
||||
.find(|r| r.is_active() && r.has_pid_ns(pid_ns_id))
|
||||
.cloned()
|
||||
{
|
||||
return PidLookupResult::Realm(realm)
|
||||
}
|
||||
|
||||
if let Some(r) = self.search_sandbox_realms(pid_ns_id, &realms) {
|
||||
return PidLookupResult::Realm(r)
|
||||
}
|
||||
|
||||
proc = match Self::parent_process(proc) {
|
||||
Some(proc) => proc,
|
||||
None => return PidLookupResult::Unknown,
|
||||
@@ -108,5 +137,4 @@ impl PidMapper {
|
||||
}
|
||||
PidLookupResult::Unknown
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -279,6 +279,9 @@ impl Realm {
|
||||
symlink::write(&rootfs, self.rootfs_symlink(), false)?;
|
||||
symlink::write(mountpoint.path(), self.realmfs_mountpoint_symlink(), false)?;
|
||||
symlink::write(self.base_path().join("home"), self.run_path().join("home"), false)?;
|
||||
if self.config().flatpak() {
|
||||
symlink::write(self.base_path().join("flatpak"), self.run_path().join("flatpak"), false)?;
|
||||
}
|
||||
|
||||
Ok(rootfs)
|
||||
}
|
||||
@@ -300,6 +303,9 @@ impl Realm {
|
||||
Self::remove_symlink(self.realmfs_mountpoint_symlink());
|
||||
Self::remove_symlink(self.rootfs_symlink());
|
||||
Self::remove_symlink(self.run_path().join("home"));
|
||||
if self.config().flatpak() {
|
||||
Self::remove_symlink(self.run_path().join("flatpak"));
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_dir(self.run_path()) {
|
||||
warn!("failed to remove run directory {}: {}", self.run_path().display(), e);
|
||||
|
@@ -31,6 +31,28 @@ impl Systemd {
|
||||
if realm.config().ephemeral_home() {
|
||||
self.setup_ephemeral_home(realm)?;
|
||||
}
|
||||
if realm.config().flatpak() {
|
||||
self.setup_flatpak_workaround(realm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// What even is this??
|
||||
//
|
||||
// Good question.
|
||||
//
|
||||
// https://bugzilla.redhat.com/show_bug.cgi?id=2210335#c10
|
||||
//
|
||||
fn setup_flatpak_workaround(&self, realm: &Realm) -> Result<()> {
|
||||
let commands = &[
|
||||
vec!["/usr/bin/mount", "-m", "-t","proc", "proc", "/run/flatpak-workaround/proc"],
|
||||
vec!["/usr/bin/chmod", "700", "/run/flatpak-workaround"],
|
||||
];
|
||||
|
||||
for cmd in commands {
|
||||
Self::machinectl_shell(realm, cmd, "root", false, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@@ -23,8 +23,8 @@ impl ResizeSize {
|
||||
|
||||
pub fn gigs(n: usize) -> Self {
|
||||
ResizeSize(BLOCKS_PER_GIG * n)
|
||||
|
||||
}
|
||||
|
||||
pub fn megs(n: usize) -> Self {
|
||||
ResizeSize(BLOCKS_PER_MEG * n)
|
||||
}
|
||||
@@ -45,8 +45,8 @@ impl ResizeSize {
|
||||
self.0 / BLOCKS_PER_MEG
|
||||
}
|
||||
|
||||
/// If the RealmFS needs to be resized to a larger size, returns the
|
||||
/// recommended size.
|
||||
/// If the RealmFS has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new
|
||||
/// size to resize the filesystem to and return it. Otherwise, return `None`
|
||||
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
|
||||
let sb = match Superblock::load(realmfs.path(), 4096) {
|
||||
Ok(sb) => sb,
|
||||
@@ -56,22 +56,37 @@ impl ResizeSize {
|
||||
},
|
||||
};
|
||||
|
||||
sb.free_block_count();
|
||||
let free_blocks = sb.free_block_count() as usize;
|
||||
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
|
||||
let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
|
||||
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
|
||||
let mask = grow_size - 1;
|
||||
let grow_blocks = (free_blocks + mask) & !mask;
|
||||
Some(ResizeSize::blocks(grow_blocks))
|
||||
if free_blocks >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let metainfo_nblocks = realmfs.metainfo().nblocks();
|
||||
|
||||
if metainfo_nblocks >= AUTO_RESIZE_INCREASE_SIZE.nblocks() {
|
||||
return Some(ResizeSize::blocks(metainfo_nblocks + AUTO_RESIZE_INCREASE_SIZE.nblocks()))
|
||||
}
|
||||
|
||||
// If current size is under 4GB (AUTO_RESIZE_INCREASE_SIZE) and raising size to 4GB will create more than the
|
||||
// minimum free space (1GB) then just do that.
|
||||
if free_blocks + (AUTO_RESIZE_INCREASE_SIZE.nblocks() - metainfo_nblocks) >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
Some(AUTO_RESIZE_INCREASE_SIZE)
|
||||
} else {
|
||||
None
|
||||
// Otherwise for original size under 4GB, since raising to 4GB is not enough,
|
||||
// raise size to 8GB
|
||||
Some(ResizeSize::blocks(AUTO_RESIZE_INCREASE_SIZE.nblocks() * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERBLOCK_SIZE: usize = 1024;
|
||||
|
||||
/// An EXT4 superblock structure.
|
||||
///
|
||||
/// A class for reading the first superblock from an EXT4 filesystem
|
||||
/// and parsing the Free Block Count field. No other fields are parsed
|
||||
/// since this is the only information needed for the resize operation.
|
||||
///
|
||||
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
|
||||
|
||||
impl Superblock {
|
||||
|
@@ -76,8 +76,6 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
self.realmfs.copy_image_file(self.target())?;
|
||||
|
||||
self.truncate_verity()?;
|
||||
self.resize_image_file()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -115,9 +113,8 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
// Return size of image file in blocks based on metainfo `nblocks` field.
|
||||
// Include header block in count so add one block
|
||||
fn metainfo_nblock_size(&self) -> usize {
|
||||
self.realmfs.metainfo().nblocks() + 1
|
||||
self.realmfs.metainfo().nblocks()
|
||||
}
|
||||
|
||||
fn unmount_update_image(&mut self) {
|
||||
@@ -159,7 +156,8 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
fn set_target_len(&self, nblocks: usize) -> Result<()> {
|
||||
let len = (nblocks * BLOCK_SIZE) as u64;
|
||||
// add one block for header block
|
||||
let len = ((nblocks + 1) * BLOCK_SIZE) as u64;
|
||||
let f = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&self.target)
|
||||
@@ -176,7 +174,7 @@ impl <'a> Update<'a> {
|
||||
|
||||
if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
|
||||
self.set_target_len(metainfo_nblocks)?;
|
||||
} else if file_nblocks > metainfo_nblocks {
|
||||
} else if file_nblocks > (metainfo_nblocks + 1) {
|
||||
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
|
||||
}
|
||||
Ok(())
|
||||
@@ -185,7 +183,7 @@ impl <'a> Update<'a> {
|
||||
// If resize was requested, adjust size of update copy of image file.
|
||||
fn resize_image_file(&self) -> Result<()> {
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks() + 1,
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
@@ -224,7 +222,7 @@ impl <'a> Update<'a> {
|
||||
fn seal(&mut self) -> Result<()> {
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => self.metainfo_nblock_size() - 1,
|
||||
None => self.metainfo_nblock_size(),
|
||||
};
|
||||
|
||||
let salt = hex::encode(randombytes(32));
|
||||
@@ -232,20 +230,11 @@ impl <'a> Update<'a> {
|
||||
.map_err(context!("failed to create verity context for realmfs update image {:?}", self.target()))?;
|
||||
let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)
|
||||
.map_err(context!("failed to generate dm-verity hashtree for realmfs update image {:?}", self.target()))?;
|
||||
// XXX passes metainfo for nblocks
|
||||
//let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?;
|
||||
|
||||
let root_hash = output.root_hash()
|
||||
.ok_or_else(|| format_err!("no root hash returned from verity format operation"))?;
|
||||
info!("root hash is {}", output.root_hash().unwrap());
|
||||
|
||||
/*
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => self.metainfo_nblock_size() - 1,
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
info!("Signing new image with user realmfs keys");
|
||||
let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash);
|
||||
let keys = self.realmfs.sealing_keys().expect("No sealing keys");
|
||||
|
@@ -40,6 +40,11 @@ impl ResourceImage {
|
||||
pub fn find(image_type: &str) -> Result<Self> {
|
||||
let channel = Self::rootfs_channel();
|
||||
|
||||
// search when citadel is installed
|
||||
if let Some(image) = search_directory(format!("/storage/resources/{channel}/"), image_type, Some(&channel))? {
|
||||
return Ok(image);
|
||||
}
|
||||
|
||||
info!("Searching run directory for image {} with channel {}", image_type, channel);
|
||||
|
||||
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, Some(&channel))? {
|
||||
@@ -353,6 +358,11 @@ impl ResourceImage {
|
||||
if Mounts::is_source_mounted("/dev/mapper/citadel-storage")? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if Mounts::is_source_mounted("/storage")? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let path = Path::new("/dev/mapper/citadel-storage");
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
@@ -420,8 +430,11 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
|
||||
None => return Ok(b),
|
||||
};
|
||||
|
||||
let ver_a = a.metainfo().version();
|
||||
let ver_b = b.metainfo().version();
|
||||
let bind_a = a.metainfo();
|
||||
let bind_b = b.metainfo();
|
||||
|
||||
let ver_a = bind_a.version();
|
||||
let ver_b = bind_b.version();
|
||||
|
||||
if ver_a > ver_b {
|
||||
Ok(a)
|
||||
@@ -521,8 +534,14 @@ fn maybe_add_dir_entry(entry: &DirEntry,
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
|
||||
return Ok(());
|
||||
if kernel_id.is_some() {
|
||||
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
} else { // in live mode, kernel_id is None
|
||||
if image_type == "kernel" && metainfo.kernel_version() != kernel_version {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
images.push(ResourceImage::new(&path, header));
|
||||
|
@@ -15,11 +15,12 @@ impl Mounts {
|
||||
pub fn is_source_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mounted = Self::load()?
|
||||
.mounts()
|
||||
.any(|m| m.source_path() == path);
|
||||
|
||||
Ok(mounted)
|
||||
for i in Self::load()?.mounts() {
|
||||
if i.line.contains(&path.display().to_string()) {
|
||||
return Ok(true)
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn is_target_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||
|
97
libcitadel/src/updates.rs
Normal file
97
libcitadel/src/updates.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use std::fmt;
|
||||
use std::slice::Iter;
|
||||
|
||||
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
|
||||
|
||||
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of the that information
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CryptoContainerFile {
|
||||
pub serialized_citadel_version: Vec<u8>, // we serialize CitadelVersion
|
||||
pub signature: String, // serialized CitadelVersion gets signed
|
||||
pub signatory: String, // name of org or person who holds the key
|
||||
}
|
||||
|
||||
/// This struct contains the entirety of the information needed to decide whether to update or not
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct CitadelVersionStruct {
|
||||
pub client: String,
|
||||
pub channel: String, // dev, prod ...
|
||||
pub component_version: Vec<AvailableComponentVersion>,
|
||||
pub publisher: String, // name of org or person who released this update
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CitadelVersionStruct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} image with channel {} has components:\n",
|
||||
self.client, self.channel
|
||||
)?;
|
||||
|
||||
for i in &self.component_version {
|
||||
write!(
|
||||
f,
|
||||
"\n{} with version {} at location {}",
|
||||
i.component, i.version, i.file_path
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
|
||||
pub struct AvailableComponentVersion {
|
||||
pub component: Component, // rootfs, kernel or extra
|
||||
pub version: String, // stored as semver
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
impl PartialOrd for AvailableComponentVersion {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
// absolutely require that the components be in the same order in all structs (rootfs, kernel, extra)
|
||||
if &self.component != &other.component {
|
||||
panic!("ComponentVersion comparison failed because comparing different components");
|
||||
}
|
||||
|
||||
Some(
|
||||
semver::Version::parse(&self.version)
|
||||
.unwrap()
|
||||
.cmp(&semver::Version::parse(&other.version).unwrap()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AvailableComponentVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} image has version {}",
|
||||
self.component, self.version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, clap::ValueEnum)]
|
||||
pub enum Component {
|
||||
Rootfs,
|
||||
Kernel,
|
||||
Extra,
|
||||
}
|
||||
|
||||
impl Component {
|
||||
pub fn iterator() -> Iter<'static, Component> {
|
||||
static COMPONENTS: [Component; 3] =
|
||||
[Component::Rootfs, Component::Kernel, Component::Extra];
|
||||
COMPONENTS.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Component {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Component::Rootfs => write!(f, "rootfs"),
|
||||
Component::Kernel => write!(f, "kernel"),
|
||||
&Component::Extra => write!(f, "extra"),
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ use std::env;
|
||||
use std::fs::{self, File, DirEntry};
|
||||
use std::ffi::CString;
|
||||
use std::io::{self, Seek, Read, BufReader, SeekFrom};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
@@ -47,6 +48,12 @@ fn search_path(filename: &str) -> Result<PathBuf> {
|
||||
bail!("could not find {} in $PATH", filename)
|
||||
}
|
||||
|
||||
pub fn append_to_path(p: &Path, s: &str) -> PathBuf {
|
||||
let mut p_osstr = p.as_os_str().to_owned();
|
||||
p_osstr.push(s);
|
||||
p_osstr.into()
|
||||
}
|
||||
|
||||
pub fn ensure_command_exists(cmd: &str) -> Result<()> {
|
||||
let path = Path::new(cmd);
|
||||
if !path.is_absolute() {
|
||||
@@ -217,7 +224,8 @@ where
|
||||
///
|
||||
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
if path.exists() {
|
||||
let is_symlink = fs::symlink_metadata(path).is_ok();
|
||||
if is_symlink || path.exists() {
|
||||
fs::remove_file(path)
|
||||
.map_err(context!("failed to remove file {:?}", path))?;
|
||||
}
|
||||
@@ -336,7 +344,6 @@ pub fn is_euid_root() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
|
||||
let cstr = CString::new(path.as_os_str().as_bytes())
|
||||
.expect("path contains null byte");
|
||||
@@ -368,9 +375,38 @@ pub fn touch_mtime(path: &Path) -> Result<()> {
|
||||
|
||||
utimes(path, meta.atime(),mtime)?;
|
||||
Ok(())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn nsenter_netns(netns: &str) -> Result<()> {
|
||||
let mut path = PathBuf::from("/run/netns");
|
||||
path.push(netns);
|
||||
if !path.exists() {
|
||||
bail!("Network namespace '{}' does not exist", netns);
|
||||
}
|
||||
let f = File::open(&path)
|
||||
.map_err(context!("error opening netns file {}", path.display()))?;
|
||||
|
||||
let fd = f.as_raw_fd();
|
||||
|
||||
unsafe {
|
||||
if libc::setns(fd, libc::CLONE_NEWNET) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to setns() into network namespace '{}': {}", netns, err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
|
||||
unsafe {
|
||||
if libc::setgid(gid) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to call setgid({}): {}", gid, err);
|
||||
|
||||
} else if libc::setuid(uid) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to call setuid({}): {}", uid, err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@@ -4,6 +4,6 @@ StartLimitIntervalSec=0
|
||||
|
||||
[Path]
|
||||
PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/rootfs/var/lib/flatpak/exports/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/flatpak/exports/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/flatpak/exports/share/applications
|
||||
|
529
update-generator/src/main.rs
Normal file
529
update-generator/src/main.rs
Normal file
@@ -0,0 +1,529 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use ed25519_dalek::pkcs8::DecodePublicKey;
|
||||
use ed25519_dalek::pkcs8::EncodePublicKey;
|
||||
use ed25519_dalek::Signature;
|
||||
use ed25519_dalek::Signer;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use ed25519_dalek::KEYPAIR_LENGTH;
|
||||
use glob::glob;
|
||||
use libcitadel::updates::Component;
|
||||
use libcitadel::{updates, ImageHeader};
|
||||
use rand::rngs::OsRng;
|
||||
use sodiumoxide::crypto::pwhash;
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use sodiumoxide::crypto::stream;
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
const SALSA_NONCE: &[u8] = &[
|
||||
116, 138, 142, 103, 234, 105, 192, 48, 117, 29, 150, 214, 106, 116, 195, 64, 120, 251, 94, 20,
|
||||
212, 118, 125, 189,
|
||||
];
|
||||
|
||||
const PASSWORD_SALT: &[u8] = &[
|
||||
18, 191, 168, 237, 156, 199, 54, 43, 122, 165, 35, 9, 89, 106, 36, 209, 145, 161, 90, 2, 121,
|
||||
51, 242, 182, 14, 245, 47, 253, 237, 153, 251, 219,
|
||||
];
|
||||
|
||||
const LAST_RESORT_CLIENT: &str = "public";
|
||||
const LAST_RESORT_CHANNEL: &str = "prod";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Perform tasks needed to create an update and publish it")]
|
||||
#[command(author, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
enum Commands {
|
||||
/// Generate a keypair to be used to sign version file. You will be asked to provide a mandatory password.
|
||||
GenerateKeypair {
|
||||
/// keypair filepath to save to
|
||||
#[arg(short, long)]
|
||||
keypair_filepath: Option<String>,
|
||||
},
|
||||
|
||||
/// Generate the complete cbor file. If no components are passed, generate by reading image of each component
|
||||
CreateSignedFile {
|
||||
/// rootfs image semver version
|
||||
#[arg(short, long)]
|
||||
rootfs_image_version: Option<String>,
|
||||
/// kernel image semver version
|
||||
#[arg(short, long)]
|
||||
kernel_image_version: Option<String>,
|
||||
#[arg(short, long)]
|
||||
extra_image_version: Option<String>,
|
||||
/// keypair filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
path_keypair: Option<String>,
|
||||
/// command output complete filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
versionfile_filepath: Option<String>,
|
||||
},
|
||||
|
||||
/// Verify that the version file is correctly signed
|
||||
VerifySignature {
|
||||
/// public key filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
publickey_filepath: Option<String>,
|
||||
/// command output complete filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
versionfile_filepath: Option<String>,
|
||||
},
|
||||
|
||||
UploadToServer {
|
||||
#[arg(long)]
|
||||
component: Option<updates::Component>,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Some(Commands::GenerateKeypair { keypair_filepath }) => {
|
||||
generate_keypair(keypair_filepath).context("Failed to generate keypair")?
|
||||
}
|
||||
Some(Commands::CreateSignedFile {
|
||||
path_keypair,
|
||||
rootfs_image_version,
|
||||
kernel_image_version,
|
||||
extra_image_version,
|
||||
versionfile_filepath,
|
||||
}) => create_signed_version_file(
|
||||
path_keypair,
|
||||
rootfs_image_version,
|
||||
kernel_image_version,
|
||||
extra_image_version,
|
||||
versionfile_filepath,
|
||||
)
|
||||
.context("Failed to create signed file")?,
|
||||
Some(Commands::VerifySignature {
|
||||
publickey_filepath,
|
||||
versionfile_filepath,
|
||||
}) => verify_version_signature(publickey_filepath, versionfile_filepath)
|
||||
.context("Failed to verify signature")?,
|
||||
Some(Commands::UploadToServer { component, path }) => {
|
||||
upload_components_to_server(component, path)
|
||||
.context("Failed to upload to the server")?
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_keypair(keypair_filepath: &Option<String>) -> Result<()> {
|
||||
// if the user did not pass a path, we save key files to current directory
|
||||
let keypair_filepath = &keypair_filepath.clone().unwrap_or_else(|| ".".to_string());
|
||||
let path = std::path::Path::new(keypair_filepath);
|
||||
|
||||
let mut password;
|
||||
|
||||
loop {
|
||||
// get passphrase used to encrypt key from user
|
||||
password = rpassword::prompt_password(
|
||||
"Please enter the passphrase we will use to encrypt the private key with: ",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let password_confirm = rpassword::prompt_password("Retype same passphrase: ").unwrap();
|
||||
|
||||
if password != password_confirm {
|
||||
println!("\nPassphrases did not match. Please try again")
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// generate keypair
|
||||
let mut csprng = OsRng;
|
||||
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
|
||||
|
||||
let keypair_fp: PathBuf;
|
||||
let publickey_fp: PathBuf;
|
||||
|
||||
if path.is_dir() {
|
||||
keypair_fp = path.join("keypair.priv");
|
||||
publickey_fp = path.join("update_server_key.pub");
|
||||
} else {
|
||||
keypair_fp = path.to_path_buf();
|
||||
publickey_fp = path.parent().unwrap().join("update_server_key.pub");
|
||||
}
|
||||
|
||||
let mut keyfile = std::fs::File::create(&keypair_fp)?;
|
||||
let mut public_key = std::fs::File::create(&publickey_fp)?;
|
||||
|
||||
// encrypt private key
|
||||
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||
|
||||
let secretbox::Key(ref mut kb) = k;
|
||||
let password_hash = pwhash::derive_key(
|
||||
kb,
|
||||
password.as_bytes(),
|
||||
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||
.unwrap(),
|
||||
pwhash::OPSLIMIT_INTERACTIVE,
|
||||
pwhash::MEMLIMIT_INTERACTIVE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let plaintext = signing_key.to_keypair_bytes();
|
||||
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||
.expect("failed to unwrap key");
|
||||
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||
.expect("failed to unwrap nonce");
|
||||
|
||||
// encrypt the plaintext
|
||||
let ciphertext = stream::stream_xor(&plaintext, &nonce, &key);
|
||||
|
||||
keyfile.write_all(&ciphertext)?;
|
||||
public_key.write_all(
|
||||
&signing_key
|
||||
.verifying_key()
|
||||
.to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
password.zeroize();
|
||||
|
||||
println!(
|
||||
"Generated the keypair and wrote to {}. The public key is here: {}",
|
||||
keypair_fp.display(),
|
||||
publickey_fp.display()
|
||||
);
|
||||
println!(
|
||||
"You may now move the public key from {} to the citadel build path at citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub",
|
||||
publickey_fp.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_signed_version_file(
|
||||
signing_key_path: &Option<String>,
|
||||
citadel_rootfs_version: &Option<String>,
|
||||
citadel_kernel_version: &Option<String>,
|
||||
citadel_extra_version: &Option<String>,
|
||||
versionfile_filepath: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let rootfs_version = match citadel_rootfs_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse rootfs semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Rootfs).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let kernel_version = match citadel_kernel_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse kernel semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Kernel).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let extra_version = match citadel_extra_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse extra semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Extra).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let rootfs_path = get_component_path(&Component::Rootfs);
|
||||
let channel;
|
||||
|
||||
if rootfs_path.is_ok() {
|
||||
channel = match ImageHeader::from_file(rootfs_path?) {
|
||||
Ok(image_header) => image_header.metainfo().channel().to_string(),
|
||||
Err(_) => env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
};
|
||||
} else {
|
||||
channel = env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string());
|
||||
}
|
||||
|
||||
let component_version_vector = vec![
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Rootfs,
|
||||
version: rootfs_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"rootfs",
|
||||
rootfs_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Kernel,
|
||||
version: kernel_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"kernel",
|
||||
kernel_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Extra,
|
||||
version: extra_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"extra",
|
||||
extra_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
// generate version file
|
||||
let citadel_version = updates::CitadelVersionStruct {
|
||||
client: env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel: channel.to_string(),
|
||||
component_version: component_version_vector,
|
||||
publisher: "Subgraph".to_string(),
|
||||
};
|
||||
|
||||
let fp = match signing_key_path {
|
||||
Some(fp) => Path::new(fp),
|
||||
None => Path::new("keypair.priv"),
|
||||
};
|
||||
|
||||
// serialized to cbor
|
||||
let serialized_citadel_version = serde_cbor::to_vec(&citadel_version)?;
|
||||
|
||||
// get signing_key_bytes from the file passed
|
||||
let mut keyfile = std::fs::File::open(&fp)?;
|
||||
let mut buf = [0; KEYPAIR_LENGTH];
|
||||
keyfile.read_exact(&mut buf)?;
|
||||
|
||||
// prompt user for keypair decryption password
|
||||
let mut password =
|
||||
rpassword::prompt_password("Please enter the passphrase used to decrypt the private key: ")
|
||||
.unwrap();
|
||||
|
||||
// decrypt private key
|
||||
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||
|
||||
let secretbox::Key(ref mut kb) = k;
|
||||
let password_hash = pwhash::derive_key(
|
||||
kb,
|
||||
password.as_bytes(),
|
||||
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||
.unwrap(),
|
||||
pwhash::OPSLIMIT_INTERACTIVE,
|
||||
pwhash::MEMLIMIT_INTERACTIVE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||
.expect("failed to unwrap key");
|
||||
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||
.expect("failed to unwrap nonce");
|
||||
|
||||
// decrypt the ciphertext
|
||||
let plaintext = stream::stream_xor(&buf, &nonce, &key);
|
||||
|
||||
let signing_key = SigningKey::from_keypair_bytes(plaintext[0..64].try_into()?)?;
|
||||
|
||||
// sign serialized_citadel_version for inclusion in version_file
|
||||
let signature: Signature = signing_key.sign(&serialized_citadel_version);
|
||||
|
||||
// generate signature of citadel_version cbor format (signed)
|
||||
let version_file = updates::CryptoContainerFile {
|
||||
serialized_citadel_version,
|
||||
signature: signature.to_string(),
|
||||
signatory: "Subgraph".to_string(),
|
||||
};
|
||||
|
||||
let vf_fp = match versionfile_filepath {
|
||||
Some(vf_fp) => {
|
||||
if Path::new(vf_fp).is_dir() {
|
||||
Path::new(vf_fp).join("version.cbor")
|
||||
} else {
|
||||
Path::new(vf_fp).to_path_buf()
|
||||
}
|
||||
}
|
||||
None => Path::new("version.cbor").to_path_buf(),
|
||||
};
|
||||
|
||||
let outfile = std::fs::File::create(&vf_fp)?;
|
||||
|
||||
serde_cbor::to_writer(outfile, &version_file)?;
|
||||
|
||||
password.zeroize();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that the completed version file correctly verifies given the self-embedded signature and public key
|
||||
fn verify_version_signature(
|
||||
pubkey_filepath: &Option<String>,
|
||||
versionfile_filepath: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let pub_fp = match pubkey_filepath {
|
||||
Some(pub_fp) => Path::new(pub_fp),
|
||||
None => Path::new("update_server_key.pub"),
|
||||
};
|
||||
|
||||
let version_fp = match versionfile_filepath {
|
||||
Some(version_fp) => Path::new(version_fp),
|
||||
None => Path::new("version.cbor"),
|
||||
};
|
||||
|
||||
let mut pubkey_file = std::fs::File::open(&pub_fp)?;
|
||||
|
||||
let mut buf = String::new();
|
||||
pubkey_file.read_to_string(&mut buf)?;
|
||||
|
||||
let verifying_key = VerifyingKey::from_public_key_pem(&buf)?;
|
||||
|
||||
let version_file = &std::fs::File::open(version_fp)?;
|
||||
let crypto_container_struct: updates::CryptoContainerFile =
|
||||
serde_cbor::from_reader(version_file)?;
|
||||
|
||||
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||
serde_cbor::from_slice(&crypto_container_struct.serialized_citadel_version)?;
|
||||
|
||||
let signature = ed25519_dalek::Signature::from_str(&crypto_container_struct.signature)?;
|
||||
|
||||
match verifying_key.verify_strict(
|
||||
&crypto_container_struct.serialized_citadel_version,
|
||||
&signature,
|
||||
) {
|
||||
Ok(_) => println!("Everythin ok. Signature verified correctly"),
|
||||
Err(e) => panic!(
|
||||
"Error: Signature was not able to be verified! Threw error: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
println!(
|
||||
"The destructured version file contains the following information:\n{}",
|
||||
citadel_version_struct
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Make sure to add your ssh key to the "updates" user on the server
|
||||
fn send_with_scp(from: &PathBuf, to: &PathBuf) -> Result<()> {
|
||||
Command::new("scp")
|
||||
.arg(from)
|
||||
.arg(format!(
|
||||
"updates@{}:/updates/files/{}",
|
||||
updates::UPDATE_SERVER_HOSTNAME,
|
||||
to.to_string_lossy()
|
||||
))
|
||||
.spawn()
|
||||
.context("scp command failed to run")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_imageheader_version(component: &Component) -> Result<String> {
|
||||
let version = match ImageHeader::from_file(get_component_path(component)?) {
|
||||
Ok(image_header) => image_header.metainfo().version().to_string(),
|
||||
Err(_) => String::from("0.0.0"),
|
||||
};
|
||||
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
fn get_component_path(component: &updates::Component) -> Result<PathBuf> {
|
||||
let mut gl = match component {
|
||||
Component::Rootfs => glob("../build/images/citadel-rootfs*.img")?,
|
||||
Component::Kernel => glob("../build/images/citadel-kernel*.img")?,
|
||||
Component::Extra => glob("../build/images/citadel-extra*.img")?,
|
||||
};
|
||||
|
||||
let first = gl.nth(0).context(format!(
|
||||
"Failed to find citadel-{}*.img in ../build/images/",
|
||||
component
|
||||
))?;
|
||||
|
||||
Ok(PathBuf::from(first?))
|
||||
}
|
||||
|
||||
fn upload_all_components() -> Result<()> {
|
||||
for component in updates::Component::iterator() {
|
||||
upload_component(component, &None)?
|
||||
}
|
||||
upload_cbor()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_component(component: &updates::Component, path: &Option<PathBuf>) -> Result<()> {
|
||||
let final_path;
|
||||
|
||||
// uggliest if statement i've ever written
|
||||
// if path was passed to this function
|
||||
if let Some(p) = path {
|
||||
final_path = p.to_path_buf();
|
||||
} else {
|
||||
// if the path wasn't passed to this function, search for the component path
|
||||
if let Ok(p) = get_component_path(&component) {
|
||||
final_path = p;
|
||||
} else {
|
||||
// if path was not passed and we failed to locate the component's path
|
||||
println!(
|
||||
"We failed to find the {} image we were looking for... Skipping...",
|
||||
component
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let image_header = libcitadel::ImageHeader::from_file(&final_path)?;
|
||||
|
||||
let to = PathBuf::from(format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
component,
|
||||
image_header.metainfo().version()
|
||||
));
|
||||
send_with_scp(&final_path, &to)
|
||||
}
|
||||
|
||||
// If no parameters are passed to this command, upload all components regardless of version on the server
|
||||
// If a path is passed, upload that image to the server regardless version.
|
||||
fn upload_components_to_server(
|
||||
component: &Option<updates::Component>,
|
||||
path: &Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
// check if a component is passed to this function:
|
||||
match component {
|
||||
Some(comp) => upload_component(comp, path)?, // This function handles the option of not passing a path
|
||||
None => upload_all_components()?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_cbor() -> Result<()> {
|
||||
send_with_scp(
|
||||
&PathBuf::from("version.cbor"),
|
||||
&PathBuf::from(format!(
|
||||
"{}/{}/version.cbor",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
)),
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user