8000 feat(minvmd): add net module with NetworkMode and gvproxy spawn by gominimal-aw-bot[bot] · Pull Request #463 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"crates/common",
"crates/decode",
"crates/graph",
"crates/gvproxy",
"crates/mctx",
"crates/mfile",
"crates/op",
Expand Down Expand Up @@ -132,6 +133,7 @@ petgraph = "0.8"
# our crates
args = { version = "0.0.1", path = "crates/args" }
graph = { version = "0.0.1", path = "crates/graph" }
gvproxy = { version = "0.0.1", path = "crates/gvproxy" }
lcache = { version = "0.0.1", path = "crates/lcache" }
rcache = { version = "0.0.1", path = "crates/rcache" }
check = { version = "0.0.1", path = "crates/check" }
Expand Down
9 changes: 9 additions & 0 deletions crates/gvproxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "gvproxy"
version = "0.0.1"
edition.workspace = true
publish.workspace = true

[dependencies]
anyhow.workspace = true
tracing.workspace = true
106 changes: 106 additions & 0 deletions crates/gvproxy/src/lib.rs
< 8000 td class="blob-code blob-code-addition js-file-line"> //! - **Linux per-sandbox path** (`sandbox2`): `<n>` is the tap fd from
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Transport-agnostic gvproxy process management.
//!
//! gvproxy (containers/gvisor-tap-vsock) is a userspace network stack. Both
//! network paths in this workspace spawn it the same way — `gvproxy --fd <n>`,
//! where `<n>` is the file descriptor gvproxy reads and writes Ethernet frames
//! on — but they source that fd differently:
//!
//! - **macOS VM path** (`minvmd`): `<n>` is one end of a `socketpair`; the other
//! end is handed to libkrun via `krun_set_passt_fd`. One gvproxy per VM.
//! hakoniwa's `Child::rustslirp_tapfd` after configuring the container with
//! `Network::RustSlirp`. One gvproxy per sandbox.
//!
//! This crate owns only the parts that are identical across both: locating the
//! binary and spawning it with the fd. The fd handoff and lifecycle ownership
//! stay with each caller.

use std::os::unix::io::RawFd;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};

/// Resolve the gvproxy binary path.
///
/// Returns the value of the `env_override` environment variable if set;
/// otherwise returns `"gvproxy"` (resolved via `PATH` by the OS at spawn).
/// `env_override` lets each caller keep its own override variable name
/// (e.g. `MINVMD_GVPROXY_PATH`).
#[must_use]
pub fn gvproxy_bin(env_override: &str) -> PathBuf {
match std::env::var(env_override) {
Ok(path) => {
tracing::debug!(var = %env_override, path = %path, "using gvproxy path override");
PathBuf::from(path)
}
Err(_) => {
tracing::debug!(var = %env_override, "gvproxy path override unset, searching PATH");
PathBuf::from("gvproxy")
}
}
}

/// Spawn the gvproxy child process.
///
/// Spawns `bin` with the argument `--fd <net_fd>`. Sets `stdin`, `stdout`, and
/// `stderr` to `Stdio::null()` (gvproxy logs over the fd, not stdio).
///
/// The caller is responsible for ensuring `net_fd` does **not** have
/// `FD_CLOEXEC` set before this call so it survives the spawn, and for reaping
/// the returned child.
///
/// Returns `Err` with a clear message when the binary is not found.
#[must_use = "the spawned child must be reaped"]
pub fn spawn_gvproxy(bin: impl AsRef<Path>, net_fd: RawFd) -> anyhow::Result<Child> {
let bin = bin.as_ref();

Command::new(bin)
.arg("--fd")
.arg(net_fd.to_string())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| anyhow::anyhow!("failed to spawn gvproxy from {}: {e}", bin.display()))
}

#[cfg(test)]
mod tests {
use std::sync::Mutex;

use super::*;

// Serialise every test that mutates the environment behind a single lock.
// `set_var`/`remove_var` mutate process-global state (and are `unsafe` on
// the 2024 edition), so distinct per-variable locks would still let two
// tests touch the environment concurrently. One lock covers all of them.
static ENV_LOCK: Mutex<()> = Mutex::new(());

#[test]
fn gvproxy_bin_from_env() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var("GVPROXY_TEST_PATH", "/custom/path/to/gvproxy") };
assert_eq!(
gvproxy_bin("GVPROXY_TEST_PATH"),
PathBuf::from("/custom/path/to/gvproxy")
);
unsafe { std::env::remove_var("GVPROXY_TEST_PATH") };
}

#[test]
fn gvproxy_bin_unset_defaults_to_gvproxy() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::remove_var("GVPROXY_TEST_PATH") };
assert_eq!(gvproxy_bin("GVPROXY_TEST_PATH"), PathBuf::from("gvproxy"));
}

