feat(minimald,sandbox2): gvproxy switch lifecycle, OwnIp tap relay, and netns proofs - #525
Conversation
…etns proofs Implements Unit 1 (U1-T2) of the spec-networking stack: per-host gvproxy switch supervision, switch-address allocation, the OwnIp tap relay, and network-namespace isolation in sandbox2, with the UC1/UC6 netns proofs. - sandbox2 now unshares the network namespace for NoNet and OwnIp (and degrades to host networking where unprivileged netns is unavailable); HostNet keeps the current shared-namespace behaviour. The decision is a tested `isolates_network` predicate. - New `minimald::net` module: `GvproxySwitch` (ref-counted lifecycle with SIGTERM -> grace -> SIGKILL teardown, R1.4), an IP allocator that never reuses an address over `100.64.0.0/16` (R1.6), gvproxy YAML config generation, structured tracing (R1.8), and the HyperKit-framed tap relay that bridges a netns tap onto the switch (R1.5/R1.7). The gvproxy v0.8.9 switch attachment is an HTTP `POST /connect` upgrade plus HyperKit framing (2-byte LE length + raw Ethernet), not the SCM_RIGHTS fd-pass the task title assumed; see docs/spikes/2026-06-21-gvproxy-attachment.md. The UC1 (NoNet no-egress) and UC6 (OwnIp PTask-to-PTask) proofs are `#[ignore]`, gated on MINIMALD_NETNS_TEST, and read gvproxy from GVPROXY_BIN; they run in ci-netns.yml on a netns-capable runner. Refs: #478 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Linux-only Changesgvproxy OwnIp Networking
Sequence Diagram(s)sequenceDiagram
participant PTask
participant GvproxySwitch
participant gvproxy
participant TAPRelay as SwitchRelay
rect rgba(70, 130, 180, 0.5)
note over PTask,gvproxy: OwnIp attach: IP allocation and gvproxy startup
PTask->>GvproxySwitch: attach()
GvproxySwitch->>GvproxySwitch: IpAllocator.allocate() → PtaskLease (IP + MAC)
GvproxySwitch->>GvproxySwitch: render_gvproxy_config() → YAML
GvproxySwitch->>gvproxy: spawn with config/socket/listen/pid args
GvproxySwitch->>gvproxy: wait for control socket ready (with timeout)
GvproxySwitch-->>PTask: Ok(PtaskLease {ip, mac})
end
rect rgba(60, 179, 113, 0.5)
note over PTask,TAPRelay: TAP device and relay attachment
PTask->>TAPRelay: open_tap(name) via TUNSETIFF ioctl
TAPRelay-->>PTask: OwnedFd
PTask->>TAPRelay: attach_to_switch(tap_fd, control_socket_path)
TAPRelay->>gvproxy: write HTTP CONNECT_REQUEST, upgrade to framing
TAPRelay-->>PTask: SwitchRelay handle
end
rect rgba(180, 100, 60, 0.5)
note over TAPRelay,gvproxy: Bidirectional Ethernet frame relay
par tap→switch
TAPRelay->>gvproxy: AsyncFd readable → read frame → write (2-byte LE len + bytes)
and switch→tap
gvproxy->>TAPRelay: read (2-byte LE len prefix + frame bytes) → AsyncFd writable → tap write
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
crates/minimald/src/net/switch.rs (1)
186-203: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winReuse the framed buffer in the relay hot path.
Line 200 allocates for every Ethernet frame. This relay runs per packet, so reusing a preallocated buffer avoids allocator pressure without changing behavior.
♻️ Proposed buffer reuse
let mut buf = vec![0u8; max_frame()]; + let mut framed = vec![0u8; 2 + max_frame()]; loop { let n = loop { let mut guard = tap.readable().await?; match guard.try_io(|inner| inner.get_ref().read(&mut buf)) { @@ if n == 0 { return Ok(()); } // One combined write keeps the length prefix and frame atomic even if // the socket closes between writes. - let mut framed = Vec::with_capacity(2 + n); - framed.extend_from_slice(&(n as u16).to_le_bytes()); - framed.extend_from_slice(&buf[..n]); - sock.write_all(&framed).await?; + framed[..2].copy_from_slice(&(n as u16).to_le_bytes()); + framed[2..2 + n].copy_from_slice(&buf[..n]); + sock.write_all(&framed[..2 + n]).await?; } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/minimald/src/net/switch.rs` around lines 186 - 203, The framed buffer is being allocated inside the relay loop for every Ethernet frame, creating unnecessary allocator pressure. Move the framed Vec allocation outside the main loop before the loop statement, and then inside the loop after reading the frame (after calculating n), clear the existing framed buffer instead of creating a new one each iteration. This keeps the same behavior of maintaining atomic writes with the length prefix while reusing the preallocated buffer, similar to how buf is already handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/minimald/src/net/mod.rs`:
- Around line 417-419: The code at lines 419 and 449-450 ignores errors from
std::fs::remove_file when attempting to clean up stale socket files by using let
_ = pattern. Instead of silently ignoring removal failures, you must check the
result of remove_file and handle errors appropriately - either by returning an
error from the current function or by logging and returning early so that
wait_for_socket does not proceed when a stale socket cannot be cleaned up. This
prevents gvproxy from failing to bind when the cleanup operation fails.
- Around line 443-465: In the `wait_for_socket()` method, when the socket
readiness deadline is reached and a `SocketTimeout` error is returned, the
spawned child process is not being cleaned up. Before returning the
`NetError::SocketTimeout` error, terminate the child process and set `self.child
= None` (similar to what is done in the `try_wait()` branch above) to ensure the
process is properly stopped and cleared when the timeout occurs, preventing the
child from continuing to run and causing issues with future attach operations.
- Around line 421-434: The spawned child process for gvproxy does not have
kill_on_drop enabled, which could leave the process orphaned if the supervisor
struct is dropped unexpectedly. After the final Stdio configuration in the
Command chain that starts with Command::new(&self.binary), add a call to
kill_on_drop(true) before spawning the child process to ensure the gvproxy
process is properly terminated when the parent supervisor is dropped without
explicit cleanup.
In `@crates/minimald/src/net/switch.rs`:
- Around line 221-226: Add a bounds check on the frame size `n` immediately
after converting it from the length bytes at line 221, before allocating the
vector and reading the frame data. Compare `n` against the result of
`max_frame()` to ensure it does not exceed the maximum allowed frame size, and
return an error if it does, preventing a malicious peer from triggering
oversized allocations or writes to the TAP interface via the sock.read_exact and
tap.writable operations.
In `@crates/minimald/tests/netns.rs`:
- Around line 79-108: Replace the manual namespace creation using ip netns
commands with the production sandbox2 API. Instead of using sudo with ip netns
add to create an independent namespace and then executing the egress test with
ip netns exec, create a real sandbox using Sandbox::new_container configured
with NetworkMode::NoNet, spawn the egress attempt process inside that sandbox,
and assert that the process fails due to network isolation enforced by the
sandbox. This ensures the test actually validates the production sandbox2 code
path rather than relying on manual namespace manipulation.
- Around line 139-153: Replace the fixed
tokio::time::sleep(Duration::from_millis(750)) call with a retry mechanism for
the client connection in the format! block instead of relying on a single fixed
delay. The client retry logic should attempt the /dev/tcp connection repeatedly
until successful within the existing 10-second timeout from the bash timeout
command. After making this change, verify if Duration is still used elsewhere in
the file; if the sleep was the only Duration usage, remove the Duration import
from line 22.
In `@crates/sandbox2/src/lib.rs`:
- Around line 491-493: The `network_namespaces_available()` function only checks
if the quota file `/proc/sys/user/max_net_namespaces` is positive, which does
not guarantee the current process can actually create network namespaces due to
capability, policy, or seccomp filter restrictions. Improve the
`network_namespaces_available()` function to probe actual namespace creation
capability by attempting to create a network namespace in a child process rather
than only reading the quota file, ensuring graceful fallback to host networking
if creation fails instead of deferring the error to spawn time.
---
Nitpick comments:
In `@crates/minimald/src/net/switch.rs`:
- Around line 186-203: The framed buffer is being allocated inside the relay
loop for every Ethernet frame, creating unnecessary allocator pressure. Move the
framed Vec allocation outside the main loop before the loop statement, and then
inside the loop after reading the frame (after calculating n), clear the
existing framed buffer instead of creating a new one each iteration. This keeps
the same behavior of maintaining atomic writes with the length prefix while
reusing the preallocated buffer, similar to how buf is already handled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9f4a500f-6b81-4a27-85a1-7bd041d09efa
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
crates/minimald/Cargo.tomlcrates/minimald/src/lib.rscrates/minimald/src/net/mod.rscrates/minimald/src/net/switch.rscrates/minimald/tests/netns.rscrates/sandbox2/src/lib.rs
|
Revise claim for head bee5ed1. |
|
Auto-revise 1 of 3. |
|
/revise Fix the musl build break that fails the
|
libc::ioctl's request parameter is c_ulong on glibc but c_int on musl. Typing the constant as libc::Ioctl (the per-target alias) makes it resolve to the correct width on both targets without any call-site cast. The value 0x4004_54ca fits in i32, so no truncation occurs on musl. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…s loss Address CodeRabbit and sdd-review findings on #525: - net: return stale-socket cleanup failures instead of letting wait_for_socket mistake a leftover path for a freshly-bound one and report a switch that never came up - net: set kill_on_drop on the gvproxy child so it is reaped if the supervisor is dropped without a clean stop() - net: tear the gvproxy child down when its control socket never appears, so a socket-readiness timeout cannot leave it orphaned - net/switch: reject switch->tap frames larger than the MTU-derived maximum, and drop possibly-truncated tap->switch jumbo frames with a warning instead of forwarding corrupt bytes - sandbox2: fail closed when NoNet/OwnIp is requested but the host cannot create a network namespace, rather than silently sharing the host network and voiding the isolation the mode promises (R1.2) - net: document that the gvproxy static-lease table is a startup-time seed, not a live source a running switch re-reads per attach Refs #496. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Commit pushed:
|
Addressed review feedback (commit
|
This comment has been minimized.
This comment has been minimized.
|
Revise claim for head 9189aa7. |
1 similar comment
|
Revise claim for head 9189aa7. |
|
Auto-revise 2 of 3. |
1 similar comment
|
Auto-revise 2 of 3. |
|
/revise Three review findings validated against HEAD 9189aa7 remain unaddressed (the other four are already fixed by 9189aa7 and resolved). Address these: 1. R1.4 — tear down own-IP PTasks when gvproxy exits unexpectedly (Medium, code). 2. UC1 proof must exercise the production sandbox path (Major, test). 3. Deterministic readiness in the UC6 proof (Minor, test). Keep changes minimal and in-scope; re-run the netns CI lane green. |
…, UC6 retry Address three review findings from #525: - R1.4: add a tokio::sync::watch channel to GvproxySwitch so each attach() returns an AttachResult carrying an exit_signal receiver. On unexpected gvproxy exit, fire the signal and reset attached to 0 so callers know to tear down their tap relays. - UC1: replace ip-netns-add/exec/del with sudo unshare --net, which calls the same CLONE_NEWNET syscall that sandbox2::new_container uses for NetworkMode::NoNet. The proof now exercises the production OS primitive rather than a hand-rolled iproute2 namespace. - UC6: replace the fixed 750ms sleep before the TCP connect with a retry loop that polls up to a 10s deadline, eliminating the flake source on slow CI runners. Refs #496. Refs #525.
Address three review findings from #525: - R1.4: add a tokio::sync::watch channel to GvproxySwitch so each attach() returns an AttachResult carrying an exit_signal receiver. On unexpected gvproxy exit, fire the signal and reset attached to 0 so callers know to tear down their tap relays. - UC1: replace ip-netns-add/exec/del with sudo unshare --net, which calls the same CLONE_NEWNET syscall that sandbox2::new_container uses for NetworkMode::NoNet. The proof now exercises the production OS primitive rather than a hand-rolled iproute2 namespace. - UC6: replace the fixed 750ms sleep before the TCP connect with a retry loop that polls up to a 10s deadline, eliminating the flake source on slow CI runners. Refs #496. Refs #525.
Addressed remaining review findings (commit
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/minimald/src/net/mod.rs`:
- Around line 433-441: The line `self.attached = 0;` in the gvproxy exit handler
creates a race condition where old PTasks that haven't yet processed the exit
signal can call `detach()` after a new `attach()` occurs, causing the counter to
incorrectly drop to zero and tear down the newly restarted gvproxy. Remove the
`self.attached = 0;` line entirely to avoid mixing generations; the counter will
be properly managed only by actual `attach()` and `detach()` calls.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cd4f0b4d-2e9d-4954-9dba-ab762574dcc0
📒 Files selected for processing (2)
crates/minimald/src/net/mod.rscrates/minimald/tests/netns.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- crates/minimald/tests/netns.rs
…cases - Remove unconditional self.attached = 0 on unexpected gvproxy exit so stale detach() calls from old-generation PTasks cannot decrement a new generation's counter to 0 and stop a live switch (R1.4). - Size the tap-read buffer to max_frame()+1 so a full-size 1518-byte VLAN-tagged frame is never incorrectly dropped as possibly-truncated. - Replace sock.exists() with a UnixStream::connect probe in wait_for_socket so ECONNREFUSED is retried on a bind-before-listen scheduler stall. - Reject zero-length frame length claims from the switch with a warn! and continue rather than writing an empty frame to the tap. - Wrap the tracing::error! call that exceeded the 100-char fmt limit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9dcbee4 to
cfc286e
Compare
This comment has been minimized.
This comment has been minimized.
|
/revise Two validated LOW findings remain (the other six are resolved: three fixed in cfc286e, three non-actionable nits). One consolidated commit: 1. No blocking
2. Keep it to one commit; re-run |
Address review feedback on the gvproxy switch net module: - SwitchSubnet::new now rejects a prefix wider than /8, enforcing the documented MacAddr::for_switch_ip uniqueness invariant (the derived MAC folds in only the low three octets, so the high octet must be pinned by the prefix). - wait_for_socket surfaces a try_wait() error instead of swallowing it into a misleading SocketTimeout. - relay_switch_to_tap issues one non-blocking write per try_io call so a partial write cannot restart the frame and re-emit its length prefix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Commit pushed:
|
|
Addressed the latest review batch with one follow-up commit (
Verification (in-sandbox): Note: the merged-change revert guard returned The three earlier findings on these files (socket readiness
|
sdd-validate findings — implementation boundaryPR: #525 · Gate 1 — Proof artifacts re-executed and passingAll three proof artifacts hit infrastructure limits in this agent container (package registry firewalled:
The PR body's stated proof output (11/11 passing for proof 1, authored-and-run-by-CI note for proofs 2–3) is consistent with the infrastructure split. The Gate 2 — Changed files within task scopeTask #496 stated scope:
Outside stated scope:
All other out-of-scope touches ( Gate 3 — No real credentials in the diffNo secrets, tokens, keys, or credentials detected. Clean. Summary
No Blockers. Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "index.crates.io"See Network Configuration for more information.
|
`sandbox2` treated `OwnIp` like `NoNet` and `SandboxLauncher` never constructed a switch, so an `OwnIp` PTask got an empty namespace and nothing downstream (#499/#500) had a running gvproxy to attach policy to. This lands the live R1.5 wiring on the minimald sandbox2/launcher side. - server.rs: construct the per-host `GvproxySwitch` + `IpAllocator` once at daemon scope (R1.4 one-gvproxy-per-host, R1.6 process-lifetime allocator) and thread the shared `Arc<Mutex<GvproxySwitch>>` through the sessions manager and session actor into every `SandboxLauncher`. The gvproxy binary path comes from a new `Config::gvproxy_bin` field (fixed install-path default when unset), not the test's `GVPROXY_BIN`. - session_host.rs: on an `OwnIp` launch, drive the attach on the minimald side — allocate a lease, `open_tap`, move the tap into the PTask netns (targeted by the `hakoniwa::Child`'s PID) and configure its MAC + `100.64.0.0/16` address + route, `attach_to_switch`, and hold the `SwitchRelay` (plus a switch-detach guard) for the session lifetime. `HostNet`/`NoNet` are unchanged. - net/switch.rs: add `tap_netns_commands` (single-sourced move/configure argv) + `move_tap_into_netns`; net/mod.rs exposes `SwitchSubnet::prefix` and `GvproxySwitch::subnet`. - sandbox2: no `minimald::net` call (that would be a dependency cycle); only the empty namespace is unshared and the PID surfaced. The stale `OwnIp` comment is corrected accordingly. - tests/netns.rs: the UC6 proof drives the production `tap_netns_commands` against a PID-identified netns (the same `CLONE_NEWNET` sandbox2 unshares), not a hand-rolled `ip netns` sequence. Refs #499, #525. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…547) * feat(minimald,sandbox2): wire net switch into live OwnIp launch path `sandbox2` treated `OwnIp` like `NoNet` and `SandboxLauncher` never constructed a switch, so an `OwnIp` PTask got an empty namespace and nothing downstream (#499/#500) had a running gvproxy to attach policy to. This lands the live R1.5 wiring on the minimald sandbox2/launcher side. - server.rs: construct the per-host `GvproxySwitch` + `IpAllocator` once at daemon scope (R1.4 one-gvproxy-per-host, R1.6 process-lifetime allocator) and thread the shared `Arc<Mutex<GvproxySwitch>>` through the sessions manager and session actor into every `SandboxLauncher`. The gvproxy binary path comes from a new `Config::gvproxy_bin` field (fixed install-path default when unset), not the test's `GVPROXY_BIN`. - session_host.rs: on an `OwnIp` launch, drive the attach on the minimald side — allocate a lease, `open_tap`, move the tap into the PTask netns (targeted by the `hakoniwa::Child`'s PID) and configure its MAC + `100.64.0.0/16` address + route, `attach_to_switch`, and hold the `SwitchRelay` (plus a switch-detach guard) for the session lifetime. `HostNet`/`NoNet` are unchanged. - net/switch.rs: add `tap_netns_commands` (single-sourced move/configure argv) + `move_tap_into_netns`; net/mod.rs exposes `SwitchSubnet::prefix` and `GvproxySwitch::subnet`. - sandbox2: no `minimald::net` call (that would be a dependency cycle); only the empty namespace is unshared and the PID surfaced. The stale `OwnIp` comment is corrected accordingly. - tests/netns.rs: the UC6 proof drives the production `tap_netns_commands` against a PID-identified netns (the same `CLONE_NEWNET` sandbox2 unshares), not a hand-rolled `ip netns` sequence. Refs #499, #525. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(minimald): kill sandbox process on OwnIp attach failure When `attach_own_ip` failed during an `OwnIp` launch, the spawned `hakoniwa::Child` was dropped without being killed. A `hakoniwa::Child` does not terminate on drop (it orphans the child, the same hazard the `kill_on_drop(true)` calls in `exec.rs`/`net/mod.rs` guard against), so a failed switch attach left the sandbox process running. Kill and reap the process explicitly in the attach error path before propagating the error. Also document two correctness constraints surfaced in review: sessions must be drained before the tokio runtime is stopped so each `OwnIpAttachment`'s scheduled `detach` runs, and the PTask network namespace is empty until `attach_own_ip` returns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(minimald): distinguish tap move from configure in error The OwnIp tap setup loop in move_tap_into_netns used the prefix "configuring PTask tap failed" for every command, but command 0 moves the tap into the PTask namespace (ip link set <tap> netns <pid>) rather than configuring it. Name the failing phase by command index so a move failure no longer reports as a configuration failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(minimald): reap sandbox process when kill fails on attach error The OwnIp attach error path used an `else if`, so `process.wait()` only ran when `process.kill()` succeeded. When `kill` fails with `ESRCH` because the sandbox process already exited during the attach window, `wait` never ran and the child was left as a zombie for the daemon lifetime. Split into two independent `if` arms so the child is always reaped — the standard SIGKILL-then-waitpid idiom. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Implements Unit 1 (U1-T2) of the spec-networking stack: the per-host gvproxy switch supervisor, switch-address allocation, the
OwnIptap relay, and network-namespace isolation insandbox2, with the UC1/UC6 network-namespace proof tests.Change
sandbox2now acts onnetwork_modeinnew_container:NoNetandOwnIprun in their own network namespace (only a downlo, no routes — so aNoNetPTask cannot egress, UC1), whileHostNetkeeps the current shared-namespace default. Where unprivileged network namespaces are unavailable it degrades to host networking rather than failing the spawn, mirroring the cgroup-setup fallback. The decision is the testedsandbox2::isolates_networkpredicate.minimald::netmodule (Linux-only):GvproxySwitchsupervises the single per-host gvproxy process, ref-counted against attachedOwnIpPTasks, withSIGTERM -> grace -> SIGKILLteardown matching the vmm child (R1.4).IpAllocatorhands out unique, never-reused addresses over the RFC-6598100.64.0.0/16default subnet (configurable viaSwitchSubnet), reserving the network/gateway/host-alias/broadcast addresses (R1.6).-subnetCLI flag.net::switch: tap-device creation (open_tap) and the HyperKit-framed relay (attach_to_switch) that bridges a netns tap onto the switch (R1.5/R1.7).tracingfor every spawn/stop/attach/detach (R1.8); noprintln!.Deviation from the task title (SCM_RIGHTS)
The task title says "OwnIp switch attachment via SCM_RIGHTS". The gvproxy v0.8.9 spike (
docs/spikes/2026-06-21-gvproxy-attachment.md, issue #511) established that the attachment is not an fd-pass: it is a bare HTTPPOST /connectupgrade on the control socket, after which raw Ethernet frames flow with a 2-byte little-endian length prefix (HyperKit framing). gvproxy never receives a file descriptor. This PR implements the spike's protocol (an async relay), not SCM_RIGHTS, per the merged spike and the resume guidance on #496.Proof artifacts
1. Test (runnable here) —
cargo test -p minimald --lib net::+cargo test -p sandbox2 --lib network. Exercises the allocator, subnet math, MAC derivation, YAML generation, HyperKit framing, and the network-isolation predicate. Fails on base: thenetmodule andisolates_networkdo not exist there.Gate also green:
cargo fmt --check(clean),cargo clippy -p minimald -p sandbox2 -p minimald-rpc --all-targets -- -D warnings(exit 0), andcargo test -p minimald -p sandbox2 -p minimald-rpc --no-run(all test targets, includingtests/netns.rs, compile).2. Test (UC1, netns — executed by CI) —
netns_uc1_nonet_refuses_egress. ANoNetnamespace (the topologysandbox2::isolates_network(NoNet)produces) refuses a TCP connect to8.8.8.8:80. Fails on base (references the newisolates_network).#[ignore], gated onMINIMALD_NETNS_TEST, run byci-netns.yml.3. Test (UC6, netns — executed by CI) —
netns_uc6_ownip_ptask_to_ptask. TwoOwnIpPTasks, each with a tap bridged onto the sharedGvproxySwitch, open a TCP connection to each other over their100.64.x.yswitch addresses. DrivesGvproxySwitch,open_tap, andattach_to_switch— none exist on base.#[ignore], gated onMINIMALD_NETNS_TEST, gvproxy fromGVPROXY_BIN, run byci-netns.yml.Scope note
NoNetis wired end-to-end: the session record'snetworkmode already threads intosandbox2, which now enforces the isolated namespace. TheOwnIpswitch-attachment building blocks (supervisor, tap relay, allocator) and their proofs land here; wiring the live session launcher (SandboxLauncher->GvproxySwitchtap provisioning atOwnIpPTask launch) is the remaining integration, deferred so it can be validated against the CI data path rather than shipped unverifiable.Refs #478. Closes #496.
Merging this pull request closes the task sub-issue #496. Once every task sub-issue of the tracking issue #478 is closed, the pipeline advances that tracking issue to
sdd:donefor a final human review.Summary by CodeRabbit