Add veth capability probe + --no-veth network flag

Prep work for a tap+userspace-relay fallback: the real target device
supports tun/tap but not veth (CONFIG_VETH stripped from its kernel),
so -n/--network joins there can't use the veth-pair mechanism as-is.

probe_veth_support() (network_bridge.{h,cpp}) detects kernel veth
support the same way bwrap.cpp's kernel_supports_namespace() probes
namespace types: fork, unshare(CLONE_NEWNET) into a throwaway
namespace, try `ip link add ... type veth ...` there. should_use_veth()
combines that with a new per-network NetworkEntry::veth policy flag
(default true, config_file.h), mirroring how namespace_policy_enabled()
already combines kernel capability with policy for --unshare-xxx.

--no-veth at network-creation time (-n <name> --extern|--intern
--no-veth) sets veth: false, forcing the not-yet-built tap+relay
fallback even on a veth-capable kernel like this dev machine -- lets
that path be exercised here without the actual veth-less hardware.

Verified as root via the doas rule: probe_veth_support() returns true
on this dev machine (a real veth pair is created successfully), and
--no-veth correctly persists veth: false while should_use_veth() still
returns false regardless of kernel support.

The fallback itself (network_tap_relay.{h,cpp}) isn't wired in yet --
a --no-veth network simply has no way to join a container until that
lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-30 15:59:40 +00:00
parent a9cd3bf643
commit 08895297a9
9 changed files with 141 additions and 19 deletions
+44 -5
View File
@@ -76,11 +76,19 @@ Source layout (all under `src/`):
`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
`--subnet6 <cidr>`/`--no-veth` (`ParsedArgs::network_extern_flag`/`network_intern_flag`/
`network_subnet_flag`/`network_no_ipv6_flag`/`network_subnet6_flag`/
`network_no_veth_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
requires exactly one of `--extern`/`--intern`. `--no-veth` forces
`NetworkEntry::veth` (`config_file.h`) to `false` at creation time,
overriding the default `true` — see `network_bridge.h`'s
`probe_veth_support()`/`should_use_veth()` for what this controls: lets
the tap+relay fallback (the real target device's kernel lacks `CONFIG_VETH`
— see `docs/networking-design.md`'s addendum) be exercised on a
veth-capable machine like this dev box, without needing the actual
veth-less hardware. **`-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
@@ -609,6 +617,32 @@ 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.
`probe_veth_support()` (added for the tap+relay fallback, see
`docs/networking-design.md`'s addendum and `network_join.{h,cpp}` below):
the real target device supports `tun`/`tap` but not `veth`
(`CONFIG_VETH` commonly stripped from mobile kernels), so joins there can't
use the veth-pair mechanism this file/`network_join.cpp` otherwise assume.
Probes kernel support the same way `bwrap.cpp`'s
`kernel_supports_namespace()` probes namespace types: forks a child that
`unshare(CLONE_NEWNET)`s into a throwaway namespace and attempts `ip link
add ... type veth peer name ...` there (via the existing `run_process()`)
— the whole namespace, and anything created in it, vanishes with the
child, so no cleanup is needed either way. Cached in a function-local
static (a fixed fact about the running kernel, not something that varies
per network, so a container joining several networks in one run only
probes once). `should_use_veth(network)` combines this with the network's
own `veth` policy flag (`config_file.h`'s `NetworkEntry::veth`, default
`true`) the same "capability and policy are independent gates" way
`namespace_policy_enabled()` (`bwrap.cpp`) already combines kernel support
with `global.unshare-*` policy — both must allow veth for it to actually
be used. **Verified on this dev machine (root, via the scoped `doas`
rule)**: `probe_veth_support()` correctly returns `true` here (a real `ip
link add ... type veth ...` succeeds), and `--no-veth` at network-creation
time correctly persists `NetworkEntry::veth = false`, making
`should_use_veth()` return `false` even though the kernel itself supports
veth — this dev machine's own way to exercise the tap+relay fallback (see
`network_tap_relay.{h,cpp}`, not yet built) without needing the actual
veth-less target device.
- `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()`,
@@ -1139,7 +1173,12 @@ Source layout (all under `src/`):
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.
`unshare-*` keys. `veth` (default `true`) round-trips the same way as
`ipv6` (`parse_bool_flag()`, written as canonical `"true"`/`"false"`,
always written regardless of value — unlike `subnet6`, there's no
companion field whose presence depends on it) — see `network_bridge.h`'s
`probe_veth_support()`/`should_use_veth()` above and `--no-veth`
(`cli_args.{h,cpp}`) below for what it controls.
- `network_subnet.{h,cpp}` — pure CIDR arithmetic backing `-n/--network`'s
subnet allocation and `network_bridge.{h,cpp}`'s (see below) gateway-address
computation; no kernel/`ip`/`iptables` calls of its own. `is_valid_network_name()`
@@ -1274,7 +1313,7 @@ Build directory is `buildDir/` (already configured).
`-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`, `-n/--network`, `--extern`, `--intern`, `--subnet`, `--no-ipv6`, `--subnet6`,
`--list-networks`, `--delete-network`, `-p/--port-forward`, `--list-processes`, `--clean-processes`,
`--no-veth`, `--list-networks`, `--delete-network`, `-p/--port-forward`, `--list-processes`, `--clean-processes`,
`-w/--write-config`, `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir`