Skip to content

feat(minvmd): implement DM1 gvproxy tap relay for OwnIp PTasks - #540

Merged
norrietaylor merged 3 commits into
mainfrom
feat/535-minvmd-gvproxy-relay
Jun 23, 2026
Merged

feat(minvmd): implement DM1 gvproxy tap relay for OwnIp PTasks#540
norrietaylor merged 3 commits into
mainfrom
feat/535-minvmd-gvproxy-relay

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jun 23, 2026

Copy link
Copy Markdown
Member

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):

  • gvproxy -config YAML (crates/minvmd/src/net.rs): render_gvproxy_config (subnet + gatewayIP + NAT host-alias + dhcpStaticLeases), MacAddr::for_switch_ip, GvproxyConfig now writes the YAML and argv() emits -config <yaml> -listen <sock> instead of only -listen.
  • TAP provisioning + async relay (crates/minvmd/src/net/relay.rs, new, #[cfg(target_os = "linux")]): open_tap (/dev/net/tun + TUNSETIFF), attach_to_switch (bare POST /connect upgrade, then 2-byte-LE-framed Ethernet frames both directions), SwitchRelay.
  • attach_ptask now (on Linux) allocates the IP, opens the tap, starts the relay, and returns a PtaskAttachment that owns the relay task (drop = detach). The portable allocate_ptask IP+MAC path is retained for unit tests.
  • vm.rs: VmConfig::is_own_ip() + deterministic IFNAMSIZ-safe tap_name(index) for the OwnIp PTask.
  • Preserves the feat(minvmd): extend per-VM gvproxy to full PTask switch with vsock shuttle #522 GvproxySwitch supervisor / stop / Drop / SwitchExit semantics unchanged.

Proof

  • crates/minvmd/tests/vsock_relay_e2e.rs (new): path matches vsock, #[ignore], gated on MINVMD_INTEGRATION_TEST + cfg(minvmd_libkrun, target_os="linux"). Boots an OwnIp VM, attaches via the relay, asserts a 100.64.0.0/16 switch 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 --nocapture under MINVMD_INTEGRATION_TEST=1, sudo -E for 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-gated relay.rs + attach_ptask were additionally cross-compiled + clippy-checked in a rust:1.95 container.

The hardware relay proof runs only on the KVM + libkrun CI lanes (no /dev/kvm/gvproxy locally) — that lane is the authoritative validation for the end-to-end relay.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Linux-only async TAP↔gvproxy relaying with automatic TAP provisioning and proper Ethernet frame handling.
    • Improved OwnIp networking by introducing deterministic tap naming and predictable switch IP/MAC lease assignment.
    • Enhanced gvproxy startup by generating a lease-driven configuration before launching the relay.
  • Tests
    • Added an ignored-by-default Linux integration test (enabled with MINVMD_INTEGRATION_TEST=1) to validate switch IP reachability inside the OwnIp network namespace.
    • Updated Linux/KVM CI to fetch/materialize the gvproxy relay binary and run the new test with required privileges.

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
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 403395ef-aeb0-4dc6-b2be-acab4d0b93dd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a Linux-only async TAP↔gvproxy relay module (net/relay.rs) using HTTP upgrade and 2-byte LE Ethernet framing. Refactors GvproxySwitch/PtaskAttachment APIs to include YAML config generation (MacAddr, render_gvproxy_config), relay lifecycle management, and a new VmConfig::tap_name. Adds an E2E integration test and corresponding CI workflow steps.

Changes

DM1 async TAP↔gvproxy relay for OwnIp PTask networking

Layer / File(s) Summary
TAP provisioning and relay constants
crates/minvmd/src/net/relay.rs
Adds relay.rs with CONNECT_REQUEST, TAP ioctl flags, max_frame(), open_tap via TUNSETIFF, set_nonblocking, the SwitchRelay handle with Drop-based task abort, and unit tests for framing and open_tap name-length validation.
Bidirectional TAP↔switch relay loops
crates/minvmd/src/net/relay.rs
Implements attach_to_switch (HTTP upgrade, AsyncFd wrapping, task spawning), the tap→switch loop (2-byte LE prefix, truncation guard), and the switch→tap loop (prefix read, zero-length skip, MTU validation, frame-atomic write, clean shutdown on EOF).
MacAddr, SwitchSubnet helpers, and gvproxy YAML config
crates/minvmd/src/net.rs
Adds MacAddr with Display and for_switch_ip, SwitchSubnet::host_alias/Display, render_gvproxy_config producing subnet/gateway/NAT-alias/dhcpStaticLeases YAML, GvproxyConfig config_path field, updated argv/write_config/spawn, module docs, fmt import, and relay re-exports with DEFAULT_MTU/GATEWAY_MAC constants.
GvproxySwitch allocation, async attach, and PtaskAttachment shape
crates/minvmd/src/net.rs
Refactors GvproxySwitch with allocate_ip, async attach_ptask (provisions TAP + starts relay), allocate_ptask (relay-free path), and detach_ptask by value; changes PtaskAttachment to store mac and optional SwitchRelay; adds mac/lease accessors; updates unit tests for host_alias/Display, MacAddr derivation, YAML generation, config writing, and sequential IP allocation.
VmConfig OwnIp and tap_name helpers
crates/minvmd/src/vm.rs
Adds is_own_ip() and tap_name(index) to VmConfig, with tests asserting HostNet/NoNet are not OwnIp, tap_name determinism, index distinctness, and IFNAMSIZ compliance.
E2E integration test and CI wiring
crates/minvmd/tests/vsock_relay_e2e.rs, .github/workflows/ci-linux-kvm.yml
Adds the own_ip_ptask_gets_switch_ip_via_vsock_relay test (gated on MINVMD_INTEGRATION_TEST, requires KVM/CAP_NET_ADMIN) that boots an OwnIp VM, attaches to gvproxy via TAP, asserts switch IP in the PTask netns via ip netns exec addr show, pings the gateway, and tears down; adds CI steps to fetch gvproxy and run the test under sudo -E.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #535 (DM1 vsock relay: TAP↔gvproxy + OwnIp netns wiring): This PR directly implements all deliverables from #535: the async TAP↔gvproxy relay, gvproxy -config YAML generation, OwnIp PTask attachment wiring, and the gated KVM CI proof step.
  • #526: Targets complementary hardening of the relay infrastructure via prefix validation and lifecycle tracking in the same net.rs classes/methods.

Possibly related PRs

  • gominimal/minimal#522: Established the GvproxyConfig/GvproxySwitch/PtaskAttachment APIs and OwnIp attachment intent that this PR directly extends with async TAP relay and YAML config generation.
  • gominimal/minimal#512: The spike that documented the HTTP POST /connect upgrade protocol and 2-byte LE framing that this PR's relay.rs directly operationalizes.
  • gominimal/minimal#525: Introduced the same open_tap + attach_to_switch/SwitchRelay plumbing and OwnIp PTask switch IP propagation pattern implemented here.

Suggested labels

needs-human

Suggested reviewers

  • bryan-minimal

Poem

🐇 A TAP and a socket, a handshake to share,
Two-byte LE frames dancing through the air!
The switch assigns IPs deep in the netns,
The gateway pings back — what relay finesse! 🎉
Said the rabbit: "Our packets now travel with grace,
Through gvproxy's tunnel at a blistering pace!" 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main feature being implemented: a DM1 gvproxy TAP relay for OwnIp PTasks, which directly matches the primary changes across all modified files.
Linked Issues check ✅ Passed The PR fully addresses issue #535 requirements: implements TAP/netns provisioning, async relay with proper framing, gvproxy YAML config, VM helpers, and includes corrected integration test with netns setup and frame validation.
Out of Scope Changes check ✅ Passed All changes align with #535 scope: TAP relay implementation, gvproxy config generation, attachment flow, VM helpers, CI workflow additions, and integration testing—no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/minvmd/src/net/relay.rs (1)

176-181: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Relay task failures are silently swallowed and the two directions aren't cross-cancelled.

Both JoinHandle<io::Result<()>> results are only ever abort()ed on Drop; they are never awaited or logged. If a relay loop returns an Err (or relay_switch_to_tap returns Ok(()) 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 tap is moved into the first closure above; keep the Arc::clone/move ordering consistent with the surrounding borrow of tap. Cross-cancelling the sibling (e.g. via a shared CancellationToken or 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

📥 Commits

Reviewing files that changed from the base of the PR and between b239125 and 8e0c0ef.

📒 Files selected for processing (5)
  • .github/workflows/ci-linux-kvm.yml
  • crates/minvmd/src/net.rs
  • crates/minvmd/src/net/relay.rs
  • crates/minvmd/src/vm.rs
  • crates/minvmd/tests/vsock_relay_e2e.rs

Comment thread crates/minvmd/src/net.rs
@norrietaylor

Copy link
Copy Markdown
Member Author

Hardware-lane feedback (the first real KVM run of this blind impl):

minvmd-linux-kvm-e2e → the DM1 relay E2E reached the proof and got close — build, boot, and gvproxy v0.8.9 spawn all pass — but the assertion failed:

test own_ip_ptask_gets_switch_ip_via_vsock_relay ... FAILED
  crates/minvmd/tests/vsock_relay_e2e.rs:135:
  PTask netns ptask-vmtap2 must carry switch IP 100.64.0.2; got: <empty>

So gvproxy launches and the relay plumbing runs, but the PTask netns never receives 100.64.0.2. The IP-assignment path is the gap, not the relay scaffolding. Likely culprits for hardware iteration:

  • gvproxy dhcpStaticLeases / -config not actually handing out the lease (verify the rendered YAML + that gvproxy reads it), or
  • the tap not configured/up inside the netns (the relay connects but no DHCP client runs / no static ip addr is applied), or
  • the relay POST /connect upgrade succeeds but frames are not bridged to the in-netns tap.

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

@bryan-minimal bryan-minimal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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
@norrietaylor

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

DM1 vsock relay: TAP↔gvproxy + OwnIp netns wiring (human-driven, hardware-gated)

2 participants