8000 feat(release): ship the switch binary to Linux installs, as `gvproxy-min` by norrietaylor · Pull Request #994 · 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
3 changes: 3 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions crates/minimal/src/diag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const PROCESS_MARKERS: &[&str] = &[
"minimald",
"minvmd",
"__krun-vmm",
// Both names: `gvproxy-min` is what an install stamps into `bin/` (see
// `switch::GVPROXY_FILE`); a dev checkout and a system package still run
// the binary under its upstream name.
"gvproxy-min",
"gvproxy",
];

Expand Down
6 changes: 4 additions & 2 deletions crates/minimald/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,10 @@ pub struct ListenArgs {
detach: bool,

/// Path to the gvproxy ("gvisor-tap-vsock") binary backing the per-host
/// `OwnIp` switch. Defaults to the fixed system install path when unset;
/// point it at a local build to run own-IP without a system install.
/// `OwnIp` switch. Defaults to the installed location when unset: the
/// user-local `bin/gvproxy-min` the installer stamps, else the system
/// install path. Point it at a local build to run own-IP without an
/// install.
#[arg(long)]
gvproxy_bin: Option<std::path::PathBuf>,
}
Expand Down
16 changes: 7 additions & 9 deletions crates/minimald/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,14 @@ pub enum HostKey {
},
}

/// Default install path for the gvproxy ("gvisor-tap-vsock") switch binary,
/// used when [`Config::gvproxy_bin`] is unset. The `GVPROXY_BIN` env var is
/// scoped to the `#[ignore]` netns proof and is never consulted by the daemon.
const DEFAULT_GVPROXY_BIN: &str = "/usr/lib/minimal/bin/gvproxy";

/// Global Configuration for the minimald server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config {
pub host_key: HostKey,
pub minimal_state_dir: DaemonAbsPath,
pub minimal_cache_dir: DaemonAbsPath,
/// Path to the gvproxy binary backing the per-host `OwnIp` switch. Defaults
/// to [`DEFAULT_GVPROXY_BIN`] when unset.
/// to the installed location (`switch::installed_gvproxy_bin`) when unset.
#[serde(default)]
pub gvproxy_bin: Option<PathBuf>,
/// Whether this `minimald` runs inside a `minvmd` libkrun VM (DM1/3/4). When
Expand All @@ -61,12 +56,15 @@ pub struct Config {
}

