bunpen: fix --bunpen-net pasta integration to work even when pasta isnt on PATH

This commit is contained in:
2024-11-09 09:00:17 +00:00
parent 870a09282f
commit 3abf1fdff8
5 changed files with 104 additions and 36 deletions

View File

@@ -1,6 +1,21 @@
all: bunpen
bunpen:
# these are *runtime* dependencies
IP ?= $(shell which ip)
IPTABLES ?= $(shell which iptables)
PASTA ?= $(shell which pasta)
config/runtime_deps.ha: config/runtime_deps.ha.in
test -x "$(IP)"
test -x "$(IPTABLES)"
test -x "$(PASTA)"
sed -e "s:@IP@:$(IP):" \
-e "s:@IPTABLES@:$(IPTABLES):" \
-e "s:@PASTA@:$(PASTA):" \
$^ > $@
cat $@
bunpen: config/runtime_deps.ha
hare build -o $@
install:

View File

@@ -0,0 +1,6 @@
// the sandboxer sometimes shells out to these binary helpers;
// build script is expected to patch in the right paths at build time.
export const IP: str = "@IP@";
export const IPTABLES: str = "@IPTABLES@";
export const PASTA: str = "@PASTA@";

View File

@@ -1,6 +1,9 @@
{
hareHook,
iproute2,
iptables,
lib,
passt,
procps,
stdenv,
util-linux,
@@ -10,11 +13,20 @@
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ hareHook ];
makeFlags = [ "PREFIX=${builtins.placeholder "out"}" ];
nativeBuildInputs = [
hareHook
which
];
makeFlags = [
"PREFIX=${builtins.placeholder "out"}"
"IP=${lib.getExe' iproute2 "ip"}"
"IPTABLES=${lib.getExe' iptables "iptables"}"
"PASTA=${lib.getExe' passt "pasta"}"
];
nativeCheckInputs = [
iproute2
iproute2 # for `ip`
procps # for `ps`
util-linux # for `setsid`
which

View File

@@ -1,5 +1,6 @@
// vim: set shiftwidth=2 :
use config;
use errors;
use errors::ext;
use fmt;
@@ -10,22 +11,25 @@ use os::exec;
use restrict;
use rt;
use rt::ext;
use strings;
use unix;
fn setup_pasta(net: restrict::net_subset) void = {
// `pasta PID [options]`: creates a device in the netns of PID.
// ordering:
// 1. fork
// 2. child: enter netns, signal parent.
// 3. parent: fork and spawn `pasta --pid /proc/fd/$N`
// then dumbly wait on child until completion
// 4. child: wait for `pasta` to signal readiness on fd `$N`
// 1. fork into (P, C1)
// 2. child C1: enter netns, signal parent P.
// 3. parent P: fork and spawn `pasta --pid /proc/fd/$N` (C2)
// then dumbly wait on child (C2) until completion
// 4. child C1:: wait for `pasta` to signal readiness on fd `$N`
//
// after that, we can continue on & exec into the user code
// after that, C1 can continue on & exec into the user code
//
// TODO: this currently lacks the synchronization described above
// TODO: this currently lacks the synchronization described above.
// it still generally _works_, but possible that some applications are flaky
let (pipe_parent_rd, pipe_child_wr) = unix::pipe()!;
log::printfln("[namespace/pasta]: forking: parent will launch pasta while child will exec user code");
match (os::exec::fork()) {
case let child_pid: os::exec::process =>
io::close(pipe_child_wr)!;
@@ -78,29 +82,32 @@ fn attach_pasta(net: restrict::net_subset, child: os::exec::process) (void | os:
// log::printfln("[namespace/pasta] notify {}", notify_fd_path);
// defer free(notify_fd_path);
// TODO: append dns, gateway arguments to `pasta`.
yield rt::ext::execvpe(
// TODO: append dns argument to `pasta`.
let pasta_args = [
"pasta",
[
"pasta",
"--ipv4-only",
// pasta `up`s `lo` regardless of this flag; `--config-net` just tells
// it to assign an IP and routes to the new device it creates
"--config-net",
// port forwards:
"-u", "none",
"-t", "none",
"-U", "none",
"-T", "none",
// "-U", "53", #< if using the host's DNS
// "-T", "53", #< if using the host's DNS
"--outbound-if4", net.dev,
"--gateway", net.gateway,
"--netns-only",
// pidstr,
"--netns", netns_path,
// "--pid", notify_fd_path,
],
"--ipv4-only",
// pasta `up`s `lo` regardless of this flag; `--config-net` just tells
// it to assign an IP and routes to the new device it creates
"--config-net",
// port forwards:
"-u", "none",
"-t", "none",
"-U", "none",
"-T", "none",
// "-U", "53", #< if using the host's DNS
// "-T", "53", #< if using the host's DNS
"--outbound-if4", net.dev,
"--gateway", net.gateway,
"--netns-only",
// pidstr,
"--netns", netns_path,
// "--pid", notify_fd_path,
];
log::printfln("[namespace/pasta]: invoking pasta: {}", strings::join(" ", pasta_args...));
yield rt::ext::execvpe_or_default(
config::PASTA,
"pasta",
pasta_args,
os::getenvs(),
);
};
@@ -116,7 +123,8 @@ fn config_routing_in_ns(net: restrict::net_subset) (void | os::exec::error | rt:
// forward dns to the desired endpoint
let dnsdest = fmt::asprintf("{}:53", net.dns);
defer free(dnsdest);
let rc = rt::ext::shellvpe(
let rc = rt::ext::shellvpe_or_default(
config::IPTABLES,
"iptables",
[
"iptables",
@@ -139,7 +147,8 @@ fn config_routing_in_ns(net: restrict::net_subset) (void | os::exec::error | rt:
// therefore, set it up *now*, and delete the addrs, and then since it's
// already up they won't re-appear when we exec pasta.
let rc = rt::ext::shellvpe(
let rc = rt::ext::shellvpe_or_default(
config::IP,
"ip",
[
"ip",
@@ -150,7 +159,8 @@ fn config_routing_in_ns(net: restrict::net_subset) (void | os::exec::error | rt:
)?;
log::printfln("[namespace/pasta] 'ip link set lo up' exited {}", rc.status);
let rc = rt::ext::shellvpe(
let rc = rt::ext::shellvpe_or_default(
config::IP,
"ip",
[
"ip",

View File

@@ -37,6 +37,16 @@ export fn execvpe(name: str, argv: []str, envp: []str = []) rt::errno = {
};
};
// try `execvpe(name, argv, envp)`,
// but if it fails to find `name` on PATH, then `execve(default, argv, envp)`
export fn execvpe_or_default(default: str, name: str, argv: []str, envp: []str = []) rt::errno = {
let rc = execvpe(name, argv, envp);
return switch (rc) {
case rt::ENOENT => yield execve(default, argv, envp);
case => yield rc;
};
};
// run the provided command in a new process, and wait for its return.
// under the hood:
// 1. fork
@@ -53,6 +63,19 @@ export fn shellvpe(name: str, argv: []str, envp: []str = []) (os::exec::status |
};
};
// same as `shellvpe(name, argv, envp)` but fallback to `name=default` if `name`
// isn't found on PATH
export fn shellvpe_or_default(default: str, name: str, argv: []str, envp: []str = []) (os::exec::status | os::exec::error) = {
return match (os::exec::fork()) {
case void =>
errors::ext::check("shellvpe", execvpe_or_default(default, name, argv, envp));
assert(false, "unreachable");
return os::exec::status { ... };
case let child_pid: os::exec::process =>
yield wait_child(child_pid);
};
};
fn wait_child(child_pid: os::exec::process) (os::exec::status | os::exec::error) = {
for (true) {
match (os::exec::wait(&child_pid)) {
@@ -82,6 +105,8 @@ fn to_cstr_array(strs: []str) []nullable *const c::char = {
// split `path` on `:`, crawl it, and try executing until we succeed.
// note that 1:1 parity with libc's execvpe is probably not what we have here.
//
// returns ENOENT if unable to locate the command on PATH
fn _execvpe(path: str, name: str, argv: []str, envp: []str = []) rt::errno = {
let status = rt::ENOENT;
let pieces = strings::split(path, ":");