#[test]
fn spawn_gvproxy_returns_error_when_binary_not_found() {
let result = spawn_gvproxy("/nonexistent/gvproxy", 3);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("failed to spawn gvproxy"),
"error message: {err_msg}"
);
}
}
1 change: 1 addition & 0 deletions crates/minvmd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ clap.workspace = true
clap_complete.workspace = true
dirs.workspace = true
fd-lock.workspace = true
gvproxy.workspace = true
libc.workspace = true
serde = { workspace = true }
serde_json.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/minvmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod cmd;
pub mod error;
pub mod image;
pub mod lifecycle;
pub mod net;
Comment thread
norrietaylor marked this conversation as resolved.
Resolved
8BD0
pub mod sock;
pub mod state;
pub mod vm;
Expand Down
123 changes: 123 additions & 0 deletions crates/minvmd/src/net.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Network mode selection for `minvmd`-managed VMs (R1.1, R1.2).
//!
//! Defines the `NetworkMode` type (gvproxy vs. TSI) and resolves it from the
//! `MINVMD_NETMODE` env var. TSI is a libkrun feature with no analogue on other
//! transports, so the mode selection lives here rather than in the shared
//! [`gvproxy`] crate. The transport-agnostic gvproxy spawn/lookup helpers come
//! from that crate; `gvproxy_bin` below wraps the lookup with `minvmd`'s own
//! override variable.

use std::path::PathBuf;

#[doc(inline)]
pub use gvproxy::spawn_gvproxy;

/// The two supported outbound network transport modes for `minvmd`-managed VMs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkMode {
/// Use gvproxy (containers/gvisor-tap-vsock) as the userspace network stack.
/// This is the default and supports ~200+ concurrent outbound connections.
GvProxy,
/// Use libkrun's built-in TSI (Transport Socket Interface) shim.
/// Fallback mode with a ~62-concurrent-connection cap.
Tsi,
}

/// Resolve the network mode from the `MINVMD_NETMODE` environment variable.
///
/// Returns `NetworkMode::GvProxy` when the var is unset or set to `"gvproxy"`;
/// returns `NetworkMode::Tsi` when set to `"tsi"`. Any other value logs a
/// warning and falls back to `NetworkMode::GvProxy`.
#[must_use]
pub fn resolve_net_mode() -> NetworkMode {
match std::env::var("MINVMD_NETMODE").as_deref() {
Ok("tsi") => {
tracing::debug!("MINVMD_NETMODE=tsi: using TSI network mode");
NetworkMode::Tsi
}
Ok("gvproxy") | Err(std::env::VarError::NotPresent) => {
tracing::debug!("MINVMD_NETMODE=gvproxy (default): using gvproxy network mode");
NetworkMode::GvProxy
}
Ok(other) => {
tracing::warn!(
value = %other,
"MINVMD_NETMODE has unknown value, falling back to gvproxy"
);
NetworkMode::GvProxy
}
Err(std::env::VarError::NotUnicode(_)) => {
tracing::warn!("MINVMD_NETMODE is not valid UTF-8, falling back to gvproxy");
NetworkMode::GvProxy
}
}
}

/// Resolve the path to the gvproxy binary from `MINVMD_GVPROXY_PATH`.
///
/// Defaults to `"gvproxy"` (resolved via `PATH` at spawn) when the variable is
/// unset. Thin wrapper over [`gvproxy::gvproxy_bin`] that binds `minvmd`'s
/// override variable name.
#[must_use]
pub fn gvproxy_bin() -> PathBuf {
gvproxy::gvproxy_bin("MINVMD_GVPROXY_PATH")
}

#[cfg(test)]
mod tests {
use std::sync::Mutex;

use super::*;

// Serialise every test that mutates the environment behind a single lock.
// `set_var`/`remove_var` mutate process-global state (and are `unsafe` on
// the 2024 edition), so distinct per-variable locks would still let two
// tests touch the environment concurrently. One lock covers all of them.
static ENV_LOCK: Mutex<()> = Mutex::new(());

#[test]
fn resolve_net_mode_unset_defaults_to_gvproxy() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::remove_var("MINVMD_NETMODE") };
assert_eq!(resolve_net_mode(), NetworkMode::GvProxy);
}

#[test]
fn resolve_net_mode_gvproxy() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var("MINVMD_NETMODE", "gvproxy") };
assert_eq!(resolve_net_mode(), NetworkMode::GvProxy);
unsafe { std::env::remove_var("MINVMD_NETMODE") };
}

#[test]
fn resolve_net_mode_tsi() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var("MINVMD_NETMODE", "tsi") };
assert_eq!(resolve_net_mode(), NetworkMode::Tsi);
unsafe { std::env::remove_var("MINVMD_NETMODE") };
}

#[test]
fn resolve_net_mode_invalid_falls_back_to_gvproxy() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var("MINVMD_NETMODE", "invalid") };
assert_eq!(resolve_net_mode(), NetworkMode::GvProxy);
unsafe { std::env::remove_var("MINVMD_NETMODE") };
}

#[test]
fn gvproxy_bin_from_env() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var("MINVMD_GVPROXY_PATH", "/custom/path/to/gvproxy") };
assert_eq!(gvproxy_bin(), PathBuf::from("/custom/path/to/gvproxy"));
unsafe { std::env::remove_var("MINVMD_GVPROXY_PATH") };
}

#[test]
fn gvproxy_bin_unset_defaults_to_gvproxy() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::remove_var("MINVMD_GVPROXY_PATH") };
assert_eq!(gvproxy_bin(), PathBuf::from("gvproxy"));
}
}
Loading
Loading
0