Retry container-side tap device setup, external process only

A user's log from the real target device showed nsenter'd `ip addr
add ... dev eth0` failing with "Cannot find device" immediately after
network_tap_relay.h's relay had already created it -- confirmed by
hand that retrying the whole session a few times eventually worked.

join_one_network() now retries (bounded, ~500ms, quiet until final
success/give-up) the three steps that touch the just-created container
interface -- IPv4 address, IPv6 address, bringing it up -- via a new
run_with_retry() instead of plain run().

A tempting first fix was investigated and ruled out by direct A/B
testing, not just reasoned about: having the relay itself self-verify
the device is visible (a same-process check, immediately after
creating it, before ever reporting success) was tried first, in
relay_child_main(). It made things categorically worse: the
container-side device became permanently invisible to every external
nsenter afterward, 100% reproducibly (confirmed with a 10-second retry
budget -- never once became visible), on a mechanism that had
otherwise worked correctly and instantly, zero retries needed, on
every real session tested earlier the same day -- including a
from-scratch self-test reproduction that had passed reliably many
times before this one change, and immediately went back to passing
once it was reverted. Root cause not fully understood (something about
forking a subprocess that inherits the tap fd -- deliberately not
O_CLOEXEC -- while still holding it open, immediately after device
creation, appears to corrupt the device's external visibility on this
kernel specifically), but the fix is unambiguous: never add an
internal, same-process/fd-holding self-check to the relay; only the
external, separate-process retry is safe.

network_tap_relay.cpp ends up completely unchanged -- the actual fix
lives entirely in network_join.cpp's own retry. self_test.cpp's own
container-visibility check needed the same external retry treatment,
for the same underlying reason.

Verified as root via the doas rule: three separate real --no-veth
sessions all succeeded getting eth0 on the first attempt (no retries
triggered), and the self-test passes reliably across repeated runs.

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 19:26:39 +00:00
parent cf166ed3b4
commit ad30f93f77
4 changed files with 183 additions and 17 deletions
+58 -8
View File
@@ -333,14 +333,23 @@ Source layout (all under `src/`):
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.
namespace (checked via `nsenter --net=/proc/<pid>/ns/net -- ip link show`,
wrapped in `wait_for_container_device_visible()` — a `.cpp`-local bounded
retry, same shape as `network_join.cpp`'s own `run_with_retry()`, added
for the same real device-reported reason — see that function's own doc
comment for the full story, including the tempting-but-harmful internal-
self-check "fix" that was tried and ruled out first, confirmed by this
exact self-test: it started reliably failing, 100% of the time, the moment
that harmful check was added — despite having passed reliably many times
earlier the same session before that change — and immediately went back to
passing reliably once the harmful check was removed again), 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,
many times over, 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
@@ -853,6 +862,47 @@ Source layout (all under `src/`):
unaffected (still just loopback, whether or not the kernel happened to
give it its own otherwise-empty net namespace via the default
`global.unshare-net` policy).
**Real bug reported from the real target device (`-n <extern network> --
/bin/sh`, tap+relay fallback): the container-side tap device wasn't always
immediately visible.** The user's own log showed `nsenter --net=/proc/<ns_pid>
/ns/net -- ip addr add 10.168.0.2/24 dev eth0` failing with `"Cannot find
device \"eth0\""` right after `network_tap_relay.h`'s relay had already
created it — and confirmed by hand that simply retrying the whole session a
few times eventually worked. `run_with_retry()` (`.cpp`-local, same
shape/spirit as `wait_for_isolated_net_namespace()` below) replaces `run()`
for the three steps that touch the just-created `container_if` (IPv4
address, IPv6 address, bringing it up) — retries quietly (no per-attempt
log spam) for up to 500ms before giving up loudly, same as `run()`'s own
single-attempt error.
**A tempting "fix" investigated and ruled out by direct A/B testing on this
dev machine, not just reasoned about**: the obvious first instinct — have
the relay *itself* self-verify the device is visible (a same-process check
via its own `run_process()` call, immediately after `open_tap()`, before
ever reporting success) — was tried first, in `network_tap_relay.cpp`'s
`relay_child_main()`. It made things *worse*, not better: it made the
container-side device **permanently invisible to every external `nsenter`
afterward, 100% reproducibly** (confirmed with a 10-second retry budget —
never once became visible), on a mechanism that had otherwise worked
correctly and instantly on every single real session tested earlier this
same day, with no retries ever needed. Root cause not fully understood
(something about forking a subprocess that inherits the tap fd — opened
via `open("/dev/net/tun", O_RDWR)`, deliberately *not* `O_CLOEXEC` — while
still holding it open, immediately after device creation, appears to
corrupt the device's *external* visibility specifically on this kernel;
the *same* process's own view of the device it just created stayed correct
throughout). Confirmed via direct A/B testing, not just correlation:
removing that internal check immediately restored 100%-reliable, zero-retry
first-try success on three separate real sessions; re-adding it reliably
broke it again, including for a from-scratch self-test reproduction (see
`self_test.{h,cpp}` below) that had passed reliably many times earlier in
the same session before this specific change. The fix that shipped is
unambiguous: never add an internal, same-process/fd-holding self-check to
the relay; only the *external*, separate-process retry above
(`run_with_retry()`) is safe, and is sufficient on its own — every retried
real-device call this session succeeded on the very first attempt once the
harmful internal check was gone.
- `port_forward.{h,cpp}` — implements `-p`. `parse_port_forward_spec()`
splits `"[<network>:]<host-port>:<container-port>"` on `':'` (2 or 3
fields; the network name is deliberately restricted to excluding `':'` --