feat(minvmd): implement DM1 gvproxy tap relay for OwnIp PTasks - #540
Conversation
Wire the per-PTask gvproxy switch attachment (issue #535): write the subnet + dhcpStaticLeases into a gvproxy -config YAML, provision a host tap, and run the async TAP<->socket relay (POST /connect upgrade, then HyperKit-framed Ethernet) ported from minimald. attach_ptask now opens the tap and starts the relay, returning a handle that owns the relay task; allocate_ptask keeps the IP-only path. Preserves the #522 GvproxySwitch supervisor/stop/Drop/SwitchExit lifecycle. Adds the MINVMD_INTEGRATION_TEST-gated vsock relay proof and re-adds the gated DM1 CI step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughIntroduces a Linux-only async TAP↔gvproxy relay module ( ChangesDM1 async TAP↔gvproxy relay for OwnIp PTask networking
Sequence Diagram(s)sequenceDiagram
participant Test as E2E Test
participant GvproxyConfig
participant GvproxySwitch
participant relay_rs as net/relay.rs
participant gvproxy as gvproxy process
participant PTaskNetns as PTask netns
Test->>GvproxyConfig: write_config(leases)
GvproxyConfig->>gvproxy: spawn with -config YAML + -listen unix://socket
Test->>GvproxySwitch: attach_ptask(label, tap_name)
GvproxySwitch->>relay_rs: open_tap(tap_name)
relay_rs-->>GvproxySwitch: OwnedFd
GvproxySwitch->>relay_rs: attach_to_switch(tap_fd, api_sock)
relay_rs->>gvproxy: POST /connect HTTP/1.0 (upgrade)
relay_rs-->>GvproxySwitch: SwitchRelay (2 background tasks)
GvproxySwitch-->>Test: PtaskAttachment { switch_ip, mac }
Test->>PTaskNetns: move tap + configure address
Test->>PTaskNetns: ip netns exec addr show
PTaskNetns-->>Test: 100.64.x.x address present
Test->>PTaskNetns: ip netns exec ping gateway
PTaskNetns->>gvproxy: Ethernet frames via TAP↔relay
gvproxy-->>PTaskNetns: ping reply frames
Test->>GvproxySwitch: detach_ptask(attachment)
Note over relay_rs: Drop SwitchRelay → aborts relay tasks
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/minvmd/src/net/relay.rs (1)
176-181: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRelay task failures are silently swallowed and the two directions aren't cross-cancelled.
Both
JoinHandle<io::Result<()>>results are only everabort()ed onDrop; they are never awaited or logged. If a relay loop returns anErr(orrelay_switch_to_tapreturnsOk(())on EOF), the sibling task keeps running and the PTask loses connectivity with no diagnostic. Consider logging on task exit and tearing down the partner direction when either side ends so a half-broken relay becomes observable.♻️ One option: log terminal state on each direction
- let (sock_rx, sock_tx) = sock.into_split(); - let tap_to_switch = tokio::spawn(relay_tap_to_switch(Arc::clone(&tap), sock_tx)); - let switch_to_tap = tokio::spawn(relay_switch_to_tap(sock_rx, tap)); + let (sock_rx, sock_tx) = sock.into_split(); + let tap_to_switch = tokio::spawn(async move { + let r = relay_tap_to_switch(Arc::clone(&tap), sock_tx).await; + if let Err(ref e) = r { + tracing::warn!(error = %e, "tap→switch relay terminated with error"); + } + r + }); + let switch_to_tap = tokio::spawn(async move { + let r = relay_switch_to_tap(sock_rx, tap).await; + if let Err(ref e) = r { + tracing::warn!(error = %e, "switch→tap relay terminated with error"); + } + r + });Note
tapis moved into the first closure above; keep theArc::clone/move ordering consistent with the surrounding borrow oftap. Cross-cancelling the sibling (e.g. via a sharedCancellationTokenor aborting the other handle on exit) would additionally avoid a lingering task after one side closes.Want me to draft the cross-cancellation variant?
🤖 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/minvmd/src/net/relay.rs` around lines 176 - 181, The relay task spawning in SwitchRelay does not implement proper monitoring or cross-cancellation between the two relay directions. Currently, the JoinHandles for relay_tap_to_switch and relay_switch_to_tap are never awaited or logged, meaning failures or EOF on either side silently swallow errors with no diagnostics, while the sibling task continues running orphaned. Implement cross-cancellation by either sharing a CancellationToken between both relay closures or by having each spawned task monitor its sibling handle and abort it when it exits. Additionally, add logging at task completion for both relay_tap_to_switch and relay_switch_to_tap to capture success or error states. Ensure the Arc::clone and move ordering for tap remains consistent with the current pattern when passing it to the relay closures.
🤖 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/minvmd/src/net.rs`:
- Around line 105-114: The host_alias() function returns the last usable address
(at span-2), not the second-from-last as the documentation claims. Fix the doc
comment to correctly state that host_alias() returns the "last usable address"
instead of "second-from-last usable address". Additionally, modify allocate_ip()
to reserve this alias address so it never allocates the same IP that
host_alias() returns, preventing PTask collisions with the gvproxy host NAT
address. Ensure allocate_ip() stops incrementing next_index before it would
allocate the reserved alias address.
---
Nitpick comments:
In `@crates/minvmd/src/net/relay.rs`:
- Around line 176-181: The relay task spawning in SwitchRelay does not implement
proper monitoring or cross-cancellation between the two relay directions.
Currently, the JoinHandles for relay_tap_to_switch and relay_switch_to_tap are
never awaited or logged, meaning failures or EOF on either side silently swallow
errors with no diagnostics, while the sibling task continues running orphaned.
Implement cross-cancellation by either sharing a CancellationToken between both
relay closures or by having each spawned task monitor its sibling handle and
abort it when it exits. Additionally, add logging at task completion for both
relay_tap_to_switch and relay_switch_to_tap to capture success or error states.
Ensure the Arc::clone and move ordering for tap remains consistent with the
current pattern when passing it to the relay closures.
🪄 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: d60e8f4a-fa6f-4591-82c4-ddcea9a14b8a
📒 Files selected for processing (5)
.github/workflows/ci-linux-kvm.ymlcrates/minvmd/src/net.rscrates/minvmd/src/net/relay.rscrates/minvmd/src/vm.rscrates/minvmd/tests/vsock_relay_e2e.rs
|
Hardware-lane feedback (the first real KVM run of this blind impl):
So gvproxy launches and the relay plumbing runs, but the PTask netns never receives
This is the expected human/hardware-iteration boundary (#535) — the rest of #540 CI is green, so it is reviewable with this e2e as the known, captured follow-up. Not thrashing a blind fix. |
the vsock_relay_e2e proof asserted the ptask netns carries the switch ip but never created the netns, moved the tap into it, or applied the static lease address. attach_ptask only opens the host tap + starts the relay (it delegates netns config to the caller, per its docstring), so the netns was empty and the ip-show returned nothing. mirror the minimald uc6 netns proof (crates/minimald/tests/netns.rs): create ptask-vmtap2 before attach, then after attach move the tap into it and statically configure mac/ip/route per the gvproxy v0.8.9 spike's option-b recipe. runs as root under the ci `sudo -E` step, so no sudo prefix is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
CodeRabbit (#540): host_alias() is host(span-2), the last usable address, and allocate_ip() advances next_index until host() returns None — so its final allocation was exactly the alias, letting a PTask collide with the address gvproxy NATs to 127.0.0.1. Guard allocate_ip() to skip the alias, and correct the host_alias doc (last, not second-from-last, usable address). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
|
@coderabbitai review |
✅ Action performedReview finished.
|
Closes #535 (the human-driven hardware DM1 relay split out of #526; the headless agent correctly handed it off — protected
.github/path + KVM/libkrun hardware verification it cannot do).What this implements (per spike #512 — relay model, NOT fd-pass)
Models on the merged minimald relay (
crates/minimald/src/net/switch.rs):-configYAML (crates/minvmd/src/net.rs):render_gvproxy_config(subnet + gatewayIP + NAT host-alias +dhcpStaticLeases),MacAddr::for_switch_ip,GvproxyConfignow writes the YAML andargv()emits-config <yaml> -listen <sock>instead of only-listen.crates/minvmd/src/net/relay.rs, new,#[cfg(target_os = "linux")]):open_tap(/dev/net/tun+TUNSETIFF),attach_to_switch(barePOST /connectupgrade, then 2-byte-LE-framed Ethernet frames both directions),SwitchRelay.attach_ptasknow (on Linux) allocates the IP, opens the tap, starts the relay, and returns aPtaskAttachmentthat owns the relay task (drop = detach). The portableallocate_ptaskIP+MAC path is retained for unit tests.vm.rs:VmConfig::is_own_ip()+ deterministic IFNAMSIZ-safetap_name(index)for the OwnIp PTask.GvproxySwitchsupervisor /stop/Drop/SwitchExitsemantics unchanged.Proof
crates/minvmd/tests/vsock_relay_e2e.rs(new): path matchesvsock,#[ignore], gated onMINVMD_INTEGRATION_TEST+cfg(minvmd_libkrun, target_os="linux"). Boots an OwnIp VM, attaches via the relay, asserts a100.64.0.0/16switch IP in the PTask netns and a gateway ping traversing the relay..github/workflows/ci-linux-kvm.yml: re-added the gated DM1 step (cargo test -p minvmd vsock -- --include-ignored --nocaptureunderMINVMD_INTEGRATION_TEST=1,sudo -Efor CAP_NET_ADMIN) + materializes the pinned gvproxy binary.Verification
Verified locally (macOS):
cargo build -p minvmd,cargo test -p minvmd(71 passed; e2e correctly ignored),cargo fmt --all -- --check,cargo clippy -p minvmd --all-targets -- -D warnings— all clean. The Linux-gatedrelay.rs+attach_ptaskwere additionally cross-compiled + clippy-checked in arust:1.95container.The hardware relay proof runs only on the KVM + libkrun CI lanes (no
/dev/kvm/gvproxylocally) — that lane is the authoritative validation for the end-to-end relay.🤖 Generated with Claude Code
Summary by CodeRabbit
MINVMD_INTEGRATION_TEST=1) to validate switch IP reachability inside the OwnIp network namespace.