bunpen: get pasta working

full of race conditions and weird edge cases (some of which may have existed before)
This commit is contained in:
2024-09-21 03:40:26 +00:00
parent b105e774b3
commit 3dff60397e
2 changed files with 120 additions and 4 deletions

View File

@@ -1,12 +1,15 @@
// vim: set shiftwidth=2 :
use errors;
use errors::ext;
use fmt;
use log;
use os;
use os::exec;
use restrict;
use rt;
use rt::ext;
use time;
fn setup_pasta(net: restrict::net_subset) void = {
// `pasta PID [options]`: creates a device in the netns of PID.
@@ -23,24 +26,102 @@ fn setup_pasta(net: restrict::net_subset) void = {
match (os::exec::fork()) {
case let child_pid: os::exec::process =>
errors::ext::check("setup_pasta: attach", attach_pasta(child_pid));
time::sleep(100*time::MILLISECOND); //< TODO: BAD
errors::ext::check("setup_pasta: attach", attach_pasta(net, child_pid));
errors::ext::check("setup_pasta: wait", wait_and_propagate(child_pid));
case void =>
errors::ext::check("namespace: unshare net", rt::ext::unshare(rt::ext::clone_flag::NEWNET));
// drop enough caps so that pasta has permissions to enter our new netns.
// but retain caps so that we can manipulate the net devices after pasta setup.
// TODO: this interferes badly with no_new_privs!! figure out why pasta
// can't enter our ns.
let res = restrict::resources {
caps = rt::ext::CAPS_NONE, net = restrict::net_all, ...
};
restrict::caps_add(&res.caps, rt::ext::cap::NET_ADMIN);
restrict::capability_restrict(&res);
time::sleep(1000*time::MILLISECOND); //< TODO: BAD
errors::ext::check("setup_pasta: config routing", config_routing_in_ns(net));
case let e: os::exec::error =>
errors::ext::check("setup_pasta: fork", e);
};
};
// spawn pasta as a separate process, and have it attach to the netns of the given pid.
fn attach_pasta(child: os::exec::process) (void | os::exec::error | rt::errno) = {
fn attach_pasta(net: restrict::net_subset, child: os::exec::process) (void | os::exec::error | rt::errno) = {
return match (os::exec::fork()?) {
case let pasta_pid: os::exec::process => yield void;
case void =>
// pasta needs permissions to create a device in the netns (it apparently
// won't raise those caps itself). TODO: reduce these resources!
let res = restrict::resources {
caps = rt::ext::CAPS_ALL, net = restrict::net_all, ...
};
restrict::capability_restrict(&res);
let pidstr = fmt::asprint(child: int);
let pidpath = fmt::asprintf("/proc/{}/ns/net", child: int);
defer free(pidstr);
let pasta_args = [ "pasta", pidstr ];
// TODO: append dns, gateway arguments to `pasta`.
yield rt::ext::execvpe("pasta", pasta_args, os::getenvs());
yield rt::ext::execvpe(
"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", pidpath,
],
os::getenvs(),
);
};
};
fn config_routing_in_ns(net: restrict::net_subset) (void | os::exec::error) = {
// forward dns to the desired endpoint
let dnsdest = fmt::asprintf("{}:53", net.dns);
defer free(dnsdest);
let rc = rt::ext::shellvpe(
"iptables",
[
"iptables",
"-A", "OUTPUT",
"-t", "nat",
"-p", "udp",
"--dport", "53",
"-m", "iprange",
"--dst-range", "127.0.0.1-127.0.0.255",
"-j", "DNAT",
"--to-destination", dnsdest,
],
os::getenvs(),
)?;
log::printfln("[namespace/pasta] iptables exited {}", rc.status);
// remove the loopback routing, else iptables-based DNS forwarding (probably)
// doesn't work
let rc = rt::ext::shellvpe(
"ip",
[
"ip",
"addr",
"del", "127.0.0.1/8",
"dev", "lo",
],
os::getenvs(),
)?;
log::printfln("[namespace/pasta] ip addr exited {}", rc.status);
};

View File

@@ -1,7 +1,10 @@
// vim: set shiftwidth=2 :
use errors;
use errors::ext;
use log;
use os;
use os::exec;
use path;
use rt;
use strings;
@@ -34,6 +37,38 @@ export fn execvpe(name: str, argv: []str, envp: []str = []) rt::errno = {
};
};
// run the provided command in a new process, and wait for its return.
// under the hood:
// 1. fork
// 2. in child: execvpe
// 3. in parent: wait for child
export fn shellvpe(name: str, argv: []str, envp: []str = []) (os::exec::status | os::exec::error) = {
return match (os::exec::fork()) {
case void =>
errors::ext::check("shellvpe", execvpe(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)) {
case let e: os::exec::error => match (e) {
case errors::interrupted =>
// i guess before the days of `poll`, `wait` had to wait on either the
// child OR a signal sent to this pid; so we need to retry if the
// reason we woke isn't because the child died...
void;
case => return e;
};
case let status: os::exec::status => return status;
};
};
};
// allocate and return a NULL-terminated array of pointers to c strings.
// caller is responsible for free'ing the resulting array AND its strings.
fn to_cstr_array(strs: []str) []nullable *const c::char = {