Add -n/--network config CRUD: schema, subnet/IPv6 allocation, CLI

Commit 1/6 of the network isolation feature (docs/networking-design.md):
config-only, no host-side effects yet. Adds NetworkEntry {name, kind,
subnet, ipv6, subnet6} and a networks config-file section parallel to
volumes; network_subnet.{h,cpp} for CIDR validation, overlap checking,
and auto-allocation (10.168.<n>.0/24 / fd00:168:0:<n>::/64, paired,
--subnet/--subnet6 overrides); -n/--network create/list/delete CLI,
dual-purpose like -v/--volume (alone creates, repeatable with -r to
join -- joining isn't wired up yet, just accepted).

-n was already taken by --no-nsenter; moved that to long-option-only
(--no-nsenter), matching --kill's "rare flag, no real loss" precedent,
since --network will be far more heavily used.
This commit is contained in:
2026-08-30 12:39:28 +00:00
parent f5f1e8522b
commit ec09b96b56
10 changed files with 669 additions and 27 deletions
+77 -7
View File
@@ -47,7 +47,7 @@ Source layout (all under `src/`):
`1` for any parse error) when set; `nullopt` means `out` is ready for
`dispatch_command()`. `-D/--daemonize` (has a short form; `'D'` was free) is
a plain boolean flag (`ParsedArgs::daemonize_flag`, set in its own `case
'D':`, same pattern as `-n/--no-nsenter`). `--hostname <name>`/`--env
'D':`, same pattern as `--no-nsenter`). `--hostname <name>`/`--env
VAR=VALUE`/`--env-file <file>` (all long-option only, `--env`/`--env-file`
both repeatable) are collected here into `ParsedArgs::hostname_flag`/
`env_specs``--env` pushes `{false, optarg}`, `--env-file` pushes `{true,
@@ -67,7 +67,24 @@ Source layout (all under `src/`):
next flag if there isn't one), and only *after* the loop decides whether
that means one standalone `Mode::volume` call or, together with `-r`,
passes `volume_specs` through unresolved for `dispatch_command()`/
`run_container()` to handle.
`run_container()` to handle. `-n/--network` (see `docs/networking-design.md`
for the full feature design) is dual-purpose the same way, but simpler: since
a network join has no equivalent of a volume's container-mount-path second
argument, each occurrence is a single `required_argument` token (just the
name) accumulated into `ParsedArgs::network_specs` — no manual second-token
consumption needed, `'n'`'s own `case` just does
`network_specs.push_back(optarg)`. The same post-loop split as `-v` decides
`Mode::network` (standalone, exactly one occurrence) vs. join-with-`-r`
(repeatable, no limit). `--extern`/`--intern`/`--subnet <cidr>`/`--no-ipv6`/
`--subnet6 <cidr>` (`ParsedArgs::network_extern_flag`/`network_intern_flag`/
`network_subnet_flag`/`network_no_ipv6_flag`/`network_subnet6_flag`) only
apply to the standalone (create) case and are rejected with a clear error if
given any other way (e.g. alongside `-r`) — `Mode::network` additionally
requires exactly one of `--extern`/`--intern`. **`-n` used to belong to
`--no-nsenter`**: reassigned here since `--network` will be far more
heavily used; `--no-nsenter` moved to long-option-only (`options::no_nsenter`)
rather than hunting for a new letter, matching `--kill`'s own "rare/niche
flag, long-only is no real loss" precedent.
- `commands.{h,cpp}` — every command's implementation, plus the dispatcher.
`dispatch_command(args, config_path, config)` (the only externally-linked
function; everything else in this file is `.cpp`-local) is a `switch
@@ -115,6 +132,22 @@ Source layout (all under `src/`):
already gone) — see `config_file.{h,cpp}` below for what a "volume" means here (a
distinct concept from `OciImageConfig::volumes`). `dispatch_command()`'s
`Mode::volume`/`Mode::delete_volume`/`Mode::delete_volume_full` cases call these.
`create_network_command()`/`list_networks_command()`/`delete_network_command()`
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`. This commit is
config-only: no bridge, namespace, or iptables state is created yet, only the
config entry (later commits in the design doc's sequence wire up the actual
host-side networking). `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()`/
`ipv6_cidrs_overlap()`, `network_subnet.{h,cpp}`, see below); otherwise the
next free block is auto-allocated (`allocate_ipv4_subnet()`/
`allocate_ipv6_subnet()`). `list_networks_command()` reuses the same
independently-per-column tab-alignment scheme as `list_processes_command()`
(name/kind/subnet each aligned, then the IPv6 subnet — or `"(no ipv6)"`
appended unaligned as the trailing column, nothing follows it).
`write_config_command()` implements `-w/--write-config`: unlike
`create_volume_command()`/`delete_volume_command()`'s use of
`write_config_file()` (which only ever persists `AppConfig` fields that are
@@ -698,8 +731,9 @@ Source layout (all under `src/`):
(`process.cpp` already retries `waitpid()` the same way) — rather than
`<thread>`/`<chrono>` (unused anywhere else in this project).
- `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`,
document API, `<yaml.h>`) the `global`, `volumes`, and `networks` 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`). Supported `global` keys:
`log-level`, and six `unshare-<type>` keys (`unshare-user`/`unshare-ipc`/
`unshare-pid`/`unshare-net`/`unshare-uts`/`unshare-cgroup`, one per
@@ -735,6 +769,41 @@ Source layout (all under `src/`):
`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.
**`networks` section** (see `docs/networking-design.md` for the full
feature): unlike `volumes` (a flat `name -> directory` scalar mapping), each
network entry is itself a *nested* mapping (`kind`/`subnet`/`ipv6`/
`subnet6`), since one network needs more than a single value to describe.
`NetworkEntry` (`kind` is `NetworkKind::extern_`/`intern` — trailing
underscore on `extern_` since `extern` is a reserved C++ keyword and can't
be an enumerator name — parsed from the YAML strings `"extern"`/`"intern"`)
round-trips through `AppConfig::networks` the same way `VolumeEntry` does;
an entry with an unrecognized `kind` (or missing `kind`/`subnet`) is skipped
on load, same forward-compatible policy as everything else here. `ipv6`
reuses `parse_bool_flag()`, defaulting to `true` (enabled) if absent or
unparseable; `subnet6` is only read/written when `ipv6` is true.
`write_config_file()` writes each network as its own nested mapping under
`networks`, `ipv6` re-serialized as canonical `"true"`/`"false"` like the
`unshare-*` keys.
- `network_subnet.{h,cpp}` — pure CIDR arithmetic backing `-n/--network`'s
subnet allocation, no kernel/`ip`/`iptables` calls (those come in a later
commit per `docs/networking-design.md`'s sequence). `is_valid_ipv4_cidr()`/
`is_valid_ipv6_cidr()` and `ipv4_cidrs_overlap()`/`ipv6_cidrs_overlap()` all
build on one `.cpp`-local `parse_cidr()` (via `inet_pton()`, not hand-rolled
parsing) producing a plain byte-vector address (4 bytes for IPv4, 16 for
IPv6) + prefix length, and one shared `bytes_overlap()` byte/bit-mask
comparison generic over that byte length — IPv4 and IPv6 overlap checking
are the same algorithm, not two parallel implementations.
`allocate_ipv4_subnet()`/`allocate_ipv6_subnet()` (`commands.cpp`'s
`create_network_command()`) scan `10.168.<n>.0/24`/`fd00:168:0:<n>::/64` for
`n` in `0..255` and return the first one that doesn't overlap *any* existing
network's subnet (via the overlap checks above, not just other
auto-allocated ones — a manually `--subnet`-overridden network is checked
too). The same `n` range for both is deliberate, so the common case (no
manual overrides) allocates visibly paired v4/v6 blocks per network — though
since IPv6 hextets are hexadecimal, `n >= 10` renders as a valid but
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.
- `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()`
@@ -794,7 +863,7 @@ reexec is needed, so the mount is already directly visible in the current namesp
and `nsenter --user=...` into it then fails ("reassociate to namespace 'ns/user'
failed: Invalid argument") since the caller is already in that same user namespace.
`-r/--run` detects `geteuid() == 0` and skips `nsenter` automatically in that case;
`-n/--no-nsenter` forces it off manually for any other situation where the mount turns
`--no-nsenter` forces it off manually for any other situation where the mount turns
out to already be directly visible.
**Mutable global state and multi-container support:** an audit ahead of planned
@@ -828,9 +897,10 @@ Build directory is `buildDir/` (already configured).
(see `priv_drop_helper.cpp` in "Project state")
- Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
`-l/--list-images`, `-i/--inspect`, `-x/--exec`, `--kill`, `-n/--no-nsenter`, `-D/--daemonize`,
`-l/--list-images`, `-i/--inspect`, `-x/--exec`, `--kill`, `--no-nsenter`, `-D/--daemonize`,
`--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-w/--write-config`,
`--delete-volume-full`, `-n/--network`, `--extern`, `--intern`, `--subnet`, `--no-ipv6`, `--subnet6`,
`--list-networks`, `--delete-network`, `--list-processes`, `--clean-processes`, `-w/--write-config`,
`-t/--test`, `--log-level`, `-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir`