Compare commits
No commits in common. "working" and "main" have entirely different histories.
7 changed files with 283 additions and 509 deletions
97
Cargo.lock
generated
97
Cargo.lock
generated
|
|
@ -21,16 +21,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
name = "cc"
|
||||
version = "1.2.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elb"
|
||||
|
|
@ -82,6 +80,17 @@ 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"
|
||||
|
|
@ -92,6 +101,32 @@ 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"
|
||||
|
|
@ -127,18 +162,6 @@ 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"
|
||||
|
|
@ -164,12 +187,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"errno 0.3.14",
|
||||
"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"
|
||||
|
|
@ -218,6 +247,28 @@ 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"
|
||||
|
|
@ -245,7 +296,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"elb-dl",
|
||||
"exec",
|
||||
"landlock",
|
||||
"nix",
|
||||
"which",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ license = "GPL-3.0-or-later"
|
|||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = [ "abi-6", "cli", "compat-besteffort" ]
|
||||
default = [ "abi-6", "cli" ]
|
||||
cli = ["dep:elb-dl", "dep:which"]
|
||||
nix = []
|
||||
abi-1 = []
|
||||
|
|
@ -17,8 +17,6 @@ abi-3 = []
|
|||
abi-4 = []
|
||||
abi-5 = []
|
||||
abi-6 = []
|
||||
compat-besteffort = []
|
||||
compat-require = []
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
|
@ -29,6 +27,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"] }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
rustPlatform,
|
||||
features ? [ ],
|
||||
|
|
@ -36,9 +35,6 @@ 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;
|
||||
|
|
|
|||
535
src/main.rs
535
src/main.rs
|
|
@ -1,127 +1,148 @@
|
|||
mod namespaces;
|
||||
mod parse;
|
||||
mod types;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use landlock::{
|
||||
ABI, Access, AccessFs, AccessNet, BitFlags, Compatible, NetPort, Ruleset, RulesetAttr,
|
||||
RulesetCreated, RulesetCreatedAttr, Scope, make_bitflags, path_beneath_rules,
|
||||
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::{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,
|
||||
};
|
||||
use std::{collections::VecDeque, path::PathBuf, str::FromStr};
|
||||
use std::{fs::File, io::Read, os::fd::FromRawFd};
|
||||
|
||||
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)"
|
||||
));
|
||||
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);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
if opts.exec.is_empty() {
|
||||
// print help
|
||||
eprintln!(
|
||||
"
|
||||
yoke -- simple sandboxer
|
||||
|
||||
#[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)
|
||||
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);
|
||||
}
|
||||
|
||||
// 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")?
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"yoke was not compiled with the cli feature, cannot use --ldd"
|
||||
))
|
||||
}
|
||||
}
|
||||
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();
|
||||
// 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 = "nix")]
|
||||
let loader = DynamicLoader::options()
|
||||
.search_dirs(glibc::get_hard_coded_search_dirs(None)?)
|
||||
.new_loader();
|
||||
// create ruleset and begin inserting rules
|
||||
let mut ruleset = preempt.create().context("creating ruleset")?;
|
||||
|
||||
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.iter() {
|
||||
for (perms, paths) in opts.fs {
|
||||
let mut access = BitFlags::empty();
|
||||
if perms.read {
|
||||
access.insert(make_bitflags!(AccessFs::{ReadFile | ReadDir}));
|
||||
|
|
@ -171,110 +192,36 @@ fn insert_fs_rules(mut ruleset: RulesetCreated, opts: &Yoke, abi: ABI) -> Result
|
|||
.add_rules(path_beneath_rules(paths, access))
|
||||
.context("adding fs rule")?;
|
||||
}
|
||||
Ok(ruleset)
|
||||
}
|
||||
|
||||
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")?;
|
||||
// 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"
|
||||
));
|
||||
}
|
||||
|
||||
// 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 executable as read+execute
|
||||
// add executeable as read+execute
|
||||
if !opts.unsandbox.fs {
|
||||
ruleset = ruleset.add_rules(path_beneath_rules(
|
||||
std::slice::from_ref(&fullpath),
|
||||
|
|
@ -283,13 +230,45 @@ fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
// if requested, trace dependencies and add as read+execute
|
||||
#[cfg(feature = "cli")]
|
||||
if opts.ldd {
|
||||
ruleset = trace_deps(ruleset, abi, &fullpath)?;
|
||||
#[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")?;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
|
|
@ -308,158 +287,8 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// 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 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}");
|
||||
let err = cmd.exec();
|
||||
eprintln!("failed to run process: {}", err);
|
||||
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 +0,0 @@
|
|||
|
||||
102
src/parse.rs
102
src/parse.rs
|
|
@ -1,4 +1,4 @@
|
|||
use crate::types::{Direction, Permissions, Unshare, Yoke};
|
||||
use crate::types::{Direction, Permissions, Yoke};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||
|
||||
|
|
@ -6,67 +6,9 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
|||
enum Arg {
|
||||
Unset,
|
||||
Filesystem,
|
||||
Net,
|
||||
Tcp,
|
||||
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>> {
|
||||
|
|
@ -104,9 +46,6 @@ 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)),
|
||||
}
|
||||
}
|
||||
|
|
@ -124,10 +63,10 @@ fn fs_parse(pairs: &[String]) -> Result<HashMap<Permissions, Vec<PathBuf>>> {
|
|||
Ok(rules)
|
||||
}
|
||||
|
||||
fn net_parse(pairs: &[String]) -> Result<HashMap<Direction, Vec<u16>>> {
|
||||
fn tcp_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 net pair")?;
|
||||
let (s_io, s_port) = pair.split_once('=').context("invalid tcp pair")?;
|
||||
let mut dir = Direction::default();
|
||||
for c in s_io.chars() {
|
||||
match c {
|
||||
|
|
@ -138,7 +77,7 @@ fn net_parse(pairs: &[String]) -> Result<HashMap<Direction, Vec<u16>>> {
|
|||
dir.outbound = true;
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("invalid net specifier"));
|
||||
return Err(anyhow!("invalid tcp specifier"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,14 +103,9 @@ pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
|
|||
match cur_arg {
|
||||
Unset => (),
|
||||
Filesystem => yoke.fs.extend(fs_parse(collector)?),
|
||||
Net => yoke.net.extend(net_parse(collector)?),
|
||||
Tcp => yoke.tcp.extend(tcp_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(())
|
||||
}
|
||||
|
|
@ -186,18 +120,13 @@ pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
|
|||
"--tcp" | "-t" => {
|
||||
collect_args(&mut yoke, &collector, &cur_arg)?;
|
||||
collector.clear();
|
||||
cur_arg = Net;
|
||||
cur_arg = Tcp;
|
||||
}
|
||||
"--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();
|
||||
|
|
@ -216,18 +145,7 @@ 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)?;
|
||||
|
|
@ -242,11 +160,11 @@ pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
|
|||
cur_arg = Unset;
|
||||
yoke.unsandbox.fs = true;
|
||||
}
|
||||
"--no-net" | "-nn" => {
|
||||
"--no-tcp" | "-nt" => {
|
||||
collect_args(&mut yoke, &collector, &cur_arg)?;
|
||||
collector.clear();
|
||||
cur_arg = Unset;
|
||||
yoke.unsandbox.net = true;
|
||||
yoke.unsandbox.tcp = true;
|
||||
}
|
||||
"--fd-args" | "-fd" => {
|
||||
collect_args(&mut yoke, &collector, &cur_arg)?;
|
||||
|
|
|
|||
47
src/types.rs
47
src/types.rs
|
|
@ -2,27 +2,23 @@ use anyhow::Result;
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Yoke {
|
||||
pub fs: HashMap<Permissions, Vec<PathBuf>>,
|
||||
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 ldd: bool,
|
||||
pub fd_args: bool,
|
||||
pub exec: Vec<String>,
|
||||
pub besteffort: bool,
|
||||
pub struct Unsandbox {
|
||||
pub fs: bool,
|
||||
pub tcp: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Unsandbox {
|
||||
pub fs: bool,
|
||||
pub net: bool,
|
||||
pub struct Yoke {
|
||||
pub fs: HashMap<Permissions, Vec<PathBuf>>,
|
||||
pub tcp: HashMap<Direction, Vec<u16>>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub retain_env: bool,
|
||||
pub signals: bool,
|
||||
pub sockets: bool,
|
||||
pub unsandbox: Unsandbox,
|
||||
pub ldd: bool,
|
||||
pub fd_args: bool,
|
||||
pub exec: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
|
||||
|
|
@ -31,7 +27,6 @@ pub struct Permissions {
|
|||
pub write: bool,
|
||||
pub execute: bool,
|
||||
pub ioctl: bool,
|
||||
pub dev: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Default)]
|
||||
|
|
@ -40,22 +35,10 @@ 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.net.extend(other.net);
|
||||
self.tcp.extend(other.tcp);
|
||||
self.env.extend(other.env);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue