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 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 $@ hare build -o $@
install: 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, hareHook,
iproute2, iproute2,
iptables,
lib,
passt,
procps, procps,
stdenv, stdenv,
util-linux, util-linux,
@@ -10,11 +13,20 @@
version = "0.1.0"; version = "0.1.0";
src = ./.; src = ./.;
nativeBuildInputs = [ hareHook ]; nativeBuildInputs = [
makeFlags = [ "PREFIX=${builtins.placeholder "out"}" ]; hareHook
which
];
makeFlags = [
"PREFIX=${builtins.placeholder "out"}"
"IP=${lib.getExe' iproute2 "ip"}"
"IPTABLES=${lib.getExe' iptables "iptables"}"
"PASTA=${lib.getExe' passt "pasta"}"
];
nativeCheckInputs = [ nativeCheckInputs = [
iproute2 iproute2 # for `ip`
procps # for `ps` procps # for `ps`
util-linux # for `setsid` util-linux # for `setsid`
which which

View File

@@ -1,5 +1,6 @@
// vim: set shiftwidth=2 : // vim: set shiftwidth=2 :
use config;
use errors; use errors;
use errors::ext; use errors::ext;
use fmt; use fmt;
@@ -10,22 +11,25 @@ use os::exec;
use restrict; use restrict;
use rt; use rt;
use rt::ext; use rt::ext;
use strings;
use unix; use unix;
fn setup_pasta(net: restrict::net_subset) void = { fn setup_pasta(net: restrict::net_subset) void = {
// `pasta PID [options]`: creates a device in the netns of PID. // `pasta PID [options]`: creates a device in the netns of PID.
// ordering: // ordering:
// 1. fork // 1. fork into (P, C1)
// 2. child: enter netns, signal parent. // 2. child C1: enter netns, signal parent P.
// 3. parent: fork and spawn `pasta --pid /proc/fd/$N` // 3. parent P: fork and spawn `pasta --pid /proc/fd/$N` (C2)
// then dumbly wait on child until completion // then dumbly wait on child (C2) until completion
// 4. child: wait for `pasta` to signal readiness on fd `$N` // 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()!; 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()) { match (os::exec::fork()) {
case let child_pid: os::exec::process => case let child_pid: os::exec::process =>
io::close(pipe_child_wr)!; 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); // log::printfln("[namespace/pasta] notify {}", notify_fd_path);
// defer free(notify_fd_path); // defer free(notify_fd_path);
// TODO: append dns, gateway arguments to `pasta`. // TODO: append dns argument to `pasta`.
yield rt::ext::execvpe( let pasta_args = [
"pasta", "pasta",
[ "--ipv4-only",
"pasta", // pasta `up`s `lo` regardless of this flag; `--config-net` just tells
"--ipv4-only", // it to assign an IP and routes to the new device it creates
// pasta `up`s `lo` regardless of this flag; `--config-net` just tells "--config-net",
// it to assign an IP and routes to the new device it creates // port forwards:
"--config-net", "-u", "none",
// port forwards: "-t", "none",
"-u", "none", "-U", "none",
"-t", "none", "-T", "none",
"-U", "none", // "-U", "53", #< if using the host's DNS
"-T", "none", // "-T", "53", #< if using the host's DNS
// "-U", "53", #< if using the host's DNS "--outbound-if4", net.dev,
// "-T", "53", #< if using the host's DNS "--gateway", net.gateway,
"--outbound-if4", net.dev, "--netns-only",
"--gateway", net.gateway, // pidstr,
"--netns-only", "--netns", netns_path,
// pidstr, // "--pid", notify_fd_path,
"--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(), 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 // forward dns to the desired endpoint
let dnsdest = fmt::asprintf("{}:53", net.dns); let dnsdest = fmt::asprintf("{}:53", net.dns);
defer free(dnsdest); defer free(dnsdest);
let rc = rt::ext::shellvpe( let rc = rt::ext::shellvpe_or_default(
config::IPTABLES,
"iptables", "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 // 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. // 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",
[ [
"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); 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",
[ [
"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. // run the provided command in a new process, and wait for its return.
// under the hood: // under the hood:
// 1. fork // 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) = { fn wait_child(child_pid: os::exec::process) (os::exec::status | os::exec::error) = {
for (true) { for (true) {
match (os::exec::wait(&child_pid)) { 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. // 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. // 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 = { fn _execvpe(path: str, name: str, argv: []str, envp: []str = []) rt::errno = {
let status = rt::ENOENT; let status = rt::ENOENT;
let pieces = strings::split(path, ":"); let pieces = strings::split(path, ":");