From 969bf3a9ee8df85dd4ecc5ee6f43ab4557c04a6f Mon Sep 17 00:00:00 2001 From: Jake Hillion Date: Sun, 13 Feb 2022 23:52:41 +0000 Subject: [PATCH] specification and initial launching --- Cargo.lock | 115 +++++++++++++++++++++++++++++++++++ Cargo.toml | 6 ++ build.rs | 18 ++++++ src/error.rs | 20 +++++++ src/main.rs | 140 +++++++++++++++++++++++++++++++++++++++---- src/specification.rs | 129 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 415 insertions(+), 13 deletions(-) create mode 100644 build.rs create mode 100644 src/specification.rs diff --git a/Cargo.lock b/Cargo.lock index b5bcfae..b14f71e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,14 +46,34 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +dependencies = [ + "atty", + "bitflags", + "indexmap", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + [[package]] name = "clone-shim" version = "0.1.0" dependencies = [ + "clap", "env_logger", + "exitcode", + "ipnetwork", "libc", "log", "nix", + "serde", + "serde_json", "thiserror", ] @@ -70,6 +90,18 @@ dependencies = [ "termcolor", ] +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -85,6 +117,31 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnetwork" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355" +dependencies = [ + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "libc" version = "0.2.117" @@ -128,6 +185,15 @@ dependencies = [ "memoffset", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -163,6 +229,49 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.86" @@ -183,6 +292,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + [[package]] name = "thiserror" version = "1.0.30" diff --git a/Cargo.toml b/Cargo.toml index 1c80731..4ad6f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,12 @@ edition = "2021" log = "0.4" env_logger = "0.9" thiserror = "1" +clap = "3" +exitcode = "1" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +ipnetwork = "0.18" libc = "0.2.117" nix = "0.23.1" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..42aac32 --- /dev/null +++ b/build.rs @@ -0,0 +1,18 @@ +use std::process::Command; + +fn main() { + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap(); + let mut git_hash = String::from_utf8(output.stdout).unwrap(); + git_hash.truncate(16); + let clean_status = Command::new("git") + .args(&["diff", "--exit-code"]) + .status() + .unwrap(); + if !clean_status.success() { + git_hash.push_str("-dirty"); + } + println!("cargo:rustc-env=GIT_HASH={}", git_hash); +} diff --git a/src/error.rs b/src/error.rs index 83b5e76..8d3c5dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,27 @@ +use std::io; + use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error("{msg}: {src}")] Nix { msg: &'static str, src: nix::Error }, + + #[error("io: {0}")] + Io(#[from] io::Error), + + #[error("json: {0}")] + Json(#[from] serde_json::Error), + + #[error("bad specification type: only .json files are supported")] + BadSpecType, + + #[error("too many pipes: a pipe must have one reader and one writer: {0}")] + TooManyPipes(String), + + #[error("read only pipe: a pipe must have one reader and one writer: {0}")] + ReadOnlyPipe(String), + + #[error("write only pipe: a pipe must have one reader and one writer: {0}")] + WriteOnlyPipe(String), } diff --git a/src/main.rs b/src/main.rs index fe7890f..20b32a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,142 @@ -use log::info; +use log::{debug, error, info}; mod clone; mod error; +mod specification; use clone::{clone3, CloneArgs, CloneFlags}; use error::Error; +use specification::{Arg, Entrypoint, Pipe, Specification, Trigger}; -use nix::unistd::Pid; +use std::collections::HashMap; +use std::ffi::CString; +use std::fs::File; +use std::os::unix::io::{AsRawFd, FromRawFd}; -fn main() -> Result<(), Error> { - let env = env_logger::Env::new().filter_or("LOG", "info"); +use clap::{App, AppSettings}; +use nix::unistd::{self, Pid}; + +fn main() { + std::process::exit(match run() { + Ok(_) => { + info!("launched successfully"); + exitcode::OK + } + Err(e) => { + error!("error: {}", e); + -1 + } + }) +} + +fn run() -> Result<(), Error> { + // process arguments + let matches = App::new("clone-shim") + .version(env!("GIT_HASH")) + .author("Jake Hillion ") + .about("Launch a multi entrypoint app, cloning as requested by an external specification or the ELF.") + .arg(clap::Arg::new("spec").long("specification").short('s').help("Provide the specification as an external JSON file.").takes_value(true)) + .setting(AppSettings::TrailingVarArg) + .arg(clap::Arg::new("verbose").long("verbose").short('v').help("Use verbose logging.").takes_value(false)) + .arg(clap::Arg::new("binary").index(1).help("Binary and arguments to launch with the shim").required(true).multiple_values(true)) + .get_matches(); + + let (binary, trailing) = { + let mut argv = matches.values_of("binary").unwrap(); + + let binary = argv.next().unwrap(); + let trailing: Vec<&str> = argv.collect(); + + (binary, trailing) + }; + + // setup logging + let env = env_logger::Env::new().filter_or( + "LOG", + if matches.is_present("verbose") { + "debug" + } else { + "warn" + }, + ); env_logger::init_from_env(env); - info!("getting started"); - - if clone3(CloneArgs::new(CloneFlags::empty())).map_err(|e| Error::Nix { - msg: "clone3", - src: e, - })? != Pid::from_raw(0) - { - info!("hello from the child"); + // parse the specification + let spec: Specification = if let Some(m) = matches.value_of("spec") { + if m.ends_with(".json") { + let f = std::fs::File::open(m)?; + Ok(serde_json::from_reader(f)?) + } else { + Err(Error::BadSpecType) + } } else { - info!("hello from the parent"); + unimplemented!("reading spec from the elf is unimplemented") + }?; + + debug!("specification read: {:?}", &spec); + spec.validate()?; + + // create all the pipes + let (pipes, _) = spec.pipes(); + let mut read_pipes = HashMap::new(); + let mut write_pipes = HashMap::new(); + + for pipe in pipes { + info!("creating pipe pair `{}`", pipe); + + let (read, write) = unistd::pipe().map_err(|e| Error::Nix { + msg: "pipe", + src: e, + })?; + + // safe to create files given the successful return of pipe(2) + read_pipes.insert(pipe.to_string(), unsafe { File::from_raw_fd(read) }); + write_pipes.insert(pipe.to_string(), unsafe { File::from_raw_fd(write) }); + } + + // spawn all processes + for (name, entry) in &spec.entrypoints { + info!("spawning entrypoint `{}`", name.as_str()); + + match &entry.trigger { + Trigger::Startup => { + if clone3(CloneArgs::new(CloneFlags::empty())).map_err(|e| Error::Nix { + msg: "clone3", + src: e, + })? == Pid::from_raw(0) + { + let mut args = Vec::new(); + for arg in &entry.args { + match arg { + Arg::BinaryName => args.push(CString::new(binary).unwrap()), + Arg::Entrypoint => args.push(CString::new(name.as_str()).unwrap()), + Arg::Pipe(p) => args.push(match p { + Pipe::Rx(s) => { + CString::new(read_pipes[s].as_raw_fd().to_string()).unwrap() + } + Pipe::Tx(s) => { + CString::new(write_pipes[s].as_raw_fd().to_string()).unwrap() + } + }), + Arg::Trailing => { + args.extend(trailing.iter().map(|s| CString::new(*s).unwrap())) + } + } + } + + unistd::execv(&CString::new(binary).unwrap(), &args).map_err(|e| { + Error::Nix { + msg: "execv", + src: e, + } + })?; + } + } + Trigger::Pipe(s) => {} + } } Ok(()) } + +pub fn pipe_trigger(pipe: File, entry: &Entrypoint) {} diff --git a/src/specification.rs b/src/specification.rs new file mode 100644 index 0000000..1516e36 --- /dev/null +++ b/src/specification.rs @@ -0,0 +1,129 @@ +use crate::Error; + +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; + +use ipnetwork::{Ipv4Network, Ipv6Network}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Specification { + pub entrypoints: HashMap, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Entrypoint { + pub trigger: Trigger, + pub args: Vec, + pub permissions: HashSet, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum Trigger { + Startup, + Pipe(String), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum Arg { + /// The binary name, or argv[0], of the original program start + BinaryName, + + /// The name of this entrypoint + Entrypoint, + + /// A chosen end of a named pipe + Pipe(Pipe), + + /// The rest of argv[1..], 0 or more arguments + Trailing, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum Pipe { + Rx(String), + Tx(String), +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] +#[serde(tag = "type")] +pub enum Permissions { + Filesystem { + host_path: PathBuf, + final_path: PathBuf, + }, + Network { + network: Network, + }, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] +#[serde(tag = "type")] +pub enum Network { + InternetV4, + InternetV6, + PrivateV4(Ipv4Network), + PrivateV6(Ipv6Network), +} + +impl Specification { + pub fn pipes(&self) -> (Vec<&str>, Vec<&str>) { + let mut read = Vec::new(); + let mut write = Vec::new(); + + for (_, entry) in &self.entrypoints { + match &entry.trigger { + Trigger::Startup => {} + Trigger::Pipe(s) => read.push(s.as_str()), + } + + for arg in &entry.args { + match arg { + Arg::BinaryName => {} + Arg::Entrypoint => {} + Arg::Pipe(p) => match p { + Pipe::Rx(s) => read.push(s.as_str()), + Pipe::Tx(s) => write.push(s.as_str()), + }, + Arg::Trailing => {} + } + } + } + + (read, write) + } + + pub fn validate(&self) -> Result<(), Error> { + // validate pipes match + let (read, write) = self.pipes(); + let mut read_set = HashSet::with_capacity(read.len()); + + for pipe in read { + if read_set.insert(pipe) { + return Err(Error::TooManyPipes(pipe.to_string())); + } + } + + let mut write_set = HashSet::with_capacity(write.len()); + for pipe in write { + if write_set.insert(pipe) { + return Err(Error::TooManyPipes(pipe.to_string())); + } + } + + for pipe in read_set { + if !write_set.remove(pipe) { + return Err(Error::ReadOnlyPipe(pipe.to_string())); + } + } + + for pipe in write_set { + return Err(Error::WriteOnlyPipe(pipe.to_string())); + } + + Ok(()) + } +}