impl Config {
/// Resolves the configured gvproxy binary path, falling back to the fixed
/// install path when unset.
/// Resolves the configured gvproxy binary path, falling back to the
/// installed loc 8000 ation when unset: the user-local `bin/gvproxy-min` the
/// curl|sh installer stamps, else the system-wide path. The `GVPROXY_BIN`
/// env var is scoped to the `#[ignore]` netns proof and is never consulted
/// by the daemon.
fn gvproxy_bin_path(&self) -> PathBuf {
self.gvproxy_bin
.clone()
.unwrap_or_else(|| PathBuf::from(DEFAULT_GVPROXY_BIN))
.unwrap_or_else(::switch::installed_gvproxy_bin)
}

/// Returns the SSH host key to use.
Expand Down
61 changes: 20 additions & 41 deletions crates/minvmd/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,54 +113,24 @@ pub fn resolve_initramfs_path() -> Result<PathBuf, VmError> {
resolve_or_default("MINVMD_INITRAMFS", DEFAULT_INITRAMFS_FILE)
}

/// System-wide install path for the host gvproxy ("gvisor-tap-vsock") binary,
/// used as the final fallback when `MINVMD_GVPROXY_BIN` is unset and no
/// user-local install exists. Fetched + SHA-256-verified by
/// `scripts/fetch-gvproxy.sh` (issue #495).
pub const DEFAULT_GVPROXY_BIN: &str = "/usr/lib/minimal/bin/gvproxy";

/// Filename the installer stamps into the `bin/` prefix (see
/// `scripts/stage-release.sh`: `bin/gvproxy`).
const GVPROXY_FILE: &str = "gvproxy";

/// The user-local bin directory the installer stamps into: `$MINIMAL_BIN`, else
/// `$HOME/.local/bin`. Mirrors the installer's `bin` prefix resolution in
/// `scripts/install.sh`. `None` when neither is set.
fn installer_bin_dir() -> Option<PathBuf> {
if let Some(bin) = std::env::var("MINIMAL_BIN").ok().filter(|v| !v.is_empty()) {
return Some(PathBuf::from(bin));
}
std::env::var("HOME")
.ok()
.filter(|v| !v.is_empty())
.map(|home| PathBuf::from(home).join(".local/bin"))
}

/// Resolve the host gvproxy binary path in three tiers: the `MINVMD_GVPROXY_BIN`
/// override, then an existing user-local install (`<bin-dir>/gvproxy`, where the
/// installer stamps it), then the system-wide [`DEFAULT_GVPROXY_BIN`].
/// Resolve the host gvproxy binary path: the `MINVMD_GVPROXY_BIN` override,
/// then the installed location — the user-local `bin/gvproxy-min` the curl|sh
/// installer stamps, else a system-wide path ([`switch::installed_gvproxy_bin`],
/// the definition shared with minimald, which also honours the pre-rename
/// `/usr/lib/minimal/bin/gvproxy` when only that exists).
///
/// Unlike the kernel/rootfs/initramfs resolvers this never errors: gvproxy is
/// only spawned for an own-IP VM, so an absent override is the common case, and
/// the system path is returned as a last resort even if nothing exists there
/// (the caller surfaces a spawn failure with that concrete path).
#[must_use]
pub fn resolve_gvproxy_path() -> PathBuf {
// Tier 1: explicit override, honoured verbatim.
if let Some(val) = std::env::var("MINVMD_GVPROXY_BIN")
// Explicit override, honoured verbatim.
std::env::var("MINVMD_GVPROXY_BIN")
.ok()
.filter(|v| !v.is_empty())
{
return PathBuf::from(val);
}
// Tier 2: user-local install from the curl|sh installer, if present.
if let Some(local) = installer_bin_dir().map(|dir| dir.join(GVPROXY_FILE))
&& local.exists()
{
return local;
}
// Tier 3: system-wide install path (hardcoded fallback).
PathBuf::from(DEFAULT_GVPROXY_BIN)
.map(PathBuf::from)
.unwrap_or_else(switch::installed_gvproxy_bin)
}

#[cfg(test)]
Expand Down Expand Up @@ -274,7 +244,7 @@ mod tests {
let _g = ENV_LOCK.lock().unwrap();
clear_gvproxy_env();
let tmp = tempfile::tempdir().unwrap();
let want = tmp.path().join(GVPROXY_FILE);
let want = tmp.path().join(switch::GVPROXY_FILE);
std::fs::write(&want, b"gvproxy").unwrap();
// MINIMAL_BIN steers installer_bin_dir without touching HOME.
unsafe { std::env::set_var("MINIMAL_BIN", tmp.path()) };
Expand All @@ -289,7 +259,16 @@ mod tests {
// MINIMAL_BIN points at an empty dir: no user-local gvproxy present.
let tmp = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("MINIMAL_BIN", tmp.path()) };
assert_eq!(resolve_gvproxy_path(), PathBuf::from(DEFAULT_GVPROXY_BIN));
// One of the two system paths — the shared resolver prefers the legacy
// one on a host that still has it, so asserting a single path here
// would fail on exactly the pre-rename hosts that branch serves.
// `switch`'s own tests pin which branch wins; this one only proves the
// tier-2 miss falls through to a system path.
let got = resolve_gvproxy_path();
assert!(
got.starts_with("/usr/lib/minimal/bin"),
"expected a system-wide switch path, got {got:?}"
);
clear_gvproxy_env();
}

Expand Down
3 changes: 3 additions & 0 deletions crates/switch/Cargo.toml
10000
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ publish.workspace = true
license.workspace = true

[dependencies]

[dev-dependencies]
tempfile.workspace = true
165 changes: 165 additions & 0 deletions crates/switch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use std::fmt;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};

/// MTU advertised to the switch and the tap devices. gvproxy's own default.
pub const DEFAULT_MTU: u16 = 1500;
Expand All @@ -34,6 +35,82 @@ pub const DEFAULT_SUBNET: SwitchSubnet = SwitchSubnet {
prefix: 16,
};

