Make bwrap's --unshare-xxx namespaces configurable via global.unshare-*

Adds six global.unshare-{user,ipc,pid,net,uts,cgroup} config-file keys
(1/on/yes/true or 0/off/no/false, case-insensitive, default on) that
gate whether -r/--run requests each bwrap --unshare-xxx flag when the
kernel also supports it. Replaces the previous hardcoded skip of
--unshare-net, which is now policy-driven like the other five types
and defaults to enabled -- preparation for real network namespace
isolation (slirp4netns) next, on this same branch.
This commit is contained in:
2026-08-30 09:15:39 +00:00
parent 33cd97fdc8
commit c70abb56a9
7 changed files with 239 additions and 55 deletions
+54 -21
View File
@@ -197,11 +197,28 @@ Source layout (all under `src/`):
what `volume_mount.cpp`'s copy-into-an-empty-volume step uses to reach the
image's content when running rootless (see below); `run_bwrap()` itself now just
calls it once on the assembled `bwrap` argv.
`build_bwrap_args()` deliberately drops `--unshare-net` from what's actually passed
to `bwrap` even when the kernel supports it — without any network setup (e.g.
`slirp4netns`), unsharing it just leaves the sandbox with no network at all. Re-add
once network isolation is implemented; `detect_bwrap_unshare_args()` itself still
probes/reports it (e.g. via `-t/--test`), since that's kernel capability, not policy.
`build_bwrap_args()`/`run_bwrap()` also take a `NamespaceConfig` (`bwrap.h`) — one
plain `bool` field per `namespace_probes` entry (`user`/`ipc`/`pid`/`net`/`uts`/
`cgroup`, default `true`), resolved by `run_container()` (`commands.cpp`) from
`AppConfig`'s six `global.unshare-*` keys (`config_file.h`, see above) once, up
front — `bwrap.{h,cpp}` itself never touches `AppConfig`/YAML, only this
already-resolved struct. For each flag `detect_bwrap_unshare_args()` finds the
kernel supports, `build_bwrap_args()` additionally requires the matching
`NamespaceConfig` field to be `true` (looked up via a `.cpp`-local
`namespace_policy_enabled()` if-chain over `namespace_probes`' `name`s) before
actually passing it to `bwrap` — kernel support and policy are separate gates,
both must allow a type. This replaced an earlier hardcoded special case that
always dropped `--unshare-net` regardless of policy or kernel support (without
any network setup, e.g. `slirp4netns`, unsharing it just left the sandbox with no
network at all) — `net` now goes through the exact same policy path as every
other type, defaulting to enabled like the rest. **This is a deliberate,
user-acknowledged transitional behavior change**: as of this, a plain `-r/--run`
with no config file override gets a real network namespace and thus no network
access at all, until `slirp4netns` integration (the next task on this same
branch) actually sets one up; `global.unshare-net: off` restores the prior
no-isolation behavior in the meantime. `detect_bwrap_unshare_args()` itself is
untouched by any of this — still an unfiltered kernel-capability probe, so
`-t/--test`'s diagnostic report continues to reflect raw kernel support, not policy.
`build_bwrap_args()`/`run_bwrap()` also take an optional `hostname` (from
`--hostname`, long-option only): passed through as bwrap's own `--hostname` only
when `--unshare-uts` is actually among the flags `bwrap` is being given (bwrap
@@ -666,25 +683,41 @@ Source layout (all under `src/`):
- `config_file.{h,cpp}``load_config_file()` reads and parses (via libyaml's
document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML
config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
falling back to `$HOME/.config/slocker-lite/config.yaml`). `global.log-level` is the
only supported `global` key — other long options are one-shot flags, not settings,
so they don't belong in a persistent config file. A missing file returns a
default-constructed (empty) `AppConfig`, not an error; unknown sections/keys (and
malformed individual volume entries) are ignored for forward-compatibility;
malformed YAML syntax is a hard error. `main()` applies `config->log_level` (via
the existing `apply_log_level()`) right after `spdlog::cfg::load_env_levels()` and
before parsing CLI options, so an explicit `--log-level` on the command line always
overwrites it afterward — same precedence pattern already used for `SPDLOG_LEVEL`.
falling back to `$HOME/.config/slocker-lite/config.yaml`). Supported `global` keys:
`log-level`, and six `unshare-<type>` keys (`unshare-user`/`unshare-ipc`/
`unshare-pid`/`unshare-net`/`unshare-uts`/`unshare-cgroup`, one per
`bwrap.cpp`'s own `namespace_probes` entry) controlling whether `-r/--run`
requests each of bwrap's `--unshare-xxx` flags — every other long option is a
one-shot flag, not a setting, so it doesn't belong in a persistent config file.
Each `unshare-*` key accepts `"1"`/`"on"`/`"yes"`/`"true"` (enabled) or
`"0"`/`"off"`/`"no"`/`"false"` (disabled), case-insensitively (`.cpp`-local
`parse_bool_flag()`); an unset key defaults to enabled, and an unrecognized
value logs a `spdlog::warn` and is treated as unset (default enabled) rather
than failing the whole config load — consistent with this file's existing
forward-compatible/ignore-malformed-entries policy (only malformed *YAML
syntax* is a hard error). A missing file returns a default-constructed
(empty) `AppConfig`, not an error; unknown sections/keys (and malformed
individual volume entries) are likewise ignored for forward-compatibility.
`main()` applies `config->log_level` (via the existing `apply_log_level()`)
right after `spdlog::cfg::load_env_levels()` and before parsing CLI options,
so an explicit `--log-level` on the command line always overwrites it
afterward — same precedence pattern already used for `SPDLOG_LEVEL`.
`write_config_file()` writes the whole file back out (via libyaml's
document-building/emitter API, symmetric to the read side) — used by
`-v/--volume` (`create_volume_command()`, `commands.cpp`) to persist a new
`VolumeEntry {name, directory}` into the `volumes` section, preserving `global`
untouched. **`VolumeEntry`/the `volumes` section is a distinct concept from
`OciImageConfig::volumes`**: this is a user-defined `name -> host directory`
mapping created via `-v/--volume`, not an image's own declared mount points (still
unconsumed, see `oci_image.{h,cpp}` above). `AppConfig`/`config.volumes` is looked
up by name in `resolve_volume_mount()` (`volume_mount.{h,cpp}`, see below), which
is how `-r/--run`'s own `-v` usage finds a named volume's host directory.
`VolumeEntry {name, directory}` into the `volumes` section, preserving
`global` (including any set `unshare-*` keys, re-serialized as canonical
`"true"`/`"false"`) untouched. **`VolumeEntry`/the `volumes` section is a
distinct concept from `OciImageConfig::volumes`**: this is a user-defined
`name -> host directory` mapping created via `-v/--volume`, not an image's
own declared mount points (still unconsumed, see `oci_image.{h,cpp}` above).
`AppConfig`/`config.volumes` is looked up by name in `resolve_volume_mount()`
(`volume_mount.{h,cpp}`, see below), which is how `-r/--run`'s own `-v` usage
finds a named volume's host directory. `run_container()` (`commands.cpp`)
resolves the six `unshare-*` fields (each `value_or(true)`) into a
`NamespaceConfig` (`bwrap.h`, see below) once, up front, and passes it to
`run_bwrap()``bwrap.{h,cpp}` itself has no dependency on this file or on
YAML parsing at all, only on the already-resolved, defaults-applied struct.
- `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()`