Repository navigation
feat(minvmd): add net module with NetworkMode and gvproxy spawn #463
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
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9a85e84
feat(minvmd): add net module with NetworkMode and gvproxy spawn (R1.1…
gominimal-aw-bot[bot] 6e1e7fc
fix(minvmd): wrap env-var mutation in unsafe in net tests
norrietaylor ab7eca9
fix(minimald): restore handshake-failure resilience and guest PATH
norrietaylor 158ff5b
test(minvmd): use a single global lock for env-mutating net tests
norrietaylor 18ba4d4
refactor(minvmd,gvproxy): extract shared gvproxy spawn helper into a …
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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,9 @@ | ||
| [package] | ||
| name = "gvproxy" | ||
| version = "0.0.1" | ||
| edition.workspace = true | ||
| publish.workspace = true | ||
|
|
||
| [dependencies] | ||
| anyhow.workspace = true | ||
| tracing.workspace = true |
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,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. | ||
| < 8000 td class="blob-code blob-code-addition js-file-line"> //! - **Linux per-sandbox path** (`sandbox2`): `<n>` is the tap fd from | ||
| //! 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}" | ||
| ); | ||
| } | ||
| } |
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
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
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,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")); | ||
| } | ||
| } |
Oops, something went wrong.
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.