/// Filename the switch binary is installed under, in both the user-local `bin/`
/// prefix and the system-wide path below.
///
/// Deliberately **not** plain `gvproxy`: the installer's `bin` prefix is
/// `~/.local/bin`, which is on `PATH`, and podman/crc/Docker Desktop all ship
/// their own `gvproxy` there. Installing under the upstream name would let
/// whichever came last win a `PATH` lookup — in either direction — so the
/// binary carries a minimal-specific name even though the bytes are stock
/// gvproxy (`scripts/fetch-gvproxy.sh` pins and SHA-256-verifies them).
pub const GVPROXY_FILE: &str = "gvproxy-min";

/// System-wide install path for the switch binary: the final fallback when no
/// user-local install exists.
pub const DEFAULT_GVPROXY_BIN: &str = "/usr/lib/minimal/bin/gvproxy-min";

/// The pre-rename system-wide path, still honoured when it exists and the
/// current one does not.
///
/// This directory is not on `PATH`, so the collision that motivates
/// [`GVPROXY_FILE`] never applied here — but an operator who provisioned the
/// old path has no install record for us to migrate (`install.sh` only walks
/// user-local rows), so renaming without this fallback would silently break
/// own-IP on their hosts.
const LEGACY_SYSTEM_GVPROXY_BIN: &str = "/usr/lib/minimal/bin/gvproxy";

/// The user-local bin directory the installer stamps into: `$MINIMAL_BIN`, else
/// `$HOME/.local/bin`. Mirrors the installer's `bin` prefix resolution in
/// `scripts/install.sh`. `None` when neither is set.
fn installer_bin_dir() -> Option<PathBuf> {
if let Some(bin) = std::env::var("MINIMAL_BIN").ok().filter(|v| !v.is_empty()) {
return Some(PathBuf::from(bin));
}
std::env::var("HOME")
.ok()
.filter(|v| !v.is_empty())
.map(|home| PathBuf::from(home).join(".local/bin"))
}

/// Resolve the switch binary an install placed on this host: an existing
/// user-local install (`<bin-dir>/gvproxy-min`, where the curl|sh installer
/// stamps it), else the system-wide [`DEFAULT_GVPROXY_BIN`].
///
/// One definition shared by `minimald` (which spawns the switch for native
/// own-IP sessions) and `minvmd` (the host switch supervisor), so the two
/// daemons and the installer cannot drift on where the binary lives;
/// daemon-specific overrides (config field, env var) layer on top in the
/// callers. Never errors: the system path is returned as a last resort even if
/// nothing exists there, so the caller surfaces a spawn failure naming a
/// concrete path.
#[must_use]
pub fn installed_gvproxy_bin() -> PathBuf {
resolve_installed(
installer_bin_dir(),
Path::new(DEFAULT_GVPROXY_BIN),
Path::new(LEGACY_SYSTEM_GVPROXY_BIN),
)
}

/// The resolution itself, with every probed location passed in.
///
/// Split out so the tests can drive both system-path branches against temp< 10BC8 /td>
/// dirs. Probing the real `/usr/lib/minimal/bin` instead would make them depend
/// on host install state — and fail on exactly the pre-rename hosts the legacy
/// branch exists to serve.
fn resolve_installed(bin_dir: Option<PathBuf>, system: &Path, legacy: &Path) -> PathBuf {
if let Some(local) = bin_dir.map(|dir| dir.join(GVPROXY_FILE))
&& local.exists()
{
return local;
}
if !system.exists() && legacy.exists() {
return legacy.to_path_buf();
}
system.to_path_buf()
}

/// A subnet was constructed with a prefix outside the supported `8..=29` range —
/// a configuration error, distinct from runtime address exhaustion.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -275,4 +352,92 @@ mod tests {
assert!(yaml.contains("dhcpStaticLeases:\n {}\n"));
assert!(yaml.contains("subnet: \"100.64.0.0/16\""));
}

// Resolver tests mutate process-global env (MINIMAL_BIN), so serialise
// them to avoid races when `cargo test` runs them in parallel.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

#[test]
fn installed_name_is_not_the_upstream_one() {
// The whole point of the rename: a stock `gvproxy` on PATH (podman,
// crc) must not be mistaken for ours, nor ours for it.
assert_ne!(GVPROXY_FILE, "gvproxy");
assert!(DEFAULT_GVPROXY_BIN.ends_with(GVPROXY_FILE));
}

