Join -r/--run containers to networks: veth creation, IPs, routes

Commit 4/6 of the network isolation feature (docs/networking-design.md).
network_join.{h,cpp}: join_networks() waits (bounded, polling) for the
session's own isolated net namespace to exist -- bwrap's outer pid
never enters it, and the on_bwrap_pid_known callback fires before
bwrap has even started its own setup -- then per network: ensures it's
provisioned, creates a veth pair where the bridge lives, attaches the
bridge side, moves the container side into the session's namespace as
eth<N>, assigns it a free address, brings it up, and (extern only)
replaces the default route.

sandbox_process.{h,cpp}: generalized pid_namespace_isolated() into
namespace_isolated(outer_pid, ns_pid, ns_type) so this can reuse it for
"net" instead of "pid".

network_subnet.{h,cpp}: gateway-address logic generalized into
host_address(af, cidr, n) shared by the existing gateway functions
(n=1) and new ipv4/ipv6_host_address() (n=2, 3, ... for containers).

network_bridge.{h,cpp}: bridge_name()/wrap_for_network() exported so
network_join.cpp can attach to the exact bridge/namespace
network_bridge.cpp provisioned.

commands.cpp: run_container() validates network namespace isolation is
actually available before ever starting bwrap (can't be degraded the
way --hostname is), then joins networks from on_bwrap_pid_known,
before the -D/--daemonize report is sent.

Real bug caught by testing, fixed before landing: address allocation
first tried to detect in-use IPs via `ip addr show master <bridge>`,
but a container's address lives on its own interface inside its own
private namespace, invisible from the bridge's namespace -- two
concurrent containers on the same network both got 10.168.0.2. Fixed
with a flock-based per-address lease file (same technique pid_file.h's
SessionLock already uses), verified with two containers running
simultaneously getting distinct addresses.

Known, documented limitation: a very short-lived sandboxed command can
exit before the namespace-wait polling catches up (bwrap execs
straight into the target with no hook point in between namespace
creation and exec); real long-running networked services are
unaffected.

Verified end-to-end as root (via a scoped doas rule): two containers
on the same intern network got distinct addresses and could ping each
other; an intern-joined container could not reach the outside; an
extern-joined container reached the real internet through NAT; a
container joining both simultaneously got two working interfaces.
This commit is contained in:
2026-08-30 13:11:28 +00:00
parent 24b8ddcce7
commit 8cc967e748
12 changed files with 567 additions and 63 deletions
+23
View File
@@ -16,8 +16,31 @@
#pragma once
#include <string>
#include <vector>
#include "config_file.h"
// A stable, deterministic, <=15-character (Linux's IFNAMSIZ - 1 limit)
// interface name for `network_name`'s bridge: "slk" + 8 hex chars of a
// hand-rolled FNV-1a hash (not std::hash<std::string>(), whose value is
// implementation-defined and not guaranteed stable across a rebuild with a
// different standard library, which would silently orphan an
// already-provisioned bridge). Exported (not just this file's own internal
// helper) so network_join.h can attach a container's veth to the exact same
// bridge this file provisioned.
std::string bridge_name(const std::string& network_name);
// Wraps `argv` so it runs wherever `network`'s bridge actually lives: as-is
// for `extern` (the host's own root namespace -- this whole feature is
// root-only for now, so slocker-lite's own current namespace already is the
// right one); through `nsenter --net=<persistent path>` for `intern` (its
// own dedicated namespace, persistent_netns.h). Exported so network_join.h
// can run its own veth-setup commands (creating the pair, attaching the
// bridge-side end) in that same place, not just this file's own
// provisioning commands.
std::vector<std::string> wrap_for_network(const NetworkEntry& network, std::vector<std::string> argv);
// Checks that the external tools provisioning `network` needs are found in
// PATH, logging which are missing: always `ip`; `iptables`/`sysctl` (+
// `ip6tables` if `network.ipv6`) for `extern`; `nsenter` for `intern` (to