Add network_tap_relay: tap-backed veth substitute, standalone
Second step of the tap+relay fallback for veth-less kernels (the real target device supports tun/tap but not veth). Reuses the existing bridge as the switching fabric -- provision_bridge()'s NAT/forwarding setup needs no changes -- and only replaces how a container's namespace gets connected to it: - a host-side tap device, created wherever the network's bridge lives and enslaved to it, playing veth's host-side role - a container-side tap device, created directly inside the container's namespace and named eth<N> from the start (no rename step needed) - a relay process holding both fds open, copying raw Ethernet frames bidirectionally between them -- reproducing a veth pair's kernel wire via one userspace hop Not wired into join_one_network() yet -- this commit only adds create_tap_relay()/stop_tap_relay() and exercises them standalone via a new self-test (throwaway bridge + throwaway namespace). A real synchronization bug turned up while writing that self-test: fork() returning to the parent doesn't mean the child has reached its own unshare(CLONE_NEWNET) yet, so using its pid immediately raced and created the container-side tap in the wrong (host) namespace. Fixed by polling namespace_isolated() first, the same guard network_join.cpp's wait_for_isolated_net_namespace() already uses for a real session. Verified twice as root via the doas rule: host-side tap gets created and attached to the bridge, container-side tap gets created with the right name inside the target namespace, and -- the biggest open assumption from the design doc addendum -- both devices disappear on their own once stop_tap_relay() stops the process, no explicit `ip link del` needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -284,8 +284,34 @@ Source layout (all under `src/`):
|
||||
(checked from this process, *after* the forked child that actually did the
|
||||
`unshare()`/bind-mount has already exited — the actual claim being tested:
|
||||
the namespace outlives its creating process), then gone again after
|
||||
removal. Deliberately its own small file since more real tests are expected
|
||||
here as more of the networking feature lands.
|
||||
removal. Also exercises `network_tap_relay.{h,cpp}`'s (see below)
|
||||
create/attach/teardown cycle, same root-only skip: a throwaway bridge
|
||||
(`ip link add ... type bridge`, no `network_bridge.h` provisioning needed —
|
||||
a tap device only cares that *some* bridge exists to attach to) stands in
|
||||
for a real network's bridge, and a throwaway network namespace (a forked
|
||||
child that `unshare(CLONE_NEWNET)`s then blocks in `pause()` until
|
||||
signaled) stands in for a real `-r/--run` session's isolated one. **Real
|
||||
race caught by testing, not assumed**: `fork()` returning to the parent
|
||||
doesn't mean the child has actually reached its own `unshare(CLONE_NEWNET)`
|
||||
call yet — the same race `network_join.cpp`'s own
|
||||
`wait_for_isolated_net_namespace()` already guards against for a real
|
||||
session; an earlier version of this test used the child's pid immediately
|
||||
and the container-side tap device silently ended up created in the *host's*
|
||||
namespace instead (confirmed: it wasn't visible via `nsenter` into the
|
||||
child's namespace at all). Fixed the same way — polling (bounded, 1s,
|
||||
20ms interval) `namespace_isolated()` (`sandbox_process.h`) until the
|
||||
child's own net namespace actually differs from this process's before
|
||||
trusting its pid. Confirms the host-side tap gets created and attached to
|
||||
the bridge (`ip link show` output contains `master <bridge>`), the
|
||||
container-side tap gets created with the requested name inside the target
|
||||
namespace (checked via `nsenter --net=/proc/<pid>/ns/net -- ip link show`),
|
||||
and — the biggest previously-unverified assumption from
|
||||
`docs/networking-design.md`'s tap+relay addendum — **both devices actually
|
||||
disappear on their own once `stop_tap_relay()` stops the relay process,
|
||||
with no explicit `ip link del` needed** (neither was created with
|
||||
`IFF_PERSIST`) — confirmed directly, twice, on this dev machine (root, via
|
||||
the scoped `doas` rule). Deliberately its own small file since more real
|
||||
tests are expected here as more of the networking feature lands.
|
||||
- `env_spec.{h,cpp}` — `resolve_env_specs()` turns an ordered list of
|
||||
`EnvSpec {is_file, value}` (see `cli_args.{h,cpp}` above) into a flat, ordered list of
|
||||
`(key, value)` pairs. A literal (`--env`) is split at its *first* `=` (the
|
||||
@@ -824,6 +850,71 @@ Source layout (all under `src/`):
|
||||
its rule-removal attempted — visibly failing only for lack of root in this
|
||||
particular rootless test — and its record file actually removed,
|
||||
reported as `removed stale port-forward rules for 'faketest-999999'`).
|
||||
- `network_tap_relay.{h,cpp}` — a tap-backed substitute for one veth pair,
|
||||
used when `should_use_veth()` (`network_bridge.h`) is false: either the
|
||||
running kernel doesn't support veth at all (the real target device's
|
||||
kernel supports `tun`/`tap` but lacks `CONFIG_VETH`, commonly stripped
|
||||
from mobile kernels — see `docs/networking-design.md`'s tap+relay
|
||||
addendum), or the network was created with `--no-veth` (`cli_args.{h,cpp}`)
|
||||
specifically to exercise this path on a veth-capable machine. **Why tap
|
||||
can't just replace veth 1:1**: a veth pair is two real kernel netdevices,
|
||||
switched between (or into a bridge) entirely by the kernel; a tap device
|
||||
only has *one* kernel-side netdevice — the other "end" is a raw-Ethernet-
|
||||
frame file descriptor only a userspace process can read/write, so there's
|
||||
no second kernel endpoint to attach to a bridge (the same reason
|
||||
`slirp4netns`/QEMU's own tap networking need a userspace process on the fd
|
||||
side). `create_tap_relay(network, bridge, host_tap_name, container_ns_pid,
|
||||
container_if_name)` reproduces a veth pair's role with two tap devices and
|
||||
one relay process that copies bytes between them, **reusing the existing
|
||||
bridge as the switching fabric** so `network_bridge.cpp`'s
|
||||
`provision_bridge()`/NAT setup needs no changes at all: a host-side tap
|
||||
device (created wherever `network`'s bridge lives — `network_bridge.h`'s
|
||||
`wrap_for_network()` namespace, reached here via a direct `setns()` rather
|
||||
than that function's `nsenter`-argv-wrapping, since this whole sequence
|
||||
must keep running, and later hold onto live fds, across each namespace
|
||||
switch, not just for the duration of one external command) gets enslaved
|
||||
to `bridge` exactly like veth's host-side end does in
|
||||
`network_join.cpp`'s `join_one_network()`; a container-side tap device
|
||||
gets created directly inside the namespace named by `container_ns_pid`,
|
||||
**named `container_if_name` (e.g. `eth0`) from the start** — no
|
||||
peer-name-then-rename dance needed, unlike veth. `host_tap_name` is
|
||||
caller-provided (not derived here) specifically so a future caller
|
||||
(`join_one_network()`, once this is wired in) can reuse its own existing
|
||||
fnv1a-based veth-naming scheme rather than this file growing a second,
|
||||
drifting copy of that six-line hash. `open_tap()` (`.cpp`-local) creates
|
||||
each device via a direct `open("/dev/net/tun")` + `ioctl(TUNSETIFF,
|
||||
IFF_TAP | IFF_NO_PI)` — `IFF_NO_PI` so both ends agree on raw-frame
|
||||
framing with no extra header, deliberately **not** `IFF_PERSIST`, so each
|
||||
device is expected to disappear on its own once its one-and-only fd
|
||||
closes, the same "no explicit teardown" property veth already has (see
|
||||
`self_test.{h,cpp}` above for where this got verified). The relay child's
|
||||
entire setup sequence — enter the network's own namespace first if
|
||||
`intern` (`persistent_netns_path()`, `persistent_netns.h`), create+attach
|
||||
the host-side tap, `setns()` into the container's namespace (the
|
||||
already-open host-side fd stays valid across this switch — fds aren't
|
||||
namespace-scoped, only their *creation* is, the same property
|
||||
`wrap_for_root_namespace()`, `bwrap.cpp`, already relies on), create the
|
||||
container-side tap — reports success/failure back to `create_tap_relay()`
|
||||
over a `pipe2(O_CLOEXEC)` (same handshake shape `daemonize()`,
|
||||
`daemonize.cpp`, already uses), then falls into an unbounded
|
||||
`poll()`/`read()`/`write()` loop copying raw frames bidirectionally
|
||||
between the two fds — this loop *is* the actual "veth wire," just
|
||||
implemented once in userspace instead of by the kernel. No `SIGTERM`
|
||||
handler is installed in the relay: default disposition (terminate) already
|
||||
closes both fds on the way out, which is all `stop_tap_relay()`'s
|
||||
"no explicit teardown" contract needs. `stop_tap_relay()` sends `SIGTERM`
|
||||
and reaps the process. Once `create_tap_relay()` returns successfully,
|
||||
`container_if_name` is a completely ordinary interface from the
|
||||
container's own point of view — `join_one_network()`'s existing IP
|
||||
assignment/route/DNAT-target-address code (unchanged, not yet wired to
|
||||
call this) needs no changes at all. **Not yet wired into
|
||||
`join_one_network()`/`run_container()`** — see `self_test.{h,cpp}` above
|
||||
for how this file's own create/attach/teardown cycle was verified
|
||||
end-to-end in isolation first; full traffic-plane verification (does
|
||||
`-p`/inter-container connectivity actually work end-to-end through this
|
||||
path) is deferred to the commit that does that wiring, where two real
|
||||
`--no-veth`-joined containers pinging/curling each other is a more natural
|
||||
and simpler proof than hand-rolled raw-socket test traffic.
|
||||
- `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
|
||||
|
||||
Reference in New Issue
Block a user