#[test]
fn installed_gvproxy_prefers_existing_user_local_install() {
let _g = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
let want = tmp.path().join(GVPROXY_FILE);
std::fs::write(&want, b"gvproxy").unwrap();
unsafe { std::env::set_var("MINIMAL_BIN", tmp.path()) };
assert_eq!(installed_gvproxy_bin(), want);
unsafe { std::env::remove_var("MINIMAL_BIN") };
}

#[test]
fn ignores_an_upstream_named_binary_in_the_bin_dir() {
// A foreign `gvproxy` (podman's, crc's) sitting in the bin dir must not
// be picked up — only our own filename counts. Driven through
// `resolve_installed` so the assertion does not depend on what the host
// has under /usr/lib/minimal/bin.
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir(&bin).unwrap();
std::fs::write(bin.join("gvproxy"), b"podman's").unwrap();
let system = tmp.path().join("gvproxy-min");
let legacy = tmp.path().join("gvproxy");
assert_eq!(resolve_installed(Some(bin), &system, &legacy), system);
}

// The system-path branches go through `resolve_installed` with temp paths.
// Calling the public resolver would probe the real /usr/lib/minimal/bin and
// flip on a host that has either binary installed — failing on precisely
// the pre-rename hosts the legacy branch exists to serve.

#[test]
fn falls_back_to_the_current_system_path() {
let tmp = tempfile::tempdir().unwrap();
let system = tmp.path().join("gvproxy-min");
let legacy = tmp.path().join("gvproxy");
std::fs::write(&system, b"current").unwrap();
std::fs::write(&legacy, b"legacy").unwrap();
// Both present: the current name wins.
assert_eq!(resolve_installed(None, &system, &legacy), system);
}

#[test]
fn falls_back_to_the_legacy_system_path_when_only_it_exists() {
let tmp = tempfile::tempdir().unwrap();
let system = tmp.path().join("gvproxy-min");
let legacy = tmp.path().join("gvproxy");
std::fs::write(&legacy, b"legacy").unwrap();
// An operator who provisioned the pre-rename path keeps working.
assert_eq!(resolve_installed(None, &system, &legacy), legacy);
}

#[test]
fn reports_the_current_system_path_when_neither_exists() {
let tmp = tempfile::tempdir().unwrap();
let system = tmp.path().join("gvproxy-min");
let legacy = tmp.path().join("gvproxy");
// Nothing installed anywhere: name the current path, so the caller's
// spawn failure cites the location an install should have used.
assert_eq!(resolve_installed(None, &system, &legacy), system);
}

#[test]
fn user_local_install_beats_both_system_paths() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir(&bin).unwrap();
let want = bin.join(GVPROXY_FILE);
std::fs::write(&want, b"user-local").unwrap();
let system = tmp.path().join("gvproxy-min");
let legacy = tmp.path().join("gvproxy");
std::fs::write(&system, b"current").unwrap();
std::fs::write(&legacy, b"legacy").unwrap();
assert_eq!(resolve_installed(Some(bin), &system, &legacy), want);
}
}
2 changes: 1 addition & 1 deletion docs/reference/cli-minimald.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Runs the minimald server in the foreground.
| `--instance-num <N>` | Instance number for this minimald; determines client-relevant paths under `<minimal_state_dir>/providers/local-minimald<N>`. The SSH socket is accessible as `ssh.sock`. Default: `0` |
| `--vsock` | Host the SSH socket over vsock instead of UDS; the vsock port is the default port base plus `instance_num` |
| `--detach` | Daemonize: spawn minimald in a new session (setsid) and return once the SSH socket accepts connections, or an 8s timeout elapses. Used by the `min` CLI to auto-start a native daemon on Linux |
| `--gvproxy-bin <PATH>` | Path to the gvproxy ("gvisor-tap-vsock") binary used for networking. Defaults to a typical system install path. |
| `--gvproxy-bin <PATH>` | Path to the gvproxy ("gvisor-tap-vsock") binary used for networking. Defaults to the installed location: the user-local `bin/gvproxy-min` the installer stamps, else the system install path. |

### `completions`

Expand Down
Loading
0