bunpen: implement --bunpen-autodetect
This commit is contained in:
@@ -5,6 +5,7 @@ use os;
|
|||||||
export type error = !void;
|
export type error = !void;
|
||||||
|
|
||||||
export type cli_opts = struct {
|
export type cli_opts = struct {
|
||||||
|
autodetect: (void | autodetect),
|
||||||
// command to `exec` within the sandbox
|
// command to `exec` within the sandbox
|
||||||
cmd: []str,
|
cmd: []str,
|
||||||
// `--bunpen-debug`
|
// `--bunpen-debug`
|
||||||
@@ -18,6 +19,14 @@ export type cli_opts = struct {
|
|||||||
run_paths: []str,
|
run_paths: []str,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type autodetect = enum {
|
||||||
|
EXISTING,
|
||||||
|
EXISTING_FILE,
|
||||||
|
EXISTING_FILE_OR_PARENT,
|
||||||
|
EXISTING_OR_PARENT,
|
||||||
|
PARENT,
|
||||||
|
};
|
||||||
|
|
||||||
export fn usage() void = {
|
export fn usage() void = {
|
||||||
fmt::println("bunpen: run a program within an environment where access to external resources (files, net) is restricted (i.e. sandbox)")!;
|
fmt::println("bunpen: run a program within an environment where access to external resources (files, net) is restricted (i.e. sandbox)")!;
|
||||||
fmt::println("USAGE: bunpen [sandbox-arg ...] program [sandbox-arg|program-arg ...] [--] [program-arg ...]")!;
|
fmt::println("USAGE: bunpen [sandbox-arg ...] program [sandbox-arg|program-arg ...] [--] [program-arg ...]")!;
|
||||||
@@ -29,25 +38,11 @@ export fn usage() void = {
|
|||||||
fmt::println(" show this message")!;
|
fmt::println(" show this message")!;
|
||||||
fmt::println(" --bunpen-debug[=n]")!;
|
fmt::println(" --bunpen-debug[=n]")!;
|
||||||
fmt::println(" print debug messages to stderr")!;
|
fmt::println(" print debug messages to stderr")!;
|
||||||
fmt::println(" omit `n` for light debugging, or specify n=0/1/2/3 where higher = more verbose")!;
|
fmt::println(" omit `n` for light debugging, or specify n=0/1/2/3/4 where higher = more verbose")!;
|
||||||
fmt::println(" --bunpen-drop-shell")!;
|
fmt::println(" --bunpen-drop-shell")!;
|
||||||
fmt::println(" instead of running the program, drop into an interactive shell")!;
|
fmt::println(" instead of running the program, drop into an interactive shell")!;
|
||||||
// fmt::println(" --bunpen-disable")!;
|
fmt::println(" --bunpen-autodetect <existing|existingFile|existingFileOrParent|existingOrParent|parent>")!;
|
||||||
// fmt::println(" invoke the program directly, instead of inside a sandbox")!;
|
fmt::println(" add files which appear later as CLI arguments into the sandbox")!;
|
||||||
// fmt::println(" --bunpen-dry-run")!;
|
|
||||||
// fmt::println(" show what would be `exec`uted but do not perform any action")!;
|
|
||||||
// fmt::println(" --bunpen-method <bwrap|capshonly|pastaonly|landlock|none>")!;
|
|
||||||
// fmt::println(" use a specific sandboxer")!;
|
|
||||||
// fmt::println(" --bunpen-autodetect <existing|existingFile|existingFileOrParent|existingOrParent|parent>")!;
|
|
||||||
// fmt::println(" add files which appear later as CLI arguments into the sandbox")!;
|
|
||||||
// fmt::println(" --bunpen-cap <all|sys_admin|net_raw|net_admin|...>")!;
|
|
||||||
// fmt::println(" allow the sandboxed program to use the provided linux capability (both inside and outside the sandbox)")!;
|
|
||||||
// fmt::println(" special cap "all" to preserve all capabilities possible")!;
|
|
||||||
// fmt::println(" --bunpen-net-dev <iface>|all")!;
|
|
||||||
// fmt::println(" --bunpen-net-gateway <ip-address>")!;
|
|
||||||
// fmt::println(" --bunpen-dns <server>|host")!;
|
|
||||||
// fmt::println(" --bunpen-keep-namespace <all|cgroup|ipc|net|pid|uts>")!;
|
|
||||||
// fmt::println(" do not unshare the provided linux namespace")!;
|
|
||||||
fmt::println(" --bunpen-keep-net")!;
|
fmt::println(" --bunpen-keep-net")!;
|
||||||
fmt::println(" allow unrestricted access to the network")!;
|
fmt::println(" allow unrestricted access to the network")!;
|
||||||
fmt::println(" --bunpen-path <path>")!;
|
fmt::println(" --bunpen-path <path>")!;
|
||||||
@@ -60,6 +55,20 @@ export fn usage() void = {
|
|||||||
// fmt::println(" --bunpen-add-pwd")!;
|
// fmt::println(" --bunpen-add-pwd")!;
|
||||||
// fmt::println(" shorthand for `--bunpen-path $PWD`")!;
|
// fmt::println(" shorthand for `--bunpen-path $PWD`")!;
|
||||||
// fmt::println("")!;
|
// fmt::println("")!;
|
||||||
|
// fmt::println(" --bunpen-disable")!;
|
||||||
|
// fmt::println(" invoke the program directly, instead of inside a sandbox")!;
|
||||||
|
// fmt::println(" --bunpen-dry-run")!;
|
||||||
|
// fmt::println(" show what would be `exec`uted but do not perform any action")!;
|
||||||
|
// fmt::println(" --bunpen-method <bwrap|capshonly|pastaonly|landlock|none>")!;
|
||||||
|
// fmt::println(" use a specific sandboxer")!;
|
||||||
|
// fmt::println(" --bunpen-cap <all|sys_admin|net_raw|net_admin|...>")!;
|
||||||
|
// fmt::println(" allow the sandboxed program to use the provided linux capability (both inside and outside the sandbox)")!;
|
||||||
|
// fmt::println(" special cap "all" to preserve all capabilities possible")!;
|
||||||
|
// fmt::println(" --bunpen-net-dev <iface>|all")!;
|
||||||
|
// fmt::println(" --bunpen-net-gateway <ip-address>")!;
|
||||||
|
// fmt::println(" --bunpen-dns <server>|host")!;
|
||||||
|
// fmt::println(" --bunpen-keep-namespace <all|cgroup|ipc|net|pid|uts>")!;
|
||||||
|
// fmt::println(" do not unshare the provided linux namespace")!;
|
||||||
// fmt::println("the following environment variables are also considered and propagated to children:")!;
|
// fmt::println("the following environment variables are also considered and propagated to children:")!;
|
||||||
// fmt::println(" BUNPEN_DISABLE=1")!;
|
// fmt::println(" BUNPEN_DISABLE=1")!;
|
||||||
// fmt::println(" equivalent to `--bunpen-disable`")!;
|
// fmt::println(" equivalent to `--bunpen-disable`")!;
|
||||||
@@ -72,7 +81,7 @@ export fn usage() void = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export fn parse_args(args: []str) (cli_opts | error) = {
|
export fn parse_args(args: []str) (cli_opts | error) = {
|
||||||
let parsed = cli_opts { ... };
|
let parsed = cli_opts { autodetect = void, ... };
|
||||||
|
|
||||||
for (let idx: size = 0; idx < len(args); idx += 1) {
|
for (let idx: size = 0; idx < len(args); idx += 1) {
|
||||||
let arg = args[idx];
|
let arg = args[idx];
|
||||||
@@ -81,11 +90,13 @@ export fn parse_args(args: []str) (cli_opts | error) = {
|
|||||||
next = &args[idx+1];
|
next = &args[idx+1];
|
||||||
};
|
};
|
||||||
switch (arg) {
|
switch (arg) {
|
||||||
|
case "--bunpen-autodetect" => idx += 1; parsed.autodetect = autodetect_fromstr(expect_arg("--bunpen-autodetect", next)?)?;
|
||||||
case "--bunpen-debug" => parsed.debug = 2;
|
case "--bunpen-debug" => parsed.debug = 2;
|
||||||
case "--bunpen-debug=0" => parsed.debug = 0;
|
case "--bunpen-debug=0" => parsed.debug = 0;
|
||||||
case "--bunpen-debug=1" => parsed.debug = 1;
|
case "--bunpen-debug=1" => parsed.debug = 1;
|
||||||
case "--bunpen-debug=2" => parsed.debug = 2;
|
case "--bunpen-debug=2" => parsed.debug = 2;
|
||||||
case "--bunpen-debug=3" => parsed.debug = 3;
|
case "--bunpen-debug=3" => parsed.debug = 3;
|
||||||
|
case "--bunpen-debug=4" => parsed.debug = 4;
|
||||||
case "--bunpen-drop-shell" => parsed.drop_shell = true;
|
case "--bunpen-drop-shell" => parsed.drop_shell = true;
|
||||||
case "--bunpen-help" => parsed.help = true;
|
case "--bunpen-help" => parsed.help = true;
|
||||||
case "--bunpen-home-path" => idx += 1; append(parsed.home_paths, expect_arg("--bunpen-home-path", next)?);
|
case "--bunpen-home-path" => idx += 1; append(parsed.home_paths, expect_arg("--bunpen-home-path", next)?);
|
||||||
@@ -105,3 +116,14 @@ fn expect_arg(name: str, value: nullable *str) (str | error) = {
|
|||||||
case let v: *str => yield *v;
|
case let v: *str => yield *v;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn autodetect_fromstr(v: str) (autodetect | error) = {
|
||||||
|
return switch (v) {
|
||||||
|
case "existing" => yield autodetect::EXISTING;
|
||||||
|
case "existingFile" => yield autodetect::EXISTING_FILE;
|
||||||
|
case "existingFileOrParent" => yield autodetect::EXISTING_FILE_OR_PARENT;
|
||||||
|
case "existingOrParent" => yield autodetect::EXISTING_OR_PARENT;
|
||||||
|
case "parent" => yield autodetect::PARENT;
|
||||||
|
case => yield error;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
// vim: set shiftwidth=2 :
|
// vim: set shiftwidth=2 :
|
||||||
// ingest literal `cli_opts` into a more computer-friendly form
|
// ingest literal `cli_opts` into a more computer-friendly form
|
||||||
|
|
||||||
|
use fs;
|
||||||
use log;
|
use log;
|
||||||
use os;
|
use os;
|
||||||
use path;
|
use path;
|
||||||
|
use rt;
|
||||||
|
use rtext;
|
||||||
|
|
||||||
// the user requested to see help.
|
// the user requested to see help.
|
||||||
export type help = !void;
|
export type help = !void;
|
||||||
@@ -46,15 +49,6 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
|
|||||||
//---- ingest `debug` ----//
|
//---- ingest `debug` ----//
|
||||||
req.debug = opts.debug;
|
req.debug = opts.debug;
|
||||||
|
|
||||||
//---- ingest `drop_shell` ----//
|
|
||||||
if (opts.drop_shell) {
|
|
||||||
// ignore the original command, and overwrite it with an interactive shell.
|
|
||||||
// TODO: respect the user's `$SHELL`.
|
|
||||||
req.exec_bin = "/bin/sh";
|
|
||||||
delete(req.exec_args[..]);
|
|
||||||
append(req.exec_args, "sh");
|
|
||||||
};
|
|
||||||
|
|
||||||
//---- ingest `help` ----//
|
//---- ingest `help` ----//
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
return help;
|
return help;
|
||||||
@@ -72,21 +66,43 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
|
|||||||
//---- ingest `run_paths` ----//
|
//---- ingest `run_paths` ----//
|
||||||
ingest_paths(&req.paths, opts.run_paths, os::getenv("XDG_RUNTIME_DIR"));
|
ingest_paths(&req.paths, opts.run_paths, os::getenv("XDG_RUNTIME_DIR"));
|
||||||
|
|
||||||
|
//---- ingest `autodetect` (must be done after exec_args) ----//
|
||||||
|
match (opts.autodetect) {
|
||||||
|
case let method: autodetect =>
|
||||||
|
// N.B.: skip first arg, since that's the name of the executable and
|
||||||
|
// surely not an argument
|
||||||
|
ingest_autodetect(&req.paths, req.exec_args[1..], method);
|
||||||
|
case void => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
//---- ingest `drop_shell` (must be done after autodetect) ----//
|
||||||
|
if (opts.drop_shell) {
|
||||||
|
// ignore the original command, and overwrite it with an interactive shell.
|
||||||
|
// TODO: respect the user's `$SHELL`.
|
||||||
|
req.exec_bin = "/bin/sh";
|
||||||
|
delete(req.exec_args[..]);
|
||||||
|
append(req.exec_args, "sh");
|
||||||
|
};
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert each item in `from` to a path, relative to `base`, and append to `into`.
|
// convert each item in `from` to a path, relative to `base`, and append to `into`.
|
||||||
|
// if `allow_abs`, then paths with start with `/` are treated as absolute,
|
||||||
|
// instead of as relative to `base`.
|
||||||
fn ingest_paths(into: *[]path::buffer, from: []str, base: (str | void), allow_abs: bool = false) void = {
|
fn ingest_paths(into: *[]path::buffer, from: []str, base: (str | void), allow_abs: bool = false) void = {
|
||||||
for (let path_str .. from) {
|
for (let path_str .. from) {
|
||||||
match (get_path(path_str, base, allow_abs)) {
|
match (get_path(path_str, base, allow_abs)) {
|
||||||
case let p: path::buffer => append(into, p);
|
case let p: path::buffer => append(into, p);
|
||||||
case let e: path::error =>
|
case let e: path::error =>
|
||||||
log::printfln("[translate_opts] omitting path {}: {}", path_str, path::strerror(e));
|
log::printfln("[config] omitting path {}: {}", path_str, path::strerror(e));
|
||||||
case missing_base => log::printfln("[translate_opts] omitting path {}: no base dir", path_str);
|
case let e: missing_base =>
|
||||||
|
log::printfln("[config] omitting path {}: {}", translate_strerror(e));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// path was specified in relative form, but the directory to base that on was omitted.
|
||||||
type missing_base = !void;
|
type missing_base = !void;
|
||||||
// `ingest_paths` helper: transforms `path_str` into an absolute path.
|
// `ingest_paths` helper: transforms `path_str` into an absolute path.
|
||||||
fn get_path(path_str: str, base: (str | void), allow_abs: bool) (path::buffer | path::error | missing_base) = {
|
fn get_path(path_str: str, base: (str | void), allow_abs: bool) (path::buffer | path::error | missing_base) = {
|
||||||
@@ -97,3 +113,79 @@ fn get_path(path_str: str, base: (str | void), allow_abs: bool) (path::buffer |
|
|||||||
case void => return missing_base;
|
case void => return missing_base;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// processes arguments from `consider` in the context of `--bunpen-autodetect METHOD`,
|
||||||
|
// and appends any extra paths wanted in the sandbox to `into` in response.
|
||||||
|
fn ingest_autodetect(into: *[]path::buffer, consider: []str, method: autodetect) void = {
|
||||||
|
let base = os::getcwd();
|
||||||
|
for (let path_str .. consider) {
|
||||||
|
match (get_path(path_str, base, true)) {
|
||||||
|
case let p: path::buffer => try_as_path(into, p, method);
|
||||||
|
case let e: path::error =>
|
||||||
|
log::printfln("[config/path] omitting path {}: {}", path_str, path::strerror(e));
|
||||||
|
case let e: missing_base =>
|
||||||
|
log::printfln("[configa/path] omitting path {}: {}", translate_strerror(e));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// consider the `pathbuf` in the context of the autodetect `method`, and append
|
||||||
|
// either that path, its parent, or neither.
|
||||||
|
fn try_as_path(into: *[]path::buffer, pathbuf: path::buffer, method: autodetect) void = {
|
||||||
|
let pathstr = path::string(&pathbuf);
|
||||||
|
switch (method) {
|
||||||
|
case autodetect::EXISTING =>
|
||||||
|
append_if_exists(into, [pathstr], true);
|
||||||
|
case autodetect::EXISTING_FILE =>
|
||||||
|
append_if_exists(into, [pathstr], false);
|
||||||
|
case autodetect::EXISTING_FILE_OR_PARENT =>
|
||||||
|
append_if_exists(into, [pathstr], false)
|
||||||
|
|| append_if_exists(into, [pathstr, ".."], true);
|
||||||
|
case autodetect::EXISTING_OR_PARENT =>
|
||||||
|
append_if_exists(into, [pathstr], true)
|
||||||
|
|| append_if_exists(into, [pathstr, ".."], true);
|
||||||
|
case autodetect::PARENT =>
|
||||||
|
append_if_exists(into, [pathstr, ".."], true);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// append the file encoded by `comps`, but only if it exists and is file-like.
|
||||||
|
// if it exists but as a directory, append it only so long as `dir_ok = true`.
|
||||||
|
fn append_if_exists(into: *[]path::buffer, comps: []str, dir_ok: bool) bool = {
|
||||||
|
let pathbuf = path::buffer { ... };
|
||||||
|
let do_append = match (deref_stat(&pathbuf, comps)) {
|
||||||
|
case let st: fs::filestat =>
|
||||||
|
yield (st.mode & rt::S_IFDIR) == 0 || dir_ok;
|
||||||
|
case let e: fs::error =>
|
||||||
|
log::printfln("[config/path/try] failed to deref/stat {}: {}", path::string(&pathbuf), fs::strerror(e));
|
||||||
|
yield false;
|
||||||
|
case let e: path::error =>
|
||||||
|
log::printfln("[config/path/try] failed to deref/stat: {}", path::strerror(e));
|
||||||
|
yield false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (do_append) {
|
||||||
|
log::printfln("[config/path] autodetected path: {}", path::string(&pathbuf));
|
||||||
|
append(into, pathbuf);
|
||||||
|
} else {
|
||||||
|
log::printfln("[config/path/try] not adding path: {}", path::string(&pathbuf));
|
||||||
|
};
|
||||||
|
|
||||||
|
return do_append;
|
||||||
|
};
|
||||||
|
|
||||||
|
// invoke `stat` on the path encoded by `comps`.
|
||||||
|
// if the path is a symlink, deref through the symlinks and stat the target.
|
||||||
|
fn deref_stat(pathbuf: *path::buffer, comps: []str) (fs::filestat | fs::error | path::error) = {
|
||||||
|
*pathbuf = path::init(comps...)?;
|
||||||
|
let pathstr = fs::realpath(os::cwd, path::string(pathbuf))?;
|
||||||
|
return fs::stat(os::cwd, pathstr);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn translate_strerror(e: (missing_base | fs::error | path::error)) str = {
|
||||||
|
return match (e) {
|
||||||
|
case let e: missing_base => yield "non-absolute path, but unknown as to which base directory";
|
||||||
|
case let e: fs::error => yield fs::strerror(e);
|
||||||
|
case let e: path::error => yield path::strerror(e);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user