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:
@@ -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`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user