Skip to content

Replace deprecated tempdir crate - #1

Merged
brandonweeks merged 1 commit into
mainfrom
tempdir
Aug 18, 2025
Merged

Replace deprecated tempdir crate#1
brandonweeks merged 1 commit into
mainfrom
tempdir

Conversation

@brandonweeks

Copy link
Copy Markdown
Contributor

@brandonweeks
brandonweeks merged commit 6d05831 into main Aug 18, 2025
brandonweeks added a commit that referenced this pull request Aug 18, 2025
Replace deprecated tempdir crate
0chroma added a commit that referenced this pull request Jul 1, 2026
Per ADR #1, use anyhow for opaque error propagation in binary
crates. All cmd_* functions now return Result<(), anyhow::Error> and
use .context()/.bail! instead of hand-rolled eprintln!+Err(()). Errors
are printed centrally in main() via {e:#} for clean error chains.

Client::connect and oneshot_rpc also return anyhow::Error instead of
String, enabling .context() at all call sites.
norrietaylor added a commit that referenced this pull request Jul 1, 2026
Three review-driven fixes to the own-IP path (#589):

- DNS consolidation (Tom, CodeRabbit #1). The "own-IP DNS lives at the
  switch gateway" fact was re-derived in three places and split across a
  dead `GvproxyNetwork::nameserver()` (never reached on the live path, which
  sets `config.network = None`) and `env.rs`'s independent
  `DEFAULT_SUBNET.gateway()`. Add `switch::SwitchSubnet::dns_server()` as the
  single source of truth; have the sandbox write `/etc/resolv.conf` from the
  live `own_ip_tap.gateway` (so the resolver and the tap route come from one
  value, not two derivations); point `guest.rs` at `dns_server()`; and delete
  the now-redundant `Config::dns_nameserver`, `Network::nameserver()`, and
  `GvproxyNetwork::nameserver()`.

- Reject own-IP tap without netns isolation (CodeRabbit #3). Configuring
  `container.network(RustSlirp)` against a shared netns silently no-ops
  (hakoniwa skips the setup, leaving no tap fd), so `sandbox2` now returns
  `NetworkIsolationUnavailable` when `own_ip_tap` is set but the sandbox is
  not isolated.

- Cancel-safe phase-1 rollback (CodeRabbit #2). The pre-spawn own-IP attach
  bumps gvproxy's count before the slow env build; an `Err` was handled but a
  dropped/cancelled launch future leaked it. A `PhaseOneAttachGuard` now rolls
  the attach back on drop (spawned detach, since `Drop` can't await),
  disarmed once ownership passes to `OwnIpGuard`. `complete_local_own_ip_attach`
  and `finish_own_ip_attach` no longer self-detach on the LocalSpawn path (the
  guard owns it); `attach_own_ip` (HostShuttle) keeps its inline rollback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 2, 2026
…#589)

* feat(minimald,sandbox2): rootless host-native (DM2) own-ip networking

Make own-IP work for an unprivileged, `setcap`'d host-native minimald (the
DM2 deployment model: native Linux, no VM), end to end. Three coupled
changes are needed; none alone is sufficient:

1. In-process tap setup (`net/switch.rs`, `net/gvproxy_network.rs`). The
   per-PTask tap is created and configured (MAC, IP, netmask, default
   route, link-up) directly via `setns(CLONE_NEWNET)` + AF_INET ioctls,
   with the PTask's netns fd pinned before the gvproxy-spawning switch
   attach (a short-lived PTask can exit during that window, and a dead
   process's `/proc/<pid>/ns/net` vanishes). This removes the privileged
   `ip`/`nsenter` child processes, whose `setcap +ep` is effective-only
   and not inherited by children. DM1/3/4 (root-in-VM, HostShuttle) keep
   the existing move-into-netns path.

2. Dumpable reset (`main.rs`). Gaining file capabilities at `execve` sets
   the process dumpable flag to SUID_DUMP_ROOT, which makes
   `/proc/<pid>/{uid_map,gid_map,setgroups}` root-owned. A forked hakoniwa
   sandbox inherits that and can no longer write its own `/proc/self/
   uid_map` as the unprivileged user — EPERM, breaking every session
   (own-IP or not). `prctl(PR_SET_DUMPABLE, 1)` at startup restores it.

3. Gateway DNS (`sandbox2` + `env.rs`). An own-IP sandbox runs in a fresh
   netns where the synthesized `/etc/resolv.conf` (the host's 127.0.0.53
   systemd-resolved stub) is unreachable. A new `Network::nameserver()`
   and a `Config::dns_nameserver` (set from the network mode) let the
   sandbox builder write `nameserver <switch-gateway>` into the rootfs
   before spawn — gvproxy serves DNS at the gateway, already the PTask's
   default route. Written unconditionally to overwrite the synth default.

Also adds a `--gvproxy-bin` flag to `minimald run` so the per-host own-IP
switch can use a local gvproxy build without a system install.

Verified on native Linux: own-IP session resolv.conf -> 100.64.0.1,
`curl http://example.com` -> HTTP 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(initramfs): build guest minimald natively when host arch matches

`build-initramfs.sh` always shelled out to `cross` (Docker), so hosts
without Docker could not build the guest initramfs (`cross: not found`).
Auto-detect a same-arch native static-musl toolchain (the `*-linux-musl`
rustup target plus a matching `*-linux-musl-gcc` linker) and build
natively in that case, deriving the cargo linker-override var from the
target triple; otherwise fall back to `cross`. `FORCE_CROSS=1` keeps the
container path. This is what lets the `dm3`/`up` recipes build the guest
on a plain Linux host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: add dm1/dm2/dm3 deployment-model bring-up recipes

Explicit per-deployment-model bring-up recipes:
- `dm1` — macOS + Linux VM over Hypervisor.framework (the `up` path on
  macOS); a clean SKIP on Linux.
- `dm2` — native Linux host-native minimald (no VM) over UDS. Builds the
  daemon, applies `setcap cap_net_admin,cap_sys_admin=ep` (re-applied each
  bring-up; `cargo build` strips file caps), and starts it with
  `--gvproxy-bin` so own-IP needs no system gvproxy install.
- `dm3` — native Linux + one Linux VM, bridging the CLI socket to minvmd's
  guest bridge (the F1 path mismatch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(deps): pin hakoniwa gominimal fork, hold clap at 4.5 for RustSlirp

Enable hakoniwa's `rustslirp` feature for rootless own-IP, pinned to the
gominimal fork of souk4711/hakoniwa (rev 44969f2) which carries the
RustSlirp `gateway()` next-hop-route support (`RustSlirpGateway::
IfaceWithAddr`). Repoint to upstream once that lands.

The rustslirp feature pulls tun-rs 2.8.3 -> c2rust-bitfields 0.22, whose
derive macro exact-pins `proc-macro2 = "=1.0.103"`. clap_derive 4.6
requires `proc-macro2 >= 1.0.106`, which the resolver cannot unify with
`=1.0.103`. Holding clap/clap_complete at 4.5 (which needs only
`proc-macro2 ^1.0`) lets the resolver settle on 1.0.103. Bump back to 4.6
once tun-rs/c2rust relax that pin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(minimald,sandbox2): rootless own-ip via hakoniwa RustSlirp

Replace the native (DM2) own-IP tap setup with hakoniwa's RustSlirp, which
builds and configures the tap *inside* the sandbox's own user+net namespace
(rootless — it acts as container-root and needs no host CAP_NET_ADMIN).
This removes the privileged path entirely: no `setcap`, no
`PR_SET_DUMPABLE`, and no in-process `setns` + tap ioctls.

Per review on #589 (thanks @twitchyliquid64): the own-IP tap creation now
lives behind `container.network(RustSlirp)` and the tap fd comes back via
`Child.rustslirp_tapfd`, which minimald relays to the gvproxy switch.

- sandbox2: a `Config::own_ip_tap` (`OwnIpTap { address, netmask, gateway,
  mtu }`) drives `container.network(RustSlirp::TAP …
  gateway(IfaceWithAddr(gw)))` in `new_container`, after the netns unshare.
  The next-hop route (`0.0.0.0/0 via gateway`) is required — gvproxy is a
  real gateway and does not proxy-ARP, so RustSlirp's default on-link route
  breaks egress.
- minimald: `EnvArgs::with_own_ip_tap` threads the lease into the sandbox.
  The session launch splits into two phases because RustSlirp needs the
  address before spawn: phase 1 (pre-spawn) allocates the lease + ensures
  gvproxy for the native `LocalSpawn` transport; phase 2 (post-spawn) wraps
  `Child.rustslirp_tapfd` and relays it via `complete_local_own_ip_attach`.
  Build/spawn failures roll the phase-1 attach back so gvproxy's refcount
  stays accurate.
- The in-VM DM1/3/4 (`HostShuttle`) path is unchanged — `attach_own_ip`
  keeps the proven open-tap + move-into-netns + vsock relay (minimald is
  root there); the two paths share `finish_own_ip_attach` (ingress + guard).
- Delete the now-dead in-process tap code (`open_tap_in_netns*`,
  `open_netns_fd`, `configure_switch_interface`) and
  `restore_sandbox_dumpable`.
- `dm2` recipe: drop the `setcap` step — own-IP is rootless now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(minvmd): deflake host gvproxy liveness tests

`host_gvproxy_spawns_supervises_and_stops` and
`host_gvproxy_drop_stops_the_switch` spawned a bare `sleep` as the gvproxy
stand-in, but `HostGvproxy::spawn` hands the launched binary gvproxy's argv
(`-config … -listen … -ssh-port -1`). `sleep` rejects those flags and exits
within ~1 ms, so the supervisor's background reaper races the
`pid_is_alive` assertions — passing locally but flaking on slow/contended
CI runners (the assert saw the process already reaped). The test's
"sleep ignores argv" comment was simply wrong.

Use a stand-in that stays alive regardless of the argv it is handed — a
tiny script that `exec`s a long sleep — so the liveness and teardown
assertions are deterministic, matching real gvproxy's run-until-signalled
behaviour. Test-only; no production change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(minimald,sandbox2,switch): address own-ip review feedback

Three review-driven fixes to the own-IP path (#589):

- DNS consolidation (Tom, CodeRabbit #1). The "own-IP DNS lives at the
  switch gateway" fact was re-derived in three places and split across a
  dead `GvproxyNetwork::nameserver()` (never reached on the live path, which
  sets `config.network = None`) and `env.rs`'s independent
  `DEFAULT_SUBNET.gateway()`. Add `switch::SwitchSubnet::dns_server()` as the
  single source of truth; have the sandbox write `/etc/resolv.conf` from the
  live `own_ip_tap.gateway` (so the resolver and the tap route come from one
  value, not two derivations); point `guest.rs` at `dns_server()`; and delete
  the now-redundant `Config::dns_nameserver`, `Network::nameserver()`, and
  `GvproxyNetwork::nameserver()`.

- Reject own-IP tap without netns isolation (CodeRabbit #3). Configuring
  `container.network(RustSlirp)` against a shared netns silently no-ops
  (hakoniwa skips the setup, leaving no tap fd), so `sandbox2` now returns
  `NetworkIsolationUnavailable` when `own_ip_tap` is set but the sandbox is
  not isolated.

- Cancel-safe phase-1 rollback (CodeRabbit #2). The pre-spawn own-IP attach
  bumps gvproxy's count before the slow env build; an `Err` was handled but a
  dropped/cancelled launch future leaked it. A `PhaseOneAttachGuard` now rolls
  the attach back on drop (spawned detach, since `Drop` can't await),
  disarmed once ownership passes to `OwnIpGuard`. `complete_local_own_ip_attach`
  and `finish_own_ip_attach` no longer self-detach on the LocalSpawn path (the
  guard owns it); `attach_own_ip` (HostShuttle) keeps its inline rollback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(initramfs): set CC_<target> for native musl builds

The native same-arch musl branch set `CARGO_TARGET_*_LINKER` but not
`CC_<triple>`, which cc-rs (used by ring's build script to compile its C/asm)
reads to pick the compiler. Export it alongside the linker so the musl
toolchain is used for the C objects too, not just the link (CodeRabbit #4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(deps): repoint hakoniwa to upstream souk4711

The RustSlirp `gateway()` next-hop support (`RustSlirpGateway::IfaceWithAddr`)
merged into souk4711/hakoniwa upstream, so drop the interim gominimal fork pin
and track upstream directly (rev bc0abab). API-identical — no code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(minimald,sandbox2): keep own-ip DNS for the DM1/3/4 shuttle path

The DNS consolidation sourced `/etc/resolv.conf` from `own_ip_tap.gateway`,
but `own_ip_tap` is only set on the native DM2 (`LocalSpawn`) path — so
DM1/3/4 (`HostShuttle`, in-VM) own-IP sessions lost their resolver override
and kept the synth rootfs's dead host stub (`127.0.0.53`) in an isolated
netns (CodeRabbit).

Decouple DNS from the tap: a new `Config::own_ip_dns` is set for *every*
own-IP sandbox from the live `net_switch.subnet().dns_server()` (both
transports), and the sandbox writes resolv.conf from it. This keeps the
single-live-source property (no drift from the tap route, which comes from
the same subnet) while covering the shuttle path. Also collapses the phase-1
double switch-lock into one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-networking): fix e2e harness pty-multiplexing artifact

TC2's same-host-peer step drove two concurrent `minimal attach` sessions from
a single `expect` via two spawn ids read one-at-a-time. `attach` exec's
`ssh -tt`, whose pty has a fixed ~16KB kernel buffer; while the one process
drained session A, session B's ssh kept writing shell-setup into B's unread
pty until it filled and ssh blocked on write, so B's RequestShell never
reached the daemon. That looked like a server-side own-ip attach wedge ("first
attach works, the second hangs") but was the client starving its own pty —
and it cascaded 360s step-timeout SIGKILLs onto TC3/4/7/8.

Fixes (harness only; no product change):
- One pty per process: TC2 now runs the peer listener as a backgrounded
  `run_in_session` and dials it from a separate `session_out`, each draining
  its own pty. Documented as a RULE in the header.
- Line-buffer expect stdout (`fconfigure stdout -buffering line`) in
  `run_in_session` and the TC3/4/7/8 blocks, so a gtimeout SIGKILL can't
  discard a verdict stuck in expect's block buffer.
- Warm peer's sandbox (via the switch-IP read) before the timed trial, and
  retry demo's connect to absorb listener-bind latency.

Verified locally on DM2 (rootless own-ip): TC1/TC1b/TC2 egress pass and the
same-host peer connect returns RC=0 with no wedge and no step-timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-networking): fix TC2 verdict masking (CodeRabbit)

The TC2 peer retry loop ended each failing iteration with `sleep 1`, so
`echo TC2_RC=$?` read the sleep's exit (always 0) and reported success even
when curl never reached the peer — masking a real failure.

Track reachability in an explicit flag instead of the trailing `$?`, and
assert on the response body: socat's `printf PEER_REACHED` is a raw (HTTP/0.9)
reply, so curl's exit status is unreliable — `--http0.9` surfaces the body,
which is grepped for the actual proof. Emits `TC2_PEER=YES|NO`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(minimald): register OwnIp hostnames via published-loopback model (#542)

Land the deferred OwnIp DNS registration and align host->PTask routing
with the DM2 topology in networking-with-diagrams.md (the daemon is not
on the gvproxy switch).

An OwnIp PTask now registers its hostname on launch, resolving to
127.0.0.1 (like HostNet) rather than its switch IP: the PTask is reached
through a gvproxy-published loopback port (its forwarder binds
127.0.0.1:<external> -> lease:<internal>, the same mechanism static
ingress uses), so the host-side proxies never need switch access and no
CAP_NET_ADMIN is required. The client selects the published external
port; the registry gates only on the host.

- dns: add register_own_ip (-> 127.0.0.1); rewrite the module doc from
  the switch-IP model to the published-loopback model.
- sessions: register OwnIp hostnames on launch and follow renames, in
  addition to HostNet (NoNet still registers nothing).
- proxy: replace the own_ip_routes_to_its_switch_ip test with
  own_ip_routes_to_its_published_loopback_port.

Verified end-to-end on DM2 native: with an own-IP PTask publishing
:8080 via ingress 18080:8080, a host request to
web.local.min.internal:18080 through the :7654 proxy reaches the
in-sandbox listener (host -> proxy -> gvproxy forward -> lease:8080).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(minimald): pin the detach-vs-exit network-lifetime invariant

A session is tmux-like: detach (ctrl-w) or an abrupt client disconnect
holds the shell and its network open; only the shell exiting (or an
explicit kill/destroy) tears the network down. Add two session-host
tests over a recording NetGuard to lock this in:

- exit_releases_the_network: the shell process exiting drives
  net_guard.teardown() in the host mainloop.
- detach_keystroke_holds_the_session_and_network: a ctrl-w keystroke is
  swallowed as a detach signal (not forwarded, not fatal), the shell
  keeps running, and the network is only released on a later kill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(spec-networking): rework harness for detach-not-exit + UC2a published port

The e2e harness now treats a session like tmux: it DETACHES (ctrl-w)
between assertions instead of sending `exit`, so a session's shell and
its own-ip lease stay alive and stable across the multiple attaches a
test makes (killing the per-attach lease churn and letting a
backgrounded in-session server survive for host-side curls). Also:

- unprivileged ports only (:8080): the sandbox has no
  CAP_NET_BIND_SERVICE, so `socat TCP-LISTEN:80` fails with EACCES;
- per-call nonce markers, since reattach replays screen state that may
  still show a prior call's markers;
- drop the flaky inline `proc ready` blocks for the reliable
  run_in_session path.

TC3 (UC2a) now reflects the published-loopback model: activate with
--ingress 18080:8080 and reach the service BY HOSTNAME at
web.local.min.internal:18080 through the :7654 proxy. Status table and
per-TC notes updated to the verified DM2 results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec-networking): host->PTask via published-loopback ports (R3.1/R3.3/R4.4)

Resolve the contradiction between the normative requirements (which said
an OwnIp hostname resolves to its gvproxy switch IP, reached via the
switch) and the DM2 topology in networking-with-diagrams.md (daemon not
on the switch; host->PTask via gvproxy-published 127.0.0.1 ports) in
favour of the published-loopback model:

- R3.1/R3.3: an OwnIp PTask resolves to 127.0.0.1 and is reached through
  its gvproxy-published loopback port (R2.3), not the switch IP.
- R4.4: the mTLS reverse proxy forwards to the published loopback port.
- Proof Artifact 2 (UC2a) now curls the published port by hostname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@norrietaylor
norrietaylor deleted the tempdir branch July 23, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant