Replace deprecated tempdir crate - #1
Merged
Merged
Conversation
brandonweeks
added a commit
that referenced
this pull request
Aug 18, 2025
Replace deprecated tempdir crate
This was referenced Jun 22, 2026
Closed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://github.com/rust-lang-deprecated/tempdir#deprecation-note