diff --git a/Cargo.lock b/Cargo.lock index eebd05b..97c0a79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,14 +21,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] -name = "cc" -version = "1.2.43" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" -dependencies = [ - "find-msvc-tools", - "shlex", -] +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "elb" @@ -80,17 +82,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - [[package]] name = "errno" version = "0.3.14" @@ -101,32 +92,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "exec" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615" -dependencies = [ - "errno 0.2.8", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - [[package]] name = "glob" version = "0.3.3" @@ -162,6 +127,18 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -187,18 +164,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", - "errno 0.3.14", + "errno", "libc", "linux-raw-sys", "windows-sys", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "syn" version = "2.0.108" @@ -247,28 +218,6 @@ dependencies = [ "winsafe", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-link" version = "0.2.1" @@ -296,7 +245,7 @@ version = "0.1.0" dependencies = [ "anyhow", "elb-dl", - "exec", "landlock", + "nix", "which", ] diff --git a/Cargo.toml b/Cargo.toml index f7e987c..318db26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "GPL-3.0-or-later" edition = "2024" [features] -default = [ "abi-6", "cli" ] +default = [ "abi-6", "cli", "compat-besteffort" ] cli = ["dep:elb-dl", "dep:which"] nix = [] abi-1 = [] @@ -17,6 +17,8 @@ abi-3 = [] abi-4 = [] abi-5 = [] abi-6 = [] +compat-besteffort = [] +compat-require = [] [profile.release] strip = true @@ -27,6 +29,6 @@ lto = true [dependencies] anyhow = "1.0.100" landlock = "0.4.3" -exec = "0.3.1" elb-dl = { version = "0.3.2", features = ["glibc"], default-features = false, optional = true } which = { version = "8.0.0", optional = true } +nix = { version = "0.30.1", features = ["fs", "mount", "process"] } diff --git a/nix/package.nix b/nix/package.nix index e7756da..8708b94 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,4 +1,5 @@ { + pkgs, lib, rustPlatform, features ? [ ], @@ -35,6 +36,9 @@ rustPlatform.buildRustPackage (finalAttrs: { "abi-${toString abi}" ]; + LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; + C_INCLUDE_PATH = "${pkgs.linuxHeaders}/include"; + meta = { description = details.description; homepage = details.repository; diff --git a/src/main.rs b/src/main.rs index abee00c..7ea52ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,148 +1,127 @@ +mod namespaces; mod parse; mod types; use anyhow::{Context, Result, anyhow}; use landlock::{ ABI, Access, AccessFs, AccessNet, BitFlags, Compatible, NetPort, Ruleset, RulesetAttr, - RulesetCreatedAttr, Scope, make_bitflags, path_beneath_rules, + RulesetCreated, RulesetCreatedAttr, Scope, make_bitflags, path_beneath_rules, }; #[cfg(feature = "cli")] use elb_dl::{DependencyTree, DynamicLoader, glibc}; +use nix::{ + errno::Errno, + libc::{MOUNT_ATTR_IDMAP, mkdtemp, unshare}, + mount::mount, + sys::statvfs::fstatvfs, + unistd::pivot_root, +}; #[cfg(feature = "cli")] -use std::{collections::VecDeque, path::PathBuf, str::FromStr}; -use std::{fs::File, io::Read, os::fd::FromRawFd}; +use std::{ + collections::VecDeque, + path::{Path, PathBuf}, + str::FromStr, +}; +use std::{ + env::temp_dir, + ffi::{CString, c_char}, + fs::{File, create_dir_all}, + io::Read, + os::{fd::FromRawFd, unix::process::CommandExt}, + process::Command, +}; -fn main() -> Result<()> { - let args = std::env::args().skip(1); - let mut opts = parse::parse_args(args)?; - if opts.fd_args { - let mut fd = unsafe { File::from_raw_fd(3) }; - let mut raw_args = String::new(); - fd.read_to_string(&mut raw_args)?; - let args = raw_args - .split_ascii_whitespace() - .map(|s| s.to_string()) - .collect::>(); - let fd_opts = parse::parse_args(args.into_iter())?; - opts.merge(fd_opts); +use crate::types::Yoke; + +fn check_conflicts(opts: &Yoke) -> Result<()> { + if (!opts.fs.is_empty()) || opts.unshare.mount && opts.unsandbox.fs { + return Err(anyhow!( + "conflicting options provided (--fs or --namespace m & --no-fs)" + )); } - if opts.exec.is_empty() { - // print help - eprintln!( - " -yoke -- simple sandboxer - -use: yoke [ruletype] [space separated rules] -- [command] - -rules ------------- - filesystem: - --fs | -f [access]=/path:/another/path - - tcp port: - --tcp | -t [access]=1234:5678 - - env vars: - --env | -e [key]=[value] - - retain inherited env vars: - --retain-env | -r - - allow use of external unix domain sockets: - --sockets | -s - - allow sending signals to other processes: - --signals | -k - - resolve process dependencies and add to sandbox - (with `ldd` feature): - --ldd | -l - - unsandbox: - --no-fs | -nf - --no-tcp | -nt - - accept additional rules from fd 3: - --fd-args | -fd - - -access specifiers ------------- - fs: - r - read - w - write - x - execute - i - ioctl - - tcp: - i - in/bind - o - out/connect - -examples ------------- - yoke --fs r=/etc -l -- ls /etc - - yoke --tcp io=80:443 --fs r=/srv/web --clear-env --env SERVE_FROM=/srv/web \ - -- myhttpserver - " - ); - std::process::exit(1); + if (!opts.net.is_empty() || opts.unshare.net) && opts.unsandbox.net { + return Err(anyhow!( + "conflicting options provided (--net or --namespace n & --no-net)" + )); } + // if opts.chroot.as_ref().is_some_and(|pb| !pb.is_absolute()) { + // return Err(anyhow!("chroot path must be absolute")); + // } + Ok(()) +} - // set up scope of our intial ruleset - let mut preempt = Ruleset::default(); - - preempt = preempt.set_compatibility(landlock::CompatLevel::HardRequirement); - - #[cfg(feature = "abi-2")] - let abi = ABI::V2; - #[cfg(feature = "abi-3")] - let abi = ABI::V3; - #[cfg(feature = "abi-4")] - let abi = ABI::V4; - #[cfg(feature = "abi-5")] - let abi = ABI::V5; - #[cfg(feature = "abi-6")] - let abi = ABI::V6; - - // disallow signals to other processes - if !opts.signals && abi >= ABI::V6 { - preempt = preempt.scope(Scope::Signal).context("scoping signals")?; - } - - // disallow connections to unix domain sockets - if !opts.sockets && abi >= ABI::V6 { - preempt = preempt - .scope(Scope::AbstractUnixSocket) - .context("scoping sockets")?; - } - - // lock down fs access - preempt = if !opts.unsandbox.fs { - preempt - .handle_access(AccessFs::from_all(abi)) - .context("handling fs access")? +#[cfg(not(feature = "cli"))] +fn trace_deps(mut ruleset: RulesetCreated, opts: &Yoke, abi: ABI) -> Result { + if opts.besteffort { + eprintln!("yoke was not compiled with the cli feature, cannot use --ldd"); + Ok(ruleset) } else { - preempt - }; + Err(anyhow!( + "yoke was not compiled with the cli feature, cannot use --ldd" + )) + } +} - // lock down tcp access - preempt = if !(opts.unsandbox.tcp || abi < ABI::V3) { - preempt - .handle_access(AccessNet::BindTcp) - .context("handling tcp bind access")? - .handle_access(AccessNet::ConnectTcp) - .context("handling tcp conn access")? - } else { - preempt - }; +#[cfg(feature = "cli")] +fn trace_deps(mut ruleset: RulesetCreated, abi: ABI, fullpath: &Path) -> Result { + #[cfg(not(feature = "nix"))] + let loader = DynamicLoader::options() + .search_dirs(glibc::get_search_dirs(PathBuf::from_str("/")?)?) + .new_loader(); - // create ruleset and begin inserting rules - let mut ruleset = preempt.create().context("creating ruleset")?; + #[cfg(feature = "nix")] + let loader = DynamicLoader::options() + .search_dirs(glibc::get_hard_coded_search_dirs(None)?) + .new_loader(); + let mut tree = DependencyTree::new(); + let mut queue = VecDeque::new(); + queue.push_back(fullpath.to_path_buf()); + while let Some(path) = queue.pop_front() { + let deps = loader.resolve_dependencies(&path, &mut tree)?; + queue.extend(deps); + } + ruleset = ruleset + .add_rules(path_beneath_rules( + tree.into_iter().fold(Vec::new(), |mut acc, (_, deps)| { + acc.extend(deps); + acc + }), + AccessFs::from_read(abi), + )) + .context("adding rules for dependencies")?; + Ok(ruleset) +} + +// TODO overhaul tcp -> net +fn insert_tcp_rules(mut ruleset: RulesetCreated, opts: &Yoke, abi: ABI) -> Result { + if abi >= ABI::V3 { + for (dir, ports) in opts.net.iter() { + let mut access = BitFlags::empty(); + if dir.inbound { + access.insert(make_bitflags!(AccessNet::BindTcp)); + } + if dir.outbound { + access.insert(make_bitflags!(AccessNet::ConnectTcp)) + } + for port in ports { + ruleset = ruleset + .add_rule(NetPort::new(*port, access)) + .context("adding tcp rule")?; + } + } + } else if !opts.net.is_empty() { + return Err(anyhow!( + "tcp controls are only supported with Landlock ABI 3 or higher" + )); + } + Ok(ruleset) +} + +fn insert_fs_rules(mut ruleset: RulesetCreated, opts: &Yoke, abi: ABI) -> Result { // allow each path specified, grouped by access specifier - for (perms, paths) in opts.fs { + for (perms, paths) in opts.fs.iter() { let mut access = BitFlags::empty(); if perms.read { access.insert(make_bitflags!(AccessFs::{ReadFile | ReadDir})); @@ -192,36 +171,110 @@ examples .add_rules(path_beneath_rules(paths, access)) .context("adding fs rule")?; } + Ok(ruleset) +} - // allow each tcp action specified, grouped by access specifier - if abi >= ABI::V3 { - for (dir, ports) in opts.tcp { - let mut access = BitFlags::empty(); - if dir.inbound { - access.insert(make_bitflags!(AccessNet::BindTcp)); - } - if dir.outbound { - access.insert(make_bitflags!(AccessNet::ConnectTcp)) - } - for port in ports { - ruleset = ruleset - .add_rule(NetPort::new(port, access)) - .context("adding tcp rule")?; - } - } - } else if !opts.tcp.is_empty() { - return Err(anyhow!( - "tcp controls are only supported with Landlock ABI 3 or higher" - )); +fn recv_fd_args() -> Result { + let mut fd = unsafe { File::from_raw_fd(3) }; + let mut raw_args = String::new(); + fd.read_to_string(&mut raw_args)?; + let args = raw_args + .split_ascii_whitespace() + .map(|s| s.to_string()) + .collect::>(); + parse::parse_args(args.into_iter()) +} + +fn configure_landlock_access(mut preempt: Ruleset, opts: &Yoke, abi: ABI) -> Result { + // disallow signals to other processes + if !opts.signals && abi >= ABI::V6 { + preempt = preempt.scope(Scope::Signal).context("scoping signals")?; } + // disallow connections to unix domain sockets + if !opts.sockets && abi >= ABI::V6 { + preempt = preempt + .scope(Scope::AbstractUnixSocket) + .context("scoping sockets")?; + } + + // lock down fs access + preempt = if !opts.unsandbox.fs { + preempt + .handle_access(AccessFs::from_all(abi)) + .context("handling fs access")? + } else { + preempt + }; + + // lock down tcp access + preempt = if !(opts.unsandbox.net || abi < ABI::V3) { + preempt + .handle_access(AccessNet::BindTcp) + .context("handling tcp bind access")? + .handle_access(AccessNet::ConnectTcp) + .context("handling tcp conn access")? + } else { + preempt + }; + + Ok(preempt) +} + +fn main() -> Result<()> { + let args = std::env::args().skip(1); + let mut opts = parse::parse_args(args)?; + + if opts.fd_args { + opts.merge(recv_fd_args()?); + } + + if opts.exec.is_empty() { + print_help(); + std::process::exit(1); + } + + // set up scope of our intial ruleset + let mut preempt = Ruleset::default(); + + let compat = if opts.besteffort { + landlock::CompatLevel::BestEffort + } else { + landlock::CompatLevel::HardRequirement + }; + preempt = preempt.set_compatibility(compat); + + #[cfg(feature = "abi-2")] + let abi = ABI::V2; + #[cfg(feature = "abi-3")] + let abi = ABI::V3; + #[cfg(feature = "abi-4")] + let abi = ABI::V4; + #[cfg(feature = "abi-5")] + let abi = ABI::V5; + #[cfg(feature = "abi-6")] + let abi = ABI::V6; + + // configure what landlock will restrict access to + preempt = configure_landlock_access(preempt, &opts, abi)?; + + // ensure no conflicting configuration options were provided + check_conflicts(&opts)?; + + // create ruleset and begin inserting rules + let mut ruleset = preempt.create().context("creating ruleset")?; + + ruleset = insert_fs_rules(ruleset, &opts, abi)?; + + ruleset = insert_tcp_rules(ruleset, &opts, abi)?; + // locate our executable #[cfg(feature = "cli")] let fullpath = which::which(&opts.exec[0]).context("finding executable")?; #[cfg(not(feature = "cli"))] let fullpath = &opts.exec[0]; - // add executeable as read+execute + // add executable as read+execute if !opts.unsandbox.fs { ruleset = ruleset.add_rules(path_beneath_rules( std::slice::from_ref(&fullpath), @@ -230,45 +283,13 @@ examples } // if requested, trace dependencies and add as read+execute - #[cfg(feature = "cli")] if opts.ldd { - #[cfg(not(feature = "nix"))] - let loader = DynamicLoader::options() - .search_dirs(glibc::get_search_dirs(PathBuf::from_str("/")?)?) - .new_loader(); - - #[cfg(feature = "nix")] - let loader = DynamicLoader::options() - .search_dirs(glibc::get_hard_coded_search_dirs(None)?) - .new_loader(); - - let mut tree = DependencyTree::new(); - let mut queue = VecDeque::new(); - queue.push_back(fullpath.clone()); - while let Some(path) = queue.pop_front() { - let deps = loader.resolve_dependencies(&path, &mut tree)?; - queue.extend(deps); - } - ruleset = ruleset - .add_rules(path_beneath_rules( - tree.into_iter().fold(Vec::new(), |mut acc, (_, deps)| { - acc.extend(deps); - acc - }), - AccessFs::from_read(abi), - )) - .context("tracking dependencies")?; + ruleset = trace_deps(ruleset, abi, &fullpath)?; } // enforce the ruleset on ourselves ruleset.restrict_self().context("enforcing ruleset")?; - // construct a command for the target program - let mut cmd = exec::Command::new(fullpath); - if opts.exec.len() > 1 { - cmd.args(&opts.exec[1..]); - } - // clear env unless retention is requested if !opts.retain_env { for (k, _) in std::env::vars() { @@ -287,8 +308,158 @@ examples } } + // TODO bind mounts, pivot_root, chroot + + // do we need to use commandext pre_exec, + // or copy unshare's fork_and_wait behaviour ? + let mut spaced = false; + + unsafe { + use nix::libc::*; + let ns = opts.unshare; + let mut flag = 0; + let pairs = [ + (ns.mount, CLONE_NEWNS), + (ns.uts, CLONE_NEWUTS), + (ns.ipc, CLONE_NEWIPC), + (ns.pid, CLONE_NEWPID), + (ns.net, CLONE_NEWNET), + (ns.user, CLONE_NEWUSER), + (ns.cgroup, CLONE_NEWCGROUP), + (ns.time, CLONE_NEWTIME), + ]; + for (_, cloneflag) in pairs.iter().filter(|(o, _)| *o) { + flag |= cloneflag; + } + if flag != 0 && unshare(flag) == -1 { + let e = Errno::last(); + if opts.besteffort { + eprintln!("failed to enter namespace: {e}") + } else { + return Err(anyhow!("failed to enter namespace: {e}")); + } + }; + } + + if opts.unshare.mount && opts.chroot { + let pid = std::process::id(); + let new_root = { + let mut template = CString::from_str(format!("yoke-{pid}-XXXXXX").as_str())?.into_raw(); + PathBuf::from_str(&(unsafe { CString::from_raw(mkdtemp(template)) }.into_string()?))? + }; + for (perms, paths) in opts.fs.iter() { + for path in paths { + let target = new_root.clone(); + target.push(path); + create_dir_all(&target)?; + mount(path, target); + } + } + // see bubblewrap.c lines 1260.. + // bind mount all the paths in fs + // mount(source, target, fstype, flags, data)?; + todo!(); + // pivot_root(new_root, put_old); + } + // execute and hopefully never return - let err = cmd.exec(); - eprintln!("failed to run process: {}", err); + let exec_path = CString::from_str( + fullpath + .to_str() + .ok_or(anyhow!("could not convert exec path to string"))?, + )?; + let argv = if opts.exec.len() > 1 { + let mut args = Vec::new(); + for arg in &opts.exec[1..] { + args.push(CString::from_str(arg)?); + } + args + } else { + Vec::new() + }; + + nix::unistd::execv(&exec_path, &argv)?; + let e = nix::errno::Errno::last(); + eprintln!("failed to run process: {e}"); Ok(()) } + +fn print_help() { + eprintln!( + " +yoke -- simple sandboxer + +use: yoke [ruletype] [space separated rules] -- [command] + +rules +------------ + filesystem: + --fs | -f [access]=/path:/another/path + + tcp port: + --tcp | -t [access]=1234:5678 + + env vars: + --env | -e [key]=[value] + + retain inherited env vars: + --retain-env | -r + + allow use of unix domain sockets: + --sockets | -s + + allow sending signals to other processes: + --signals | -k + + best-effort (no error on sandbox failures): + --best-effort | -b + + resolve process dependencies and add to sandbox (requires feature): + --ldd | -l + + unsandbox: + --no-fs | -nf + --no-net | -nn + + isolate namespace: + --namespace | -n [specifier] + where specifiers are: + m - mount + h - host/uts + i - ipc + n - network + p - pid + u - user + c - cgroup + t - time + a - all + + change root: + --chroot | -c + + accept additional rules from fd 3: + --fd-args | -fd + + +access specifiers +------------ + fs: + r - read + w - write + x - execute + i - ioctl + d - set as device (if using mount namespace) + + tcp: + i - in/bind + o - out/connect + +examples +------------ + yoke --fs r=/etc -l -- ls /etc + + yoke --tcp io=80:443 --fs r=/srv/web --clear-env --env SERVE_FROM=/srv/web \ + -- myhttpserver + " + ); +} diff --git a/src/namespaces.rs b/src/namespaces.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/namespaces.rs @@ -0,0 +1 @@ + diff --git a/src/parse.rs b/src/parse.rs index cd005d0..3acea4a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,4 +1,4 @@ -use crate::types::{Direction, Permissions, Yoke}; +use crate::types::{Direction, Permissions, Unshare, Yoke}; use anyhow::{Context, Result, anyhow}; use std::{collections::HashMap, path::PathBuf, str::FromStr}; @@ -6,9 +6,67 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; enum Arg { Unset, Filesystem, - Tcp, + Net, Env, Exec, + Namespace, + // Chroot, +} + +// fn chroot_parse(paths: &[String]) -> Result { +// if paths.len() > 1 { +// return Err(anyhow!("only one chroot path may be provided")); +// } +// Ok(PathBuf::from_str(&paths[0])?.canonicalize()?) +// } + +fn namespace_parse(flags: &[String]) -> Result { + let mut unshare = Unshare::default(); + for set in flags { + for c in set.chars() { + match c { + 'm' => { + unshare.mount = true; + } + 'h' => { + unshare.uts = true; + } + 'i' => { + unshare.ipc = true; + } + 'n' => { + unshare.net = true; + } + 'p' => { + unshare.pid = true; + } + 'u' => { + unshare.user = true; + } + 'c' => { + unshare.cgroup = true; + } + 't' => { + unshare.time = true; + } + 'a' => { + unshare.mount = true; + unshare.uts = true; + unshare.ipc = true; + unshare.net = true; + unshare.pid = true; + unshare.user = true; + unshare.cgroup = true; + unshare.time = true; + return Ok(unshare); + } + _ => { + return Err(anyhow!("invalid unshare flag specified")); + } + } + } + } + Ok(unshare) } fn env_parse(pairs: &[String]) -> Result> { @@ -46,6 +104,9 @@ fn fs_parse(pairs: &[String]) -> Result>> { 'i' => { perms.ioctl = true; } + 'd' => { + perms.dev = true; + } s => return Err(anyhow!("invalid access specifier {}", s)), } } @@ -63,10 +124,10 @@ fn fs_parse(pairs: &[String]) -> Result>> { Ok(rules) } -fn tcp_parse(pairs: &[String]) -> Result>> { +fn net_parse(pairs: &[String]) -> Result>> { let mut rules = HashMap::new(); for pair in pairs { - let (s_io, s_port) = pair.split_once('=').context("invalid tcp pair")?; + let (s_io, s_port) = pair.split_once('=').context("invalid net pair")?; let mut dir = Direction::default(); for c in s_io.chars() { match c { @@ -77,7 +138,7 @@ fn tcp_parse(pairs: &[String]) -> Result>> { dir.outbound = true; } _ => { - return Err(anyhow!("invalid tcp specifier")); + return Err(anyhow!("invalid net specifier")); } } } @@ -103,9 +164,14 @@ pub fn parse_args(args: impl Iterator) -> Result { match cur_arg { Unset => (), Filesystem => yoke.fs.extend(fs_parse(collector)?), - Tcp => yoke.tcp.extend(tcp_parse(collector)?), + Net => yoke.net.extend(net_parse(collector)?), Env => yoke.env.extend(env_parse(collector)?), Exec => yoke.exec.extend(collector.iter().cloned()), + Namespace => { + yoke.unshare = namespace_parse(collector)?; + } // Chroot => { + // yoke.chroot = Some(chroot_parse(collector)?); + // } } Ok(()) } @@ -120,13 +186,18 @@ pub fn parse_args(args: impl Iterator) -> Result { "--tcp" | "-t" => { collect_args(&mut yoke, &collector, &cur_arg)?; collector.clear(); - cur_arg = Tcp; + cur_arg = Net; } "--env" | "-e" => { collect_args(&mut yoke, &collector, &cur_arg)?; collector.clear(); cur_arg = Env; } + "--namespace" | "-n" => { + collect_args(&mut yoke, &collector, &cur_arg)?; + collector.clear(); + cur_arg = Namespace; + } "--sockets" | "-s" => { collect_args(&mut yoke, &collector, &cur_arg)?; collector.clear(); @@ -145,7 +216,18 @@ pub fn parse_args(args: impl Iterator) -> Result { cur_arg = Unset; yoke.retain_env = true; } - + "--best-effort" | "-b" => { + collect_args(&mut yoke, &collector, &cur_arg)?; + collector.clear(); + cur_arg = Unset; + yoke.besteffort = true; + } + "--chroot" | "-c" => { + collect_args(&mut yoke, &collector, &cur_arg)?; + collector.clear(); + cur_arg = Unset; + yoke.chroot = true; + } #[cfg(feature = "cli")] "--ldd" | "-l" => { collect_args(&mut yoke, &collector, &cur_arg)?; @@ -160,11 +242,11 @@ pub fn parse_args(args: impl Iterator) -> Result { cur_arg = Unset; yoke.unsandbox.fs = true; } - "--no-tcp" | "-nt" => { + "--no-net" | "-nn" => { collect_args(&mut yoke, &collector, &cur_arg)?; collector.clear(); cur_arg = Unset; - yoke.unsandbox.tcp = true; + yoke.unsandbox.net = true; } "--fd-args" | "-fd" => { collect_args(&mut yoke, &collector, &cur_arg)?; diff --git a/src/types.rs b/src/types.rs index 3cdf8d0..ca54463 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,24 +1,28 @@ use anyhow::Result; use std::{collections::HashMap, path::PathBuf}; -#[derive(Debug, Default)] -pub struct Unsandbox { - pub fs: bool, - pub tcp: bool, -} - #[derive(Debug, Default)] pub struct Yoke { pub fs: HashMap>, - pub tcp: HashMap>, + pub net: HashMap>, pub env: HashMap, + pub binds: HashMap>, + pub unshare: Unshare, + pub unsandbox: Unsandbox, + pub chroot: bool, pub retain_env: bool, pub signals: bool, pub sockets: bool, - pub unsandbox: Unsandbox, pub ldd: bool, pub fd_args: bool, pub exec: Vec, + pub besteffort: bool, +} + +#[derive(Debug, Default)] +pub struct Unsandbox { + pub fs: bool, + pub net: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] @@ -27,6 +31,7 @@ pub struct Permissions { pub write: bool, pub execute: bool, pub ioctl: bool, + pub dev: bool, } #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Default)] @@ -35,10 +40,22 @@ pub struct Direction { pub outbound: bool, } +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Default)] +pub struct Unshare { + pub mount: bool, + pub uts: bool, + pub ipc: bool, + pub net: bool, + pub pid: bool, + pub user: bool, + pub cgroup: bool, + pub time: bool, +} + impl Yoke { pub fn merge(&mut self, other: Yoke) { self.fs.extend(other.fs); - self.tcp.extend(other.tcp); + self.net.extend(other.net); self.env.extend(other.env); } }