This commit is contained in:
atagen 2025-11-17 09:20:25 +11:00
parent b11ab8a728
commit 4febac2085
7 changed files with 502 additions and 276 deletions

97
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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::<Vec<String>>();
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<RulesetCreated> {
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<RulesetCreated> {
#[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<RulesetCreated> {
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<RulesetCreated> {
// 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<Yoke> {
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::<Vec<String>>();
parse::parse_args(args.into_iter())
}
fn configure_landlock_access(mut preempt: Ruleset, opts: &Yoke, abi: ABI) -> Result<Ruleset> {
// 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
"
);
}

1
src/namespaces.rs Normal file
View file

@ -0,0 +1 @@

View file

@ -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<PathBuf> {
// 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<Unshare> {
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<HashMap<String, String>> {
@ -46,6 +104,9 @@ fn fs_parse(pairs: &[String]) -> Result<HashMap<Permissions, Vec<PathBuf>>> {
'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<HashMap<Permissions, Vec<PathBuf>>> {
Ok(rules)
}
fn tcp_parse(pairs: &[String]) -> Result<HashMap<Direction, Vec<u16>>> {
fn net_parse(pairs: &[String]) -> Result<HashMap<Direction, Vec<u16>>> {
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<HashMap<Direction, Vec<u16>>> {
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<Item = String>) -> Result<Yoke> {
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<Item = String>) -> Result<Yoke> {
"--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<Item = String>) -> Result<Yoke> {
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<Item = String>) -> Result<Yoke> {
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)?;

View file

@ -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<Permissions, Vec<PathBuf>>,
pub tcp: HashMap<Direction, Vec<u16>>,
pub net: HashMap<Direction, Vec<u16>>,
pub env: HashMap<String, String>,
pub binds: HashMap<Permissions, Vec<PathBuf>>,
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<String>,
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);
}
}