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:
@@ -136,8 +136,9 @@ Source layout (all under `src/`):
|
||||
are the direct network equivalents (`Mode::network`/`Mode::list_networks`/
|
||||
`Mode::delete_network`) — see `docs/networking-design.md` for the full feature
|
||||
design and `config_file.{h,cpp}` below for `NetworkEntry`. Joining a network
|
||||
from `-r/--run` isn't wired up yet (a later commit in the design doc's
|
||||
sequence). `create_network_command()` rejects a duplicate name first, then
|
||||
from `-r/--run` (repeatable `-n <name>`, `ParsedArgs::network_specs`, see
|
||||
`cli_args.{h,cpp}` above) is handled by `run_container()`, further below,
|
||||
via `network_join.{h,cpp}` (see below). `create_network_command()` rejects a duplicate name first, then
|
||||
resolves `subnet`/`subnet6`: an explicit `--subnet`/`--subnet6` is validated
|
||||
(`is_valid_ipv4_cidr()`/`is_valid_ipv6_cidr()`) and checked for overlap
|
||||
against every existing network's subnet (`ipv4_cidrs_overlap()`/
|
||||
@@ -201,7 +202,21 @@ Source layout (all under `src/`):
|
||||
`run_container()`'s existing body completely unchanged, including the
|
||||
unmount/cleanup that already runs after `run_bwrap()` returns (no separate
|
||||
watcher/reaper — the daemonized child *is* what runs the whole session,
|
||||
start to finish).
|
||||
start to finish). `network_specs` (repeatable `-n`, `cli_args.h`) is
|
||||
validated up front, before `run_bwrap()` is ever called: joining a network
|
||||
needs a real, isolated network namespace to attach a veth into (unlike
|
||||
`--hostname`, this can't just be skipped/degraded when unavailable), so if
|
||||
any networks were requested, `run_container()` checks both that
|
||||
`namespace_config.net` is actually enabled (`global.unshare-net`,
|
||||
`config_file.h`) and that `detect_bwrap_unshare_args()` (`bwrap.h`) reports
|
||||
the running kernel actually supports `--unshare-net` — either failing sets
|
||||
`ok = false` with a clear error, the same pattern as a failed `--user`
|
||||
resolution. If validation passed, `on_bwrap_pid_known` (already used for
|
||||
`-D/--daemonize`'s `report_daemon_started()`) additionally calls
|
||||
`join_networks(pid, network_specs, app_config)` (`network_join.h`, see
|
||||
below) — networks are joined *before* the daemonize report is sent, so a
|
||||
`-D`-daemonized caller doesn't get control back until network setup has
|
||||
already had its chance to run.
|
||||
- `self_test.{h,cpp}` — `run_self_tests()` implements `-t/--test`, this
|
||||
project's own built-in self-test mode (distinct from the Meson-driven
|
||||
fixture smoke test under `tests/`, described in "Build & test commands"
|
||||
@@ -425,9 +440,10 @@ Source layout (all under `src/`):
|
||||
directory without drifting from this file's own. `xdg_state_dir()`
|
||||
(`$XDG_STATE_HOME/slocker-lite`, or the `$HOME/.local/state/...` fallback)
|
||||
is likewise exported (moved out of this file's own anonymous namespace) so
|
||||
`persistent_netns.{h,cpp}` (see below) can resolve its own subdirectory
|
||||
under the same state root without a second, drifting copy of this
|
||||
resolution logic. `session_pid_file_path()` resolves
|
||||
`persistent_netns.{h,cpp}` (see below) and `network_join.cpp`'s own
|
||||
per-address lease files (`xdg_state_dir() / "net-leases"`) can resolve
|
||||
their own subdirectories under the same state root without a second,
|
||||
drifting copy of this resolution logic. `session_pid_file_path()` resolves
|
||||
`$XDG_STATE_HOME/slocker-lite/run/<container_name>-<pid>` (falling back to
|
||||
`$HOME/.local/state/...` when `XDG_STATE_HOME` is unset/empty — same
|
||||
resolution pattern as `config_file_path()` below, for state instead of
|
||||
@@ -502,24 +518,28 @@ Source layout (all under `src/`):
|
||||
network's live state (bridge, veths, iptables rules; the persistent
|
||||
namespace itself for `intern`) survives a reboot except its `config.yaml`
|
||||
entry, so calling this again after one just recreates whatever's missing.
|
||||
`bridge_name()` derives a stable, `.cpp`-local interface name from the
|
||||
network's own name via a hand-rolled 32-bit FNV-1a (`"slk" + 8 hex chars`,
|
||||
11 characters, comfortably under Linux's `IFNAMSIZ - 1` = 15-character
|
||||
limit regardless of how long the network name is) — deliberately not
|
||||
`std::hash<std::string>`, whose exact value is implementation-defined and
|
||||
not guaranteed stable across a rebuild with a different standard library,
|
||||
which would silently orphan an already-provisioned bridge a rebuilt binary
|
||||
`bridge_name()` derives a stable interface name from the network's own name
|
||||
via a hand-rolled 32-bit FNV-1a (`"slk" + 8 hex chars`, 11 characters,
|
||||
comfortably under Linux's `IFNAMSIZ - 1` = 15-character limit regardless of
|
||||
how long the network name is) — deliberately not `std::hash<std::string>`,
|
||||
whose exact value is implementation-defined and not guaranteed stable
|
||||
across a rebuild with a different standard library, which would silently
|
||||
orphan an already-provisioned bridge a rebuilt binary
|
||||
can no longer find by the name it now computes. For `extern`, every command
|
||||
runs directly (the bridge lives in the host's own root namespace, and this
|
||||
whole feature is root-only for now — `docs/networking-design.md` — so
|
||||
`slocker-lite`'s own current namespace already is the right one). For
|
||||
`intern`, every command is wrapped through `nsenter --net=<persistent path>`
|
||||
(`wrap_for_network()`, `.cpp`-local) into the network's own dedicated
|
||||
(`wrap_for_network()`) into the network's own dedicated
|
||||
namespace (`persistent_netns.h`, created here first if it doesn't exist
|
||||
yet) — the same pattern `wrap_for_root_namespace()` (`bwrap.cpp`) already
|
||||
uses for the rootless `containers-storage` mount's namespace, just
|
||||
targeting a persistent bind-mounted path instead of a live pid's `/proc`
|
||||
entry. `provision_bridge()` (`.cpp`-local): creates the bridge, assigns it
|
||||
entry. `bridge_name()`/`wrap_for_network()` are both exported (not just
|
||||
this file's own internal helpers) specifically so `network_join.{h,cpp}`
|
||||
(see below) can attach a container's veth to the exact same bridge, in the
|
||||
exact same place, this file provisioned it in. `provision_bridge()`
|
||||
(`.cpp`-local): creates the bridge, assigns it
|
||||
the gateway address from `network_subnet.h`'s `ipv4_gateway_address()`
|
||||
(and `ipv6_gateway_address()` if `network.ipv6`), brings it up, then —
|
||||
`extern` only — `sysctl -w net.ipv4.ip_forward=1` (+ the IPv6 equivalent if
|
||||
@@ -542,6 +562,74 @@ Source layout (all under `src/`):
|
||||
all came up correctly; a real `intern` network's bridge came up inside its
|
||||
own dedicated namespace with neither forwarding nor a NAT rule, confirming
|
||||
the structural (not merely policy) isolation the design calls for.
|
||||
- `network_join.{h,cpp}` — joins a just-started `-r/--run` session to each
|
||||
network named in `-n`. `join_networks()` first waits (bounded, 3s,
|
||||
20ms-interval `nanosleep()` polling — `wait_for_isolated_net_namespace()`,
|
||||
`.cpp`-local) for `resolve_namespace_pid()` (`sandbox_process.h`) to name a
|
||||
child whose net namespace is actually isolated
|
||||
(`namespace_isolated(outer_pid, ns_pid, "net")`) — necessary because
|
||||
`run_bwrap()`'s `on_bwrap_pid_known` fires right after `fork()`, before
|
||||
`bwrap` has done any of its own namespace setup, so that child may not even
|
||||
exist yet the instant this is called; while it doesn't,
|
||||
`resolve_namespace_pid()` falls back to returning `outer_pid` itself, so
|
||||
comparing a namespace to itself naturally keeps the loop going without a
|
||||
separate "does a child exist yet" check. **Known limitation, not solved
|
||||
here**: for a very short-lived sandboxed command, the whole session can
|
||||
exit before this poll ever catches up (confirmed by testing: `-n <net> --
|
||||
echo hi` reliably timed out) — `bwrap` execs straight into the target
|
||||
command with no hook point in between namespace creation and exec, so
|
||||
there's no way for this project to guarantee network setup completes
|
||||
before a near-instant command already has too. Real (long-running)
|
||||
networked services are unaffected — confirmed by testing (see below).
|
||||
For each named network: looked up in `config.networks` (an unknown name is
|
||||
a per-network error, not fatal to the others); `ensure_network_provisioned()`
|
||||
(`network_bridge.h`) covers post-reboot recreation; a veth pair is created
|
||||
wherever that network's bridge lives (`wrap_for_network()`, reused from
|
||||
`network_bridge.h`), the bridge-side end attached and brought up, the
|
||||
container-side end moved into the session's own namespace (`ip link set
|
||||
... netns <ns_pid>`) and renamed `eth<N>` (`N` = the network's position in
|
||||
the `-n` list, so multiple joins each get a distinct interface). **Address
|
||||
allocation, `pick_free_address()`, needed a real fix during testing, not
|
||||
just design**: an interface's actual assigned IP lives inside its own
|
||||
private per-container namespace, invisible from the bridge's own namespace
|
||||
— an earlier version queried `ip -o addr show master <bridge>` (only the
|
||||
*host* side of each veth, with no address of its own, is visible there) and
|
||||
always saw nothing, so two concurrently-running containers on the same
|
||||
network were both handed the identical address (confirmed by testing:
|
||||
`10.168.0.2` twice). Fixed by giving each candidate address its own tiny
|
||||
lock file under `xdg_state_dir() / "net-leases"` (`pid_file.h`) and holding
|
||||
an exclusive, non-blocking `flock()` on it via an intentionally
|
||||
never-`close()`d fd — the same technique `pid_file.h`'s own `SessionLock`
|
||||
uses for session liveness, released automatically by the kernel the
|
||||
instant this process exits for any reason, no explicit release step or
|
||||
cleanup sweep needed. Picking a free address is then just "the first
|
||||
candidate (`network_subnet.h`'s `ipv4_host_address()`/`ipv6_host_address()`,
|
||||
`n = 2, 3, ...`) whose lock file isn't already held." For an `extern` join,
|
||||
`ip route replace default via <gateway> dev eth<N>` (`replace`, not `add`,
|
||||
so a container joining a *second* extern network doesn't fail outright with
|
||||
"File exists" — whichever extern network is joined last ends up as the
|
||||
effective default route; `intern` gets no default route at all, matching
|
||||
the design's "no route out exists" intent — the connected route for the
|
||||
local subnet is already automatic once an address is assigned, no explicit
|
||||
route command needed for same-bridge reachability regardless of kind).
|
||||
Every step failure is logged specifically (which command, which network)
|
||||
and best-effort: `join_networks()` returns `true` only if every requested
|
||||
network joined, but a failure never kills the already-running session
|
||||
(network setup can only happen after `bwrap`'s own namespace exists, i.e.
|
||||
potentially after the sandboxed command is already running). Veth teardown
|
||||
needs no explicit code: the kernel destroys an entire veth pair (both
|
||||
ends, including the one still attached to the bridge) the instant *either*
|
||||
end's owning namespace is destroyed, so a session's veths disappear on
|
||||
their own once its namespace does — only the bridge/iptables/persistent-
|
||||
namespace state is deliberately left behind (`network_bridge.h`'s
|
||||
reboot-reconciliation design). **Verified end-to-end on this dev machine
|
||||
(root, via a scoped `doas` rule)**: two concurrently-running containers on
|
||||
the same `intern` network got distinct addresses and could ping each
|
||||
other; an `intern`-joined container could not reach the outside
|
||||
(`Network unreachable`); an `extern`-joined container reached the real
|
||||
internet through the bridge's NAT; a container joining both an `intern`
|
||||
and an `extern` network simultaneously got two working interfaces
|
||||
(`eth0`/`eth1`) with neither one breaking the other.
|
||||
- `session_cgroup.{h,cpp}` — gives `--kill` (`kill_session.{h,cpp}`, see
|
||||
below) a reliable way to find every process a session ever started, however
|
||||
deeply forked/daemonized/reparented, by putting it in a dedicated cgroup v2
|
||||
@@ -600,7 +688,13 @@ Source layout (all under `src/`):
|
||||
separate pid namespace for its child (`--unshare-pid` was requested *and*
|
||||
the kernel supported it), the precondition for the kernel's own guarantee
|
||||
that killing a pid namespace's pid 1 forcibly tears down every remaining
|
||||
process in it. `collect_descendant_pids(root)` generalizes
|
||||
process in it. Generalized into `namespace_isolated(outer_pid, ns_pid,
|
||||
ns_type)` (parametrized over which `/proc/<pid>/ns/<ns_type>` entry to
|
||||
compare) once `network_join.{h,cpp}` (see below) needed the exact same
|
||||
check for `"net"` instead of `"pid"` —
|
||||
`pid_namespace_isolated()` is now just `namespace_isolated(outer_pid,
|
||||
ns_pid, "pid")`, kept as its own function since `kill_session.h` already
|
||||
depends on that exact name/signature. `collect_descendant_pids(root)` generalizes
|
||||
`resolve_namespace_pid()`'s own `/proc/<n>/stat` ppid-scanning fallback to
|
||||
collect a whole transitive tree (root included) instead of just one child,
|
||||
sharing the actual stat-parsing loop between both via a private
|
||||
@@ -896,13 +990,20 @@ Source layout (all under `src/`):
|
||||
numerically-different-from-`n` address (e.g. `n=15` becomes `...:15::/64`,
|
||||
which is hex `0x15` = 21) — purely cosmetic, allocation correctness doesn't
|
||||
depend on the two matching numerically. `ipv4_gateway_address()`/
|
||||
`ipv6_gateway_address()` (`network_bridge.cpp`'s `provision_bridge()`)
|
||||
return a network's bridge gateway address within a CIDR: masked down to its
|
||||
network address first (a shared `.cpp`-local `mask_to_network()`, in case
|
||||
the CIDR given — e.g. a manual `--subnet`/`--subnet6` — wasn't already a
|
||||
canonical network address), then `| 1` in the last bit for the `.1`
|
||||
convention this project's bridges use, reusing the same `parse_cidr()` as
|
||||
the validation/overlap functions above.
|
||||
`ipv6_gateway_address()` (`network_bridge.cpp`'s `provision_bridge()`) and
|
||||
`ipv4_host_address()`/`ipv6_host_address()` (`network_join.cpp`'s
|
||||
`pick_free_address()`, see below — `n = 2, 3, ...` for individual
|
||||
containers) are all thin wrappers around one shared `.cpp`-local
|
||||
`host_address(af, cidr, n)`: masks `cidr` down to its network address first
|
||||
(`mask_to_network()`, in case it — e.g. a manual `--subnet`/`--subnet6` —
|
||||
wasn't already a canonical network address), then adds `n` as a big-endian
|
||||
integer into the trailing host-portion bytes with proper carry propagation
|
||||
(generic over address length, so the same code handles both IPv4's 4 bytes
|
||||
and IPv6's 16 without two parallel implementations), rejecting `n` outright
|
||||
if it doesn't fit the address's host-bit width. The gateway functions are
|
||||
just `host_address(af, cidr, 1)` — the `.1` convention this project's
|
||||
bridges use. Reuses the same `parse_cidr()` as the validation/overlap
|
||||
functions above.
|
||||
- `volume_mount.{h,cpp}` — `is_valid_volume_name()` (no `/`, checked by both
|
||||
`create_volume_command()` and to tell a `-v` spec's name/path apart) and
|
||||
`resolve_volume_mount()`, called once per `-v` occurrence from `run_container()`
|
||||
|
||||
@@ -92,7 +92,7 @@ slocker-lite -V|--version
|
||||
| `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. |
|
||||
| `--delete-volume <name>` | Remove a named volume from the config. The host directory is left untouched. |
|
||||
| `--delete-volume-full <name>` | Like `--delete-volume`, but also recursively deletes the volume's host directory. |
|
||||
| `-n, --network <name>` | Create/manage a persistent named network: requires exactly one of `--extern` (a real Linux bridge in the host's own namespace, with NAT/forwarding set up so containers on it reach the host's real network) or `--intern` (a bridge inside its own dedicated, routeless namespace, only reachable by other containers on the same network). `--subnet <cidr>` overrides the auto-allocated IPv4 range (`10.168.0.0/24`, incrementing per network); `--no-ipv6` disables (and `--subnet6 <cidr>` overrides) the auto-allocated IPv6 range, on by default. With `--run`, instead joins `<name>` to the container; repeatable, no membership limit (not wired up yet, see below). Root-only for now. See [`docs/networking-design.md`](docs/networking-design.md). |
|
||||
| `-n, --network <name>` | Create/manage a persistent named network: requires exactly one of `--extern` (a real Linux bridge in the host's own namespace, with NAT/forwarding set up so containers on it reach the host's real network) or `--intern` (a bridge inside its own dedicated, routeless namespace, only reachable by other containers on the same network). `--subnet <cidr>` overrides the auto-allocated IPv4 range (`10.168.0.0/24`, incrementing per network); `--no-ipv6` disables (and `--subnet6 <cidr>` overrides) the auto-allocated IPv6 range, on by default. With `--run`, instead joins `<name>` to the container as its own `eth<N>` interface with an address from the network's subnet; repeatable, no membership limit. Root-only for now. See [`docs/networking-design.md`](docs/networking-design.md). |
|
||||
| `--list-networks` | List all named networks (see `-n/--network`) with their kind, IPv4 subnet, and IPv6 subnet (or `(no ipv6)`). |
|
||||
| `--delete-network <name>` | Remove a named network from the config. |
|
||||
| `--list-processes` | List running `--run` sessions found by their pid files under `$XDG_STATE_HOME/slocker-lite/run/`, with their pid, container name, and status (`running` or `exited`). |
|
||||
@@ -203,12 +203,13 @@ managed by `-v/--volume` (see above) rather than hand-edited — it's what
|
||||
`-r/--run`'s own `-v` usage looks named volumes up in. The `networks` section
|
||||
is likewise managed by `-n/--network` rather than hand-edited — see
|
||||
[`docs/networking-design.md`](docs/networking-design.md) for the full
|
||||
persistent-network feature design. `-n/--network` creating a network does
|
||||
stand up its real bridge/iptables state (root-only) as of this, but joining
|
||||
one from `-r/--run` isn't wired up yet, so `global.unshare-net` above is
|
||||
still the only thing actually affecting a sandboxed container's own network
|
||||
access. A missing config file is fine either way (nothing is overridden, and
|
||||
one gets created the first time `-v/--volume`/`-n/--network` is used).
|
||||
persistent-network feature design. `-n/--network` both creates a network
|
||||
(standing up its real bridge/iptables state, root-only) and, combined with
|
||||
`-r/--run`, joins a container to one or more of them with a real veth
|
||||
interface and address on each. `-p` port forwarding isn't implemented yet
|
||||
(a later commit), so nothing outside the container can reach in until then.
|
||||
A missing config file is fine either way (nothing is overridden, and one
|
||||
gets created the first time `-v/--volume`/`-n/--network` is used).
|
||||
|
||||
Run `-w/--write-config` to bootstrap a config file: it writes out every
|
||||
supported option explicitly (filling in the current or default value for
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ slocker_lite = executable('slocker-lite',
|
||||
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
|
||||
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp',
|
||||
'src/sandbox_process.cpp', 'src/session_cgroup.cpp', 'src/kill_session.cpp',
|
||||
'src/network_subnet.cpp', 'src/persistent_netns.cpp', 'src/network_bridge.cpp'],
|
||||
'src/network_subnet.cpp', 'src/persistent_netns.cpp', 'src/network_bridge.cpp',
|
||||
'src/network_join.cpp'],
|
||||
include_directories : include_directories('.'),
|
||||
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
||||
install : true)
|
||||
|
||||
+35
-4
@@ -39,6 +39,7 @@
|
||||
#include "exec_session.h"
|
||||
#include "kill_session.h"
|
||||
#include "network_bridge.h"
|
||||
#include "network_join.h"
|
||||
#include "network_subnet.h"
|
||||
#include "oci_image.h"
|
||||
#include "pid_file.h"
|
||||
@@ -546,7 +547,8 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
const std::optional<std::string>& user, const std::optional<std::string>& group,
|
||||
const std::optional<std::string>& hostname,
|
||||
const std::vector<std::pair<std::string, std::string>>& volume_specs,
|
||||
const std::vector<EnvSpec>& env_specs, bool daemonize_flag, const AppConfig& app_config) {
|
||||
const std::vector<EnvSpec>& env_specs, bool daemonize_flag,
|
||||
const std::vector<std::string>& network_specs, const AppConfig& app_config) {
|
||||
// Only depends on image_tar, so this can run before mount_image() -- moved
|
||||
// up here (rather than right before the run_bwrap() call, as before) so
|
||||
// daemonize() below can use the real container name for the log file from
|
||||
@@ -644,9 +646,38 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
app_config.unshare_uts.value_or(true), app_config.unshare_cgroup.value_or(true),
|
||||
};
|
||||
|
||||
// Joining a network needs a real, isolated network namespace to attach a
|
||||
// veth into -- can't be skipped/degraded the way e.g. --hostname is when
|
||||
// its own namespace type isn't available, so this is checked up front
|
||||
// (before ever starting bwrap) rather than discovered as a confusing
|
||||
// failure partway through join_networks().
|
||||
if (!network_specs.empty()) {
|
||||
if (!namespace_config.net) {
|
||||
spdlog::error("-n/--network requires network namespace isolation, but global.unshare-net is disabled");
|
||||
ok = false;
|
||||
} else {
|
||||
auto supported = detect_bwrap_unshare_args();
|
||||
if (std::find(supported.begin(), supported.end(), "--unshare-net") == supported.end()) {
|
||||
spdlog::error("-n/--network requires network namespace isolation, which this kernel doesn't support");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::function<void(pid_t)> on_bwrap_pid_known;
|
||||
if (daemonize_flag) {
|
||||
on_bwrap_pid_known = [&](pid_t pid) { report_daemon_started(container_name, pid); };
|
||||
if (daemonize_flag || !network_specs.empty()) {
|
||||
on_bwrap_pid_known = [&](pid_t pid) {
|
||||
// Joining networks first: report_daemon_started() below is what
|
||||
// unblocks the parent (-D/--daemonize) waiting on the pipe, so
|
||||
// network setup should have already had its chance to run by
|
||||
// then rather than racing an already-returned parent.
|
||||
if (!network_specs.empty()) {
|
||||
join_networks(pid, network_specs, app_config);
|
||||
}
|
||||
if (daemonize_flag) {
|
||||
report_daemon_started(container_name, pid);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
int exit_code = -1;
|
||||
@@ -733,7 +764,7 @@ int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config
|
||||
}
|
||||
return run_container(args.mode_arg, args.command, use_nsenter, args.user_flag, args.group_flag,
|
||||
args.hostname_flag, args.volume_specs, args.env_specs, args.daemonize_flag,
|
||||
config);
|
||||
args.network_specs, config);
|
||||
}
|
||||
}
|
||||
return 1; // unreachable
|
||||
|
||||
+6
-21
@@ -31,11 +31,8 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// Deterministic, portable 32-bit FNV-1a -- not std::hash<std::string>(),
|
||||
// whose exact value is implementation-defined and isn't guaranteed stable
|
||||
// across a rebuild with a different standard library, which would silently
|
||||
// "orphan" an already-provisioned bridge (a rebuilt binary computing a
|
||||
// different name for the same network could no longer find it).
|
||||
// Deterministic, portable 32-bit FNV-1a -- see bridge_name() below for why
|
||||
// not std::hash<std::string>().
|
||||
uint32_t fnv1a(std::string_view s) {
|
||||
uint32_t hash = 0x811c9dc5u;
|
||||
for (unsigned char c : s) {
|
||||
@@ -45,24 +42,10 @@ uint32_t fnv1a(std::string_view s) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
// "slk" + 8 hex chars = 11 characters, comfortably under Linux's 15-character
|
||||
// (IFNAMSIZ - 1) interface name limit regardless of how long `network_name`
|
||||
// is. Not human-recognizable, but doesn't need to be -- the network's own
|
||||
// identity lives in config.yaml by name; this only needs to be stable (the
|
||||
// same network always maps to the same bridge, so re-provisioning finds the
|
||||
// existing one) and collision-free at this project's expected scale (a
|
||||
// handful of networks, not thousands).
|
||||
} // namespace
|
||||
|
||||
std::string bridge_name(const std::string& network_name) { return fmt::format("slk{:08x}", fnv1a(network_name)); }
|
||||
|
||||
// For `extern` networks, commands run directly: the bridge lives in the
|
||||
// host's own root namespace, and this whole feature is root-only for now
|
||||
// (docs/networking-design.md), so slocker-lite's own current namespace
|
||||
// already *is* the right one, no wrapping needed. For `intern`, every
|
||||
// command is wrapped through nsenter into the network's own dedicated
|
||||
// persistent namespace (persistent_netns.h) -- the same pattern
|
||||
// wrap_for_root_namespace() (bwrap.cpp) already uses for reaching the
|
||||
// rootless containers-storage mount's namespace, just targeting a persistent
|
||||
// bind-mounted path instead of a live pid's /proc entry.
|
||||
std::vector<std::string> wrap_for_network(const NetworkEntry& network, std::vector<std::string> argv) {
|
||||
if (network.kind == NetworkKind::extern_) {
|
||||
return argv;
|
||||
@@ -73,6 +56,8 @@ std::vector<std::string> wrap_for_network(const NetworkEntry& network, std::vect
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool run_admin_command(const NetworkEntry& network, std::vector<std::string> argv, std::string_view what) {
|
||||
auto result = run_process(wrap_for_network(network, std::move(argv)));
|
||||
if (result.exit_code != 0) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include "network_join.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "network_bridge.h"
|
||||
#include "network_subnet.h"
|
||||
#include "pid_file.h"
|
||||
#include "process.h"
|
||||
#include "sandbox_process.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Same tiny algorithm as network_bridge.cpp's own (`.cpp`-local there too) --
|
||||
// not worth exporting/sharing a six-line hash function across files.
|
||||
uint32_t fnv1a(std::string_view s) {
|
||||
uint32_t hash = 0x811c9dc5u;
|
||||
for (unsigned char c : s) {
|
||||
hash ^= c;
|
||||
hash *= 0x01000193u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Polls (nanosleep, EINTR-retry, matching this project's existing direct-POSIX
|
||||
// style -- kill_session.cpp's poll_until() does the same) until
|
||||
// resolve_namespace_pid(outer_pid) names a child that has actually entered
|
||||
// its own net namespace, or timeout_ms elapses. While no such child exists
|
||||
// yet, resolve_namespace_pid() falls back to returning outer_pid itself, so
|
||||
// comparing a namespace to itself naturally keeps this looping rather than
|
||||
// needing a separate "does a child exist yet" check.
|
||||
std::optional<pid_t> wait_for_isolated_net_namespace(pid_t outer_pid, int timeout_ms) {
|
||||
constexpr int interval_ms = 20;
|
||||
for (int elapsed = 0; elapsed <= timeout_ms; elapsed += interval_ms) {
|
||||
pid_t ns_pid = resolve_namespace_pid(outer_pid);
|
||||
if (namespace_isolated(outer_pid, ns_pid, "net")) {
|
||||
return ns_pid;
|
||||
}
|
||||
struct timespec ts {
|
||||
0, static_cast<long>(interval_ms) * 1000000L
|
||||
};
|
||||
while (nanosleep(&ts, &ts) != 0 && errno == EINTR) {
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string> wrap_in_container(pid_t ns_pid, std::vector<std::string> argv) {
|
||||
std::vector<std::string> wrapped = {"nsenter", fmt::format("--net=/proc/{}/ns/net", ns_pid), "--"};
|
||||
wrapped.insert(wrapped.end(), argv.begin(), argv.end());
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
bool run(const std::vector<std::string>& argv, std::string_view what, std::string_view network_name) {
|
||||
auto result = run_process(argv);
|
||||
if (result.exit_code != 0) {
|
||||
spdlog::error("failed to {} for network '{}' (exit code {})", what, network_name, result.exit_code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Address allocation can't be checked by inspecting live interface state the
|
||||
// way session/cgroup liveness can: a container's actual assigned IP lives on
|
||||
// its own `eth<N>` inside its own private namespace, invisible from the
|
||||
// bridge's namespace (only the *host* side of each veth -- with no address
|
||||
// of its own -- is visible there; confirmed by testing -- an earlier version
|
||||
// of this function queried `ip -o addr show master <bridge>` and always saw
|
||||
// nothing, so every container got the same address). Instead, each candidate
|
||||
// address gets its own tiny lock file (mirroring pid_file.h's own
|
||||
// SessionLock: an exclusive, non-blocking flock() held by an intentionally
|
||||
// never-closed fd, so it's released automatically -- by the kernel -- the
|
||||
// instant this process exits for any reason, including a crash, without
|
||||
// needing an explicit release step or any cleanup sweep). Picking a free
|
||||
// address is then just "find the first candidate whose lock file isn't
|
||||
// already held."
|
||||
std::filesystem::path address_lease_path(const NetworkEntry& network, bool ipv6, uint64_t n) {
|
||||
return xdg_state_dir() / "net-leases" /
|
||||
fmt::format("{}-{}-{}", sanitize_for_filename(network.name), ipv6 ? "v6" : "v4", n);
|
||||
}
|
||||
|
||||
bool try_lock_address_lease(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
int fd = open(path.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0644);
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
// Deliberately never closed: the flock (and thus the lease) must survive
|
||||
// exactly as long as this process does, released automatically on any
|
||||
// exit -- the same reasoning create_session_lock() (pid_file.h) already
|
||||
// relies on for its own SessionLock fd.
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> pick_free_address(const NetworkEntry& network, bool ipv6) {
|
||||
constexpr uint64_t max_n = 65534; // plenty for both a /24 (254 hosts) and a bounded slice of a /64
|
||||
for (uint64_t n = 2; n <= max_n; ++n) {
|
||||
auto candidate = ipv6 ? ipv6_host_address(network.subnet6, n) : ipv4_host_address(network.subnet, n);
|
||||
if (!candidate) {
|
||||
break; // n no longer fits the host portion -- subnet exhausted
|
||||
}
|
||||
if (try_lock_address_lease(address_lease_path(network, ipv6, n))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool join_one_network(pid_t ns_pid, const NetworkEntry& network, int if_index) {
|
||||
if (!ensure_network_provisioned(network)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string bridge = bridge_name(network.name);
|
||||
// Distinct prefixes ("vh"/"vp") so the two ends' names can't collide with
|
||||
// each other while both still live in the same (bridge's) namespace,
|
||||
// right after creation and before the peer end is moved away.
|
||||
std::string suffix = fmt::format("{:08x}", fnv1a(fmt::format("{}-{}", network.name, ns_pid)));
|
||||
std::string host_veth = "vh" + suffix;
|
||||
std::string peer_veth = "vp" + suffix;
|
||||
|
||||
if (!run(wrap_for_network(network, {"ip", "link", "add", host_veth, "type", "veth", "peer", "name", peer_veth}),
|
||||
"create veth pair", network.name)) {
|
||||
return false;
|
||||
}
|
||||
if (!run(wrap_for_network(network, {"ip", "link", "set", host_veth, "master", bridge}), "attach veth to bridge",
|
||||
network.name) ||
|
||||
!run(wrap_for_network(network, {"ip", "link", "set", host_veth, "up"}), "bring host veth up",
|
||||
network.name)) {
|
||||
return false;
|
||||
}
|
||||
if (!run(wrap_for_network(network, {"ip", "link", "set", peer_veth, "netns", fmt::to_string(ns_pid)}),
|
||||
"move veth into the container's namespace", network.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string container_if = fmt::format("eth{}", if_index);
|
||||
if (!run(wrap_in_container(ns_pid, {"ip", "link", "set", peer_veth, "name", container_if}),
|
||||
"rename the container's interface", network.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto container_ip = pick_free_address(network, false);
|
||||
if (!container_ip) {
|
||||
spdlog::error("no free IPv4 address available on network '{}'", network.name);
|
||||
return false;
|
||||
}
|
||||
if (!run(wrap_in_container(ns_pid, {"ip", "addr", "add", *container_ip, "dev", container_if}),
|
||||
"assign the container's IPv4 address", network.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (network.ipv6) {
|
||||
auto container_ip6 = pick_free_address(network, true);
|
||||
if (!container_ip6) {
|
||||
spdlog::error("no free IPv6 address available on network '{}'", network.name);
|
||||
return false;
|
||||
}
|
||||
if (!run(wrap_in_container(ns_pid, {"ip", "-6", "addr", "add", *container_ip6, "dev", container_if}),
|
||||
"assign the container's IPv6 address", network.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!run(wrap_in_container(ns_pid, {"ip", "link", "set", container_if, "up"}), "bring the container's interface up",
|
||||
network.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (network.kind == NetworkKind::extern_) {
|
||||
auto gateway = ipv4_gateway_address(network.subnet);
|
||||
if (gateway) {
|
||||
std::string gateway_ip = gateway->substr(0, gateway->find('/'));
|
||||
// "replace", not "add": a container joining more than one extern
|
||||
// network would otherwise fail on the second one ("File exists")
|
||||
// -- whichever extern network is joined last ends up as the
|
||||
// effective default route.
|
||||
run(wrap_in_container(ns_pid, {"ip", "route", "replace", "default", "via", gateway_ip, "dev", container_if}),
|
||||
"set the default IPv4 route", network.name);
|
||||
}
|
||||
if (network.ipv6) {
|
||||
auto gateway6 = ipv6_gateway_address(network.subnet6);
|
||||
if (gateway6) {
|
||||
std::string gateway6_ip = gateway6->substr(0, gateway6->find('/'));
|
||||
run(wrap_in_container(
|
||||
ns_pid, {"ip", "-6", "route", "replace", "default", "via", gateway6_ip, "dev", container_if}),
|
||||
"set the default IPv6 route", network.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool join_networks(pid_t bwrap_outer_pid, const std::vector<std::string>& network_names, const AppConfig& config) {
|
||||
auto ns_pid = wait_for_isolated_net_namespace(bwrap_outer_pid, 3000);
|
||||
if (!ns_pid) {
|
||||
spdlog::error("timed out waiting for the session's own network namespace; not joining any network");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool all_ok = true;
|
||||
int if_index = 0;
|
||||
for (const auto& name : network_names) {
|
||||
auto it = std::find_if(config.networks.begin(), config.networks.end(),
|
||||
[&](const NetworkEntry& network) { return network.name == name; });
|
||||
if (it == config.networks.end()) {
|
||||
spdlog::error("no network named '{}' exists; not joining it", name);
|
||||
all_ok = false;
|
||||
continue;
|
||||
}
|
||||
if (!join_one_network(*ns_pid, *it, if_index)) {
|
||||
spdlog::error("failed to join network '{}'", name);
|
||||
all_ok = false;
|
||||
}
|
||||
++if_index;
|
||||
}
|
||||
return all_ok;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "config_file.h"
|
||||
|
||||
// Joins a just-started -r/--run session (identified by bwrap_outer_pid, the
|
||||
// same pid pid_file.h/session_cgroup.h already track) to each named network
|
||||
// in `network_names`. Waits (bounded) for the session's own isolated network
|
||||
// namespace to actually exist first -- bwrap's outer/tracked pid never
|
||||
// itself enters the namespaces it creates for its clone()'d child (see
|
||||
// sandbox_process.h's resolve_namespace_pid()), and that child may not even
|
||||
// exist yet the instant this is called (run_bwrap()'s on_bwrap_pid_known
|
||||
// fires right after fork(), before bwrap has done any of its own setup).
|
||||
//
|
||||
// For each network: ensures it's provisioned (network_bridge.h -- also
|
||||
// covers post-reboot recreation), creates a veth pair where that network's
|
||||
// bridge lives, attaches the bridge-side end, moves the container-side end
|
||||
// into the session's own namespace, assigns it a free IP from the network's
|
||||
// subnet, brings it up, and -- for an `extern` join -- makes it the
|
||||
// namespace's default route (ip route replace, so a second extern join
|
||||
// doesn't fail outright; whichever extern network is joined last ends up as
|
||||
// the effective default).
|
||||
//
|
||||
// Best-effort per network: a failure joining one network is logged loudly
|
||||
// but doesn't abort joining the rest, and never kills the already-running
|
||||
// session -- network setup can only happen after bwrap's own namespace
|
||||
// exists, i.e. after the sandboxed command may already be running. Returns
|
||||
// true only if every requested network joined successfully.
|
||||
bool join_networks(pid_t bwrap_outer_pid, const std::vector<std::string>& network_names, const AppConfig& config);
|
||||
+30
-4
@@ -98,13 +98,31 @@ void mask_to_network(std::vector<uint8_t>& addr, int prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> gateway_address(int af, const std::string& cidr) {
|
||||
// General form backing both the gateway (n=1) and per-container (n>1)
|
||||
// address functions: masks `cidr` to its network address, then adds `n` as a
|
||||
// big-endian integer into the trailing (host-portion) bytes, carrying
|
||||
// leftward same as ordinary multi-byte addition -- generic over address
|
||||
// length (4 bytes for IPv4, 16 for IPv6), not two parallel implementations.
|
||||
std::optional<std::string> host_address(int af, const std::string& cidr, uint64_t n) {
|
||||
auto parsed = parse_cidr(af, cidr);
|
||||
if (!parsed) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int addr_bits = static_cast<int>(parsed->addr.size()) * 8;
|
||||
int host_bits = addr_bits - parsed->prefix;
|
||||
if (host_bits < 64 && n >= (uint64_t{1} << host_bits)) {
|
||||
return std::nullopt; // n doesn't fit in the host portion
|
||||
}
|
||||
|
||||
mask_to_network(parsed->addr, parsed->prefix);
|
||||
parsed->addr.back() = static_cast<uint8_t>(parsed->addr.back() | 1);
|
||||
|
||||
uint64_t carry = n;
|
||||
for (auto it = parsed->addr.rbegin(); it != parsed->addr.rend() && carry != 0; ++it) {
|
||||
uint64_t sum = static_cast<uint64_t>(*it) + (carry & 0xFF);
|
||||
*it = static_cast<uint8_t>(sum & 0xFF);
|
||||
carry = (carry >> 8) + (sum >> 8);
|
||||
}
|
||||
|
||||
char buf[INET6_ADDRSTRLEN] = {};
|
||||
if (!inet_ntop(af, parsed->addr.data(), buf, sizeof(buf))) {
|
||||
@@ -177,6 +195,14 @@ std::optional<std::string> allocate_ipv6_subnet(const std::vector<NetworkEntry>&
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> ipv4_gateway_address(const std::string& cidr) { return gateway_address(AF_INET, cidr); }
|
||||
std::optional<std::string> ipv4_gateway_address(const std::string& cidr) { return host_address(AF_INET, cidr, 1); }
|
||||
|
||||
std::optional<std::string> ipv6_gateway_address(const std::string& cidr) { return gateway_address(AF_INET6, cidr); }
|
||||
std::optional<std::string> ipv6_gateway_address(const std::string& cidr) { return host_address(AF_INET6, cidr, 1); }
|
||||
|
||||
std::optional<std::string> ipv4_host_address(const std::string& cidr, uint64_t n) {
|
||||
return host_address(AF_INET, cidr, n);
|
||||
}
|
||||
|
||||
std::optional<std::string> ipv6_host_address(const std::string& cidr, uint64_t n) {
|
||||
return host_address(AF_INET6, cidr, n);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -59,3 +60,14 @@ std::optional<std::string> ipv4_gateway_address(const std::string& cidr);
|
||||
|
||||
// Same idea for IPv6.
|
||||
std::optional<std::string> ipv6_gateway_address(const std::string& cidr);
|
||||
|
||||
// The nth host address within `cidr` (n=1 is the same address
|
||||
// ipv4_gateway_address() returns -- the ".1" gateway; n=2, 3, ... are for
|
||||
// individual containers), masked to the network address first then offset by
|
||||
// n, formatted with the same prefix length. nullopt if `cidr` doesn't parse,
|
||||
// or if n doesn't fit in the address's host portion (e.g. n=300 against a
|
||||
// /24, which only has 8 host bits).
|
||||
std::optional<std::string> ipv4_host_address(const std::string& cidr, uint64_t n);
|
||||
|
||||
// Same idea for IPv6.
|
||||
std::optional<std::string> ipv6_host_address(const std::string& cidr, uint64_t n);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include <fmt/core.h>
|
||||
@@ -121,12 +122,14 @@ pid_t resolve_namespace_pid(pid_t pid) {
|
||||
return pid;
|
||||
}
|
||||
|
||||
bool pid_namespace_isolated(pid_t outer_pid, pid_t ns_pid) {
|
||||
auto outer_ns = read_ns_link(fmt::format("/proc/{}/ns/pid", outer_pid));
|
||||
auto inner_ns = read_ns_link(fmt::format("/proc/{}/ns/pid", ns_pid));
|
||||
bool namespace_isolated(pid_t outer_pid, pid_t ns_pid, std::string_view ns_type) {
|
||||
auto outer_ns = read_ns_link(fmt::format("/proc/{}/ns/{}", outer_pid, ns_type));
|
||||
auto inner_ns = read_ns_link(fmt::format("/proc/{}/ns/{}", ns_pid, ns_type));
|
||||
return outer_ns && inner_ns && *outer_ns != *inner_ns;
|
||||
}
|
||||
|
||||
bool pid_namespace_isolated(pid_t outer_pid, pid_t ns_pid) { return namespace_isolated(outer_pid, ns_pid, "pid"); }
|
||||
|
||||
std::vector<pid_t> collect_descendant_pids(pid_t root) {
|
||||
auto children_of = build_ppid_map();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// Finds the real sandboxed child of bwrap's own outer/tracked pid `pid` --
|
||||
@@ -46,6 +47,17 @@ pid_t resolve_namespace_pid(pid_t pid);
|
||||
// since a daemonized process there reparents to the *host's* pid 1 instead).
|
||||
bool pid_namespace_isolated(pid_t outer_pid, pid_t ns_pid);
|
||||
|
||||
// General form of pid_namespace_isolated() above, parametrized over which
|
||||
// /proc/<pid>/ns/<ns_type> entry to compare (e.g. "net", "uts", "ipc") --
|
||||
// pid_namespace_isolated() is just namespace_isolated(outer_pid, ns_pid, "pid")
|
||||
// (kept as its own function since kill_session.h already depends on that
|
||||
// exact name/signature). Used by network_join.h to confirm the session's
|
||||
// sandboxed child has actually entered its own network namespace (bwrap's
|
||||
// clone() happens asynchronously after this project forks/execs it, so the
|
||||
// child may not exist -- or may exist but not have finished unshare()'ing --
|
||||
// yet) before attaching a veth to it.
|
||||
bool namespace_isolated(pid_t outer_pid, pid_t ns_pid, std::string_view ns_type);
|
||||
|
||||
// Every transitive descendant of `root` (root included), found via a single
|
||||
// /proc/<n>/stat pass matching ppid chains back to `root` -- the same
|
||||
// mechanism resolve_namespace_pid()'s own fallback uses, generalized to
|
||||
|
||||
Reference in New Issue
Block a user