specification and initial launching

This commit is contained in:
Jake Hillion 2022-02-13 23:52:41 +00:00
parent 565c1567c4
commit 969bf3a9ee
6 changed files with 415 additions and 13 deletions

115
Cargo.lock generated
View File

@ -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"

View File

@ -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"

18
build.rs Normal file
View File

@ -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);
}

View File

@ -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),
}

View File

@ -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 <jake@hillion.co.uk>")
.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) {}

129
src/specification.rs Normal file
View File

@ -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<String, Entrypoint>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Entrypoint {
pub trigger: Trigger,
pub args: Vec<Arg>,
pub permissions: HashSet<Permissions>,
}
#[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(())
}
}