Repository navigation
docs(arch-minvmd-networking-gvproxy): minvmd networking — gvproxy userspace net for VM sessions #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
norrietaylor
merged 3 commits into
main
from
arch/minvmd-networking-gvproxy-06efc65792cab481
Jun 17, 2026
Merged
docs(arch-minvmd-networking-gvproxy): minvmd networking — gvproxy userspace net for VM sessions #445
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
9b23f52
docs(arch-minvmd-networking-gvproxy): add architecture for gvproxy us…
gominimal-aw-bot[bot] 70d6925
docs(minvmd): clarify gvproxy networking spec FD, reaping, and fallback
norrietaylor 5ff2483
Merge branch 'main' into arch/minvmd-networking-gvproxy-06efc65792cab481
norrietaylor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
185 changes: 185 additions & 0 deletions
185
docs/specs/03-spec-minvmd-networking-gvproxy/architecture.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| --- | ||
| id: arch-minvmd-networking-gvproxy | ||
| title: minvmd networking — gvproxy userspace net for VM sessions — architecture | ||
| kind: architecture | ||
| status: planned | ||
| tracking-issue: 404 | ||
| --- | ||
|
|
||
| # minvmd networking — gvproxy userspace net for VM sessions — architecture | ||
|
|
||
| ## Chosen approach | ||
|
|
||
| `minvmd` will spawn one `gvproxy` child process per VM, connected to the VM's virtio-net device via libkrun's `krun_set_passt_fd` API. The gvproxy child is owned by the VM supervisor (`run_foreground`) and is reaped when the VM stops or crashes, ensuring no orphan processes after `minimal stop` or an abnormal exit. | ||
|
|
||
| The mechanism uses a Unix socketpair to wire the gvproxy process to the guest's virtio-net device: | ||
|
|
||
| 1. The supervisor (`run_foreground`) creates a Unix `socketpair(AF_UNIX, SOCK_STREAM, 0)` before spawning any child. | ||
| 2. One end of the socketpair is passed to `gvproxy` via its `--fd <n>` argument; gvproxy reads/writes VM network frames on this FD. | ||
| 3. The other end's FD number is exported to the VMM child via `MINVMD_NET_FD` environment variable. | ||
| 4. The VMM child calls `krun_set_passt_fd(ctx, fd)` before `krun_start_enter`, which tells libkrun to create a virtio-net device wired to the FD. | ||
| 5. The guest kernel sees a regular `eth0` interface with gvproxy acting as NAT gateway. | ||
|
|
||
| TSI (Transport Socket Interface) remains available as a fallback: when `MINVMD_NETMODE=tsi` is set or when gvproxy is not found on `PATH`, no `krun_set_passt_fd` call is made and libkrun falls back to its built-in TSI shim. | ||
|
|
||
| The network allowlist enforcement hook is a call site in the boot path wired to a `check_network_policy(policy: &NetworkPolicy)` function. The function body is a no-op (policy default: open / allow all) for this spec. The taskspec `network` field (specced in the capability envelope issue) will be wired to this call site in a follow-up. | ||
|
|
||
| ## Data and interface changes | ||
|
|
||
| ### New module: `crates/minvmd/src/net.rs` | ||
|
|
||
| A new networking module that owns: | ||
|
|
||
| - `enum NetworkMode { GvProxy, Tsi }` — the two supported transport modes | ||
| - `fn resolve_net_mode() -> NetworkMode` — reads `MINVMD_NETMODE` env var; returns `GvProxy` by default | ||
| - `fn gvproxy_bin() -> std::path::PathBuf` — resolves the gvproxy binary path via `MINVMD_GVPROXY_PATH` env var or defaults to `"gvproxy"` (resolved via `PATH`) | ||
| - `fn spawn_gvproxy(net_fd: RawFd) -> Result<Child>` — spawns gvproxy with `--fd <net_fd>` argument | ||
| - `enum NetworkPolicy { Open, Allowlist(Vec<String>) }` — network access policy for a VM session | ||
| - `enum PolicyError { Denied { host: String } }` — `#[derive(Debug, thiserror::Error)]` error returned by the enforcement hook; for this spec it has a single `Denied` variant reserved for the follow-up allowlist work, so the hook can grow real failure modes without changing its signature | ||
| - `fn check_network_policy(policy: &NetworkPolicy) -> Result<(), PolicyError>` — enforcement hook (currently always returns `Ok(())`) | ||
|
|
||
| ### Changes to `crates/minvmd/src/cmd/run.rs` | ||
|
|
||
| `run_foreground` will be extended to: | ||
|
|
||
| - Call `resolve_net_mode()` to determine whether to use gvproxy or TSI | ||
| - When `GvProxy` mode: | ||
| - Create a Unix socketpair before spawning the VMM child | ||
| - Clear `FD_CLOEXEC` on **both** ends of the socketpair before the respective spawns: on the gvproxy-side FD so it survives the gvproxy spawn, and on the VMM-side FD so it survives the VMM child's `fork()+exec()`. Both FDs must be inheritable for the handoff to work; clearing only the gvproxy side would leave the VMM child unable to open the FD named by `MINVMD_NET_FD`. | ||
| - Spawn gvproxy via `spawn_gvproxy(gvproxy_fd)` before spawning the VMM child. No explicit readiness handshake is required: the socketpair buffers frames written by the guest before gvproxy's read loop starts, so the two children may be spawned in sequence without coordination. gvproxy connecting late only delays the first packet; it does not drop the connection (a `SOCK_STREAM` socketpair is connected at creation, so there is no connect-before-listen race — the VMM's virtio-net device is wired to an already-open FD, not a pending connection). | ||
| - Export the VMM-side FD number as `MINVMD_NET_FD=<n>` in the VMM child's environment | ||
| - Close the gvproxy-side FD in the parent after gvproxy has started (the VMM-side FD is likewise closed in the parent after the VMM child is spawned, leaving each FD owned solely by its child) | ||
| - Call `check_network_policy(&NetworkPolicy::Open)` before spawning gvproxy (early-exit on error) | ||
| - In all exit paths (boot failure, VMM child exit, signal), call `gvproxy_child.kill()` then `gvproxy_child.wait()` to ensure no orphans. Reaping is best-effort: `kill()` on an already-exited child is ignored (`ESRCH` treated as success), `wait()` retries on `EINTR`, and a reap failure is logged at `tracing::warn!` but never aborts the exit path — the supervisor always proceeds to its own teardown so a stuck gvproxy cannot wedge `minvmd` shutdown. | ||
| - The `StartingGuard` drop path must also kill gvproxy. `StartingGuard` is the existing RAII guard defined in `crates/minvmd/src/state.rs` that resets the persisted lifecycle state to `Stopped` on drop unless `commit()` was called (R4.6); the gvproxy reap is hooked into the same early-exit path so that a bail before `commit()` tears down gvproxy alongside the state reset. | ||
|
|
||
| ### Changes to `crates/minvmd/src/state.rs` | ||
|
|
||
| `State` struct will gain an optional field `gvproxy_pid: Option<u32>` alongside the existing `vmm_pid`. The field is: | ||
| - `None` when `NetworkMode::Tsi` is active or when gvproxy has not yet started | ||
| - Serialized to / deserialized from the state file (JSON) so that a supervisor crash leaves the PID readable for external cleanup tooling | ||
| - Not used by `minvmd stop` for signal delivery (the stop path already signals `vmm_pid`, which causes the VMM child to exit, which causes the supervisor to reap gvproxy) | ||
|
|
||
| ### New FFI binding in `crates/minvmd/src/krun/raw.rs` | ||
|
|
||
| ```rust | ||
| pub fn krun_set_passt_fd(ctx_id: u32, fd: i32) -> i32; | ||
| ``` | ||
|
|
||
| This declaration carries a `// SAFETY:` comment documenting that: | ||
| - `fd` must be a valid open file descriptor for the duration of the call | ||
| - libkrun takes ownership of the networking FD (the caller must not close it after this call) | ||
| - `ctx_id` must refer to a live context | ||
|
|
||
| ### Safe wrapper in `crates/minvmd/src/krun/ctx.rs` | ||
|
|
||
| ```rust | ||
| pub fn set_passt_fd(&mut self, fd: std::os::unix::io::RawFd) -> Result<(), VmError> | ||
| ``` | ||
|
|
||
| This validates `fd >= 0` before the FFI call (returning `VmError::Io` wrapping `EBADF` on negative values) and translates the return code via `raw::check_backend`. | ||
|
|
||
| ### Changes to `crates/minvmd/src/vm.rs` | ||
|
|
||
| `VmConfig` will gain a new field `net_mode: NetworkMode` (imported from `crate::net`). `VmConfig::new` will gain a corresponding parameter. | ||
|
|
||
| The existing `VmConfig.apply()` will: | ||
| - Remove the "R2.5: no network device in v0.1" comment | ||
| - Grow its signature to `fn apply(&self, ctx: &mut Context, net_fd: Option<RawFd>)` | ||
| - Add a branch: | ||
| - When `net_mode == NetworkMode::GvProxy` and `net_fd` is `Some(fd)`: call `ctx.set_passt_fd(fd)` | ||
| - When `net_mode == NetworkMode::Tsi` or `net_fd` is `None`: no `set_passt_fd` call; libkrun's built-in TSI shim handles guest networking as before | ||
| - The `net_mode == NetworkMode::GvProxy` with `net_fd == None` combination is a silent fall-through to the TSI branch, **not** a hard error: `apply()` logs at `tracing::warn!` that gvproxy mode was requested but no FD was supplied (gvproxy unavailable or failed to spawn) and continues with TSI so the VM still boots with working networking. The supervisor in `run.rs` owns the decision of whether a missing gvproxy is fatal; by the time `apply()` runs, falling back is strictly better than aborting a VM that already started. | ||
| - Remove the `#[cfg(target_os = "macos")]` gate on `apply()` — the method becomes platform-agnostic since `krun_set_passt_fd` works on both macOS (Hypervisor.framework) and Linux (KVM) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| ### Changes to `crates/minvmd/src/cmd/vmm_child.rs` | ||
|
|
||
| The `run()` function will: | ||
| - Read `MINVMD_NET_FD` env var; if present and parseable as a `RawFd`, pass `Some(fd)` to `VmConfig.apply()`; if absent or unparseable, pass `None` (TSI fallback) | ||
| - Remove the `#[cfg(not(target_os = "macos"))]` bail so the VMM child compiles and runs on Linux as well (aligned with the Linux KVM spec minimal#397) | ||
| - Log at `tracing::info!` whether gvproxy or TSI mode is active | ||
|
|
||
| ## Alternatives considered | ||
|
|
||
| ### Alternative A: gvproxy as a sibling process of the supervisor | ||
|
|
||
| Instead of spawning gvproxy as a child of the supervisor, spawn it as a separate process tree (e.g., via a systemd service or a separate launcher). | ||
|
|
||
| **Rejected because:** | ||
| - The supervisor loses control over gvproxy lifecycle — if the supervisor crashes, gvproxy becomes an orphan | ||
| - Complicates the cleanup path: `minimal stop` would need to track gvproxy's PID separately and signal it, introducing a race condition | ||
| - Adds deployment complexity: users would need to manage two separate processes | ||
|
|
||
| The child-of-supervisor model ensures gvproxy is automatically reaped when the VM exits, matching the existing process model for the VMM child (informed by arch-minvmd-host-daemon). | ||
|
|
||
| ### Alternative B: Use TSI exclusively, do not add gvproxy | ||
|
|
||
| Continue using libkrun's built-in TSI socket-emulation shim for all networking. | ||
|
|
||
| **Rejected because:** | ||
| - TSI has a practical ceiling of ~62 concurrent connections (documented in the existing codebase at `vm.rs` line 48: "R3.5: TSI ~62-concurrent-connection cap") | ||
| - The cache-fetch parallelism target is 200 concurrent outbound connections (informed by minimal#204) | ||
| - TSI multiplexes all guest TCP/IP connections over a single host transport via a state-tracking shim; the ~62 limit is an internal state-table bound in libkrun's TSI implementation | ||
| - gvproxy bypasses TSI entirely by replacing it with a real virtio-net device backed by a userspace network stack | ||
|
|
||
| TSI remains selectable as a fallback via `MINVMD_NETMODE=tsi` for debugging or environments where gvproxy is unavailable. | ||
|
|
||
| ### Alternative C: In-kernel networking via tap devices | ||
|
|
||
| Use Linux tap devices or macOS vmnet.framework instead of gvproxy's userspace network stack. | ||
|
|
||
| **Rejected because:** | ||
| - tap devices require root privileges or specific capabilities (CAP_NET_ADMIN) — `minvmd` runs as the user | ||
| - macOS vmnet.framework requires entitlements that are incompatible with the existing code-signing model | ||
| - Cross-platform: tap is Linux-only; vmnet is macOS-only; gvproxy works on both | ||
| - gvproxy's passt protocol is already supported by libkrun via `krun_set_passt_fd` since v1.18 | ||
|
|
||
| gvproxy is the minimal-privilege, cross-platform solution. | ||
|
|
||
| ## Assumption ledger | ||
|
|
||
| | Assumption | Bucket | Evidence | Depends on | | ||
| |------------|--------|----------|------------| | ||
| | `krun_set_passt_fd` exists in libkrun v1.18+ | settled | Confirmed in libkrun C API documentation; function signature matches spec R2.1 | R2.1, R2.2 | | ||
| | gvproxy binary will be available on PATH or via `MINVMD_GVPROXY_PATH` | settled | Spec explicitly defers packaging; detection and fallback to TSI is in scope (R1.2) | R1.1, R1.2 | | ||
| | socketpair FD handoff survives fork+exec when `FD_CLOEXEC` is cleared | settled | Standard Unix FD inheritance behavior; documented in POSIX `fork()` / `exec()` semantics | R1.3 | | ||
| | libkrun's `krun_set_passt_fd` is platform-agnostic (macOS + Linux) | settled | Confirmed by spec R2.4 and arch-minvmd-linux-kvm: libkrun exports the same C API on macOS (Hypervisor.framework) and Linux (KVM) (informed by arch-minvmd-linux-kvm) | R2.4 | | ||
| | gvproxy exits on EOF when the socketpair FD is closed | settled | Standard gvproxy behavior documented in `containers/gvisor-tap-vsock` — gvproxy's main loop reads frames from the FD and exits on EOF | R1.3 orphan prevention | | ||
| | TSI remains functional when `krun_set_passt_fd` is not called | settled | Confirmed by existing codebase behavior: `VmConfig.apply()` does not call any networking FFI in v0.1, and TSI is libkrun's default (informed by crates/minvmd/src/vm.rs line 26: "R2.5: no network device in v0.1") | R1.3 | | ||
| | 200 concurrent outbound connects will succeed with gvproxy | settled | gvproxy implements a full virtio-net device with no connection-count limit; confirmed by `containers/gvisor-tap-vsock` design (informed by minimal#204: TSI cap is ~62, gvproxy has no such limit) | Spec Goal 2 | | ||
|
|
||
| All assumptions are settled. No spikes are required. | ||
|
|
||
| ## Knowledge gaps | ||
|
|
||
| ### Prior constraints | ||
|
|
||
| - **Networking was explicitly deferred in `arch-minvmd-host-daemon`:** "gvproxy/TSI integration (#160) is explicitly out of scope for v0.1. The TSI ~62-concurrent-connection cap is documented as a constraint near the vsock registration but does not affect the v0.1 architecture (< 10 concurrent connections expected)." (informed by arch-minvmd-host-daemon). This spec lifts that deferral by adding gvproxy support while keeping TSI as a fallback. | ||
|
|
||
| - **The process model (parent supervisor, child VMM) is established:** `run_foreground` already spawns the VMM child and supervises it until exit. gvproxy becomes a second child of the supervisor, reusing the same lifecycle pattern (informed by arch-minvmd-host-daemon). | ||
|
|
||
| - **The platform-agnostic design is established:** `arch-minvmd-linux-kvm` removed platform gates from `VmConfig.apply()` because libkrun exports the same C API on macOS and Linux. `krun_set_passt_fd` follows the same pattern (informed by arch-minvmd-linux-kvm). | ||
|
|
||
| ### Referenced-but-missing artifacts | ||
|
|
||
| None. All referenced prior work is present in the knowledge store. | ||
|
|
||
| ### Contradictions | ||
|
|
||
| None. This spec is consistent with the prior architecture decisions. | ||
|
|
||
| ### Thin areas | ||
|
|
||
| 640B | - **Network allowlist enforcement:** The `check_network_policy` hook is wired but its body is a no-op. Real enforcement (deny-vs-allow default, warn-vs-silent for unenforced capabilities, per-task vs per-VM granularity) depends on the capability envelope issue and is explicitly deferred by the spec's Open Questions section. This is not a gap — it is a deliberate phasing decision. | |
|
|
||
| - **DNS configuration and port-forwarding:** Explicit DNS configuration and port-forwarding rules via gvproxy are flagged as future sub-items in the spec's Non-Goals section. The guest relies on gvproxy's built-in DNS resolver (resolves from the host's system resolver) for this spec. | ||
|
|
||
| - **Linux namespace sandbox path:** Whether `sandbox2` (the namespace-based isolation path for Linux hosts without `/dev/kvm`) shares the gvproxy integration code is an open question flagged by the spec. Out of scope for this architecture; this spec touches only the libkrun VMM path. | ||
|
|
||
| ## Verification | ||
|
|
||
| - `cargo test -p minvmd` passes on macOS and Linux after landing, with no regressions in existing tests. | ||
| - `MINVMD_E2E=1 cargo test -p minvmd --test minimald_session_e2e -- --include-ignored` passes on macOS with gvproxy on PATH. | ||
| - `MINVMD_NETMODE=tsi MINVMD_E2E=1 cargo test -p minvmd --test minimald_session_e2e -- --include-ignored` passes (TSI fallback unchanged). | ||
| - The architecture sub-issue is created as a child of tracking issue minimal#404. | ||
| - This architecture PR references minimal#404 but does not use a closing keyword (the tracking issue must stay open). | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.