feat(windows): stub unsupported compute drivers

Signed-off-by: Shailendra Singh <shailendras@nvidia.com>
Signed-off-by: Akber Raza <akberr@nvidia.com>
This commit is contained in:
Shailendra Singh
2026-08-05 20:15:44 -05:00
committed by Akber Raza
parent 34a0323b2e
commit b17118ef14
22 changed files with 5008 additions and 1004 deletions
+2 -1
View File
@@ -12,14 +12,15 @@ repository.workspace = true
[dependencies]
openshell-core = { path = "../openshell-core", default-features = false }
serde = { workspace = true }
[target.'cfg(not(target_os = "windows"))'.dependencies]
tokio = { workspace = true }
tonic = { workspace = true }
futures = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
bytes = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
prost-types = { workspace = true }
bollard = { version = "0.20" }
+4 -2
View File
@@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Docker compute driver.
#[cfg(target_os = "windows")]
mod windows;
#![allow(clippy::result_large_err)]
#[cfg(target_os = "windows")]
pub use windows::{DockerComputeConfig, default_docker_supervisor_image};
use bollard::Docker;
use bollard::errors::Error as BollardError;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use openshell_core::config::DEFAULT_DOCKER_NETWORK_NAME;
use std::path::PathBuf;
const DEFAULT_DOCKER_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/supervisor";
const DEFAULT_SANDBOX_PIDS_LIMIT: i64 = 2048;
#[must_use]
pub fn default_docker_supervisor_image() -> String {
format!(
"{DEFAULT_DOCKER_SUPERVISOR_IMAGE_REPO}:{}",
default_docker_supervisor_image_tag()
)
}
fn default_docker_supervisor_image_tag() -> String {
let tag = option_env!("OPENSHELL_IMAGE_TAG")
.filter(|tag| !tag.is_empty())
.or_else(|| option_env!("IMAGE_TAG").filter(|tag| !tag.is_empty()))
.unwrap_or_else(|| {
if env!("CARGO_PKG_VERSION").is_empty() || env!("CARGO_PKG_VERSION") == "0.0.0" {
"dev"
} else {
env!("CARGO_PKG_VERSION")
}
});
tag.replace('+', "-")
}
/// Gateway-local configuration for the Docker compute driver.
///
/// Windows builds keep this type so existing config files continue to parse,
/// but Docker runtime support is intentionally unavailable on Windows.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct DockerComputeConfig {
pub socket_path: Option<PathBuf>,
pub default_image: String,
pub image_pull_policy: String,
pub sandbox_namespace: String,
pub grpc_endpoint: String,
pub supervisor_bin: Option<PathBuf>,
pub supervisor_image: Option<String>,
pub guest_tls_ca: Option<PathBuf>,
pub guest_tls_cert: Option<PathBuf>,
pub guest_tls_key: Option<PathBuf>,
pub network_name: String,
pub host_gateway_ip: String,
pub ssh_socket_path: String,
pub sandbox_pids_limit: i64,
pub enable_bind_mounts: bool,
}
impl Default for DockerComputeConfig {
fn default() -> Self {
Self {
socket_path: None,
default_image: openshell_core::image::default_sandbox_image(),
image_pull_policy: String::new(),
sandbox_namespace: "default".to_string(),
grpc_endpoint: String::new(),
supervisor_bin: None,
supervisor_image: None,
guest_tls_ca: None,
guest_tls_cert: None,
guest_tls_key: None,
network_name: DEFAULT_DOCKER_NETWORK_NAME.to_string(),
host_gateway_ip: String::new(),
ssh_socket_path: "/run/openshell/ssh.sock".to_string(),
sandbox_pids_limit: DEFAULT_SANDBOX_PIDS_LIMIT,
enable_bind_mounts: false,
}
}
}
@@ -17,7 +17,9 @@ path = "src/main.rs"
[dependencies]
openshell-core = { path = "../openshell-core", default-features = false }
openshell-policy = { path = "../openshell-policy" }
serde = { workspace = true }
[target.'cfg(not(target_os = "windows"))'.dependencies]
tokio = { workspace = true }
tonic = { workspace = true, features = ["transport"] }
prost = { workspace = true }
@@ -27,7 +29,6 @@ tokio-stream = { workspace = true }
kube = { workspace = true }
kube-runtime = { workspace = true }
k8s-openapi = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
clap = { workspace = true }
tracing = { workspace = true }
@@ -37,6 +38,7 @@ miette = { workspace = true }
[dev-dependencies]
temp-env = "0.3"
serde_json = { workspace = true }
[lints]
workspace = true
@@ -2,7 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
pub mod config;
#[cfg(not(target_os = "windows"))]
pub mod driver;
#[cfg(not(target_os = "windows"))]
pub mod grpc;
pub use config::{
@@ -10,5 +13,8 @@ pub use config::{
DEFAULT_WORKSPACE_STORAGE_SIZE, KubernetesComputeConfig, KubernetesSidecarConfig,
SupervisorSideloadMethod, SupervisorTopology,
};
#[cfg(not(target_os = "windows"))]
pub use driver::{KubernetesComputeDriver, KubernetesDriverError};
#[cfg(not(target_os = "windows"))]
pub use grpc::ComputeDriverService;
@@ -1,11 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use clap::{ArgAction, Parser};
use miette::{IntoDiagnostic, Result};
use std::net::SocketAddr;
use tracing::info;
use tracing_subscriber::EnvFilter;
#[cfg(not(target_os = "windows"))]
include!("main_unix.rs");
use openshell_core::VERSION;
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
@@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use clap::{ArgAction, Parser};
use miette::{IntoDiagnostic, Result};
use std::net::SocketAddr;
use tracing::info;
use tracing_subscriber::EnvFilter;
use openshell_core::VERSION;
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
use openshell_driver_kubernetes::{
AppArmorProfile, ComputeDriverService, DEFAULT_PROXY_UID, DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME,
KubernetesComputeConfig, KubernetesComputeDriver, KubernetesSidecarConfig,
SupervisorSideloadMethod, SupervisorTopology,
};
#[derive(Parser, Debug)]
#[command(name = "openshell-driver-kubernetes")]
#[command(version = VERSION)]
struct Args {
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_BIND",
default_value = "127.0.0.1:50061"
)]
bind_address: SocketAddr,
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
log_level: String,
#[arg(long, env = "OPENSHELL_SANDBOX_NAMESPACE", default_value = "default")]
sandbox_namespace: String,
#[arg(
long,
env = "OPENSHELL_K8S_SANDBOX_SERVICE_ACCOUNT",
default_value = DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME
)]
sandbox_service_account: String,
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
sandbox_image: Option<String>,
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY")]
sandbox_image_pull_policy: Option<String>,
#[arg(
long,
env = "OPENSHELL_SANDBOX_IMAGE_PULL_SECRETS",
value_delimiter = ','
)]
sandbox_image_pull_secrets: Vec<String>,
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
grpc_endpoint: Option<String>,
#[arg(
long,
env = "OPENSHELL_SANDBOX_SSH_SOCKET_PATH",
default_value = "/run/openshell/ssh.sock"
)]
sandbox_ssh_socket_path: String,
#[arg(long, env = "OPENSHELL_CLIENT_TLS_SECRET_NAME")]
client_tls_secret_name: Option<String>,
#[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")]
host_gateway_ip: Option<String>,
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
supervisor_image: Option<String>,
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY")]
supervisor_image_pull_policy: Option<String>,
#[arg(
long,
env = "OPENSHELL_SUPERVISOR_SIDELOAD_METHOD",
default_value = "image-volume"
)]
supervisor_sideload_method: SupervisorSideloadMethod,
#[arg(long, env = "OPENSHELL_K8S_TOPOLOGY", default_value = "combined")]
topology: SupervisorTopology,
#[arg(
long = "sidecar-proxy-uid",
alias = "proxy-uid",
env = "OPENSHELL_K8S_SIDECAR_PROXY_UID",
default_value_t = DEFAULT_PROXY_UID
)]
sidecar_proxy_uid: u32,
#[arg(
long = "sidecar-process-binary-aware-network-policy",
env = "OPENSHELL_K8S_SIDECAR_PROCESS_BINARY_AWARE_NETWORK_POLICY",
default_value_t = true,
action = ArgAction::Set
)]
sidecar_process_binary_aware_network_policy: bool,
#[arg(long, env = "OPENSHELL_ENABLE_USER_NAMESPACES")]
enable_user_namespaces: bool,
#[arg(long, env = "OPENSHELL_K8S_APP_ARMOR_PROFILE")]
app_armor_profile: Option<AppArmorProfile>,
/// Lifetime (seconds) of the projected `ServiceAccount` token
/// kubelet writes into each sandbox pod for the `IssueSandboxToken`
/// bootstrap exchange. Kubelet enforces a minimum of 600s; the
/// gateway clamps values outside `[600, 86400]`. Default 3600.
#[arg(long, env = "OPENSHELL_K8S_SA_TOKEN_TTL_SECS", default_value_t = 3600)]
sa_token_ttl_secs: i64,
#[arg(long, env = "OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET")]
provider_spiffe_workload_api_socket_path: Option<String>,
#[arg(long, env = "OPENSHELL_K8S_SANDBOX_UID")]
sandbox_uid: Option<u32>,
#[arg(long, env = "OPENSHELL_K8S_SANDBOX_GID")]
sandbox_gid: Option<u32>,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)),
)
.init();
let driver = KubernetesComputeDriver::new(KubernetesComputeConfig {
namespace: args.sandbox_namespace,
service_account_name: args.sandbox_service_account,
default_image: args.sandbox_image.unwrap_or_default(),
image_pull_policy: args.sandbox_image_pull_policy.unwrap_or_default(),
image_pull_secrets: args.sandbox_image_pull_secrets,
supervisor_image: args
.supervisor_image
.unwrap_or_else(openshell_core::config::default_supervisor_image),
supervisor_image_pull_policy: args.supervisor_image_pull_policy.unwrap_or_default(),
supervisor_sideload_method: args.supervisor_sideload_method,
topology: args.topology,
sidecar: KubernetesSidecarConfig {
proxy_uid: args.sidecar_proxy_uid,
process_binary_aware_network_policy: args.sidecar_process_binary_aware_network_policy,
},
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
ssh_socket_path: args.sandbox_ssh_socket_path,
client_tls_secret_name: args.client_tls_secret_name.unwrap_or_default(),
host_gateway_ip: args.host_gateway_ip.unwrap_or_default(),
enable_user_namespaces: args.enable_user_namespaces,
app_armor_profile: args.app_armor_profile,
workspace_default_storage_size: std::env::var(
"OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE",
)
.unwrap_or_else(|_| {
openshell_driver_kubernetes::DEFAULT_WORKSPACE_STORAGE_SIZE.to_string()
}),
default_runtime_class_name: std::env::var("OPENSHELL_K8S_DEFAULT_RUNTIME_CLASS_NAME")
.unwrap_or_default(),
sa_token_ttl_secs: args.sa_token_ttl_secs,
provider_spiffe_workload_api_socket_path: args
.provider_spiffe_workload_api_socket_path
.unwrap_or_default(),
sandbox_uid: args.sandbox_uid,
sandbox_gid: args.sandbox_gid,
})
.await
.into_diagnostic()?;
info!(address = %args.bind_address, "Starting Kubernetes compute driver");
tonic::transport::Server::builder()
.add_service(ComputeDriverServer::new(ComputeDriverService::new(driver)))
.serve(args.bind_address)
.await
.into_diagnostic()
}
+4 -1
View File
@@ -14,7 +14,10 @@ repository.workspace = true
name = "openshell-driver-podman"
path = "src/main.rs"
[dependencies]
[target.'cfg(target_os = "windows")'.dependencies]
serde = { workspace = true }
[target.'cfg(not(target_os = "windows"))'.dependencies]
openshell-core = { path = "../openshell-core", default-features = false }
tokio = { workspace = true }
+7 -11
View File
@@ -1,15 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod client;
pub mod config;
pub(crate) mod container;
pub mod driver;
pub mod grpc;
#[cfg(test)]
pub(crate) mod test_utils;
pub(crate) mod watcher;
#[cfg(target_os = "windows")]
mod windows;
pub use config::PodmanComputeConfig;
pub use driver::PodmanComputeDriver;
pub use grpc::ComputeDriverService;
#[cfg(target_os = "windows")]
pub use windows::PodmanComputeConfig;
#[cfg(not(target_os = "windows"))]
include!("lib_unix.rs");
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod client;
pub mod config;
pub(crate) mod container;
pub mod driver;
pub mod grpc;
#[cfg(test)]
pub(crate) mod test_utils;
pub(crate) mod watcher;
pub use config::PodmanComputeConfig;
pub use driver::PodmanComputeDriver;
pub use grpc::ComputeDriverService;
+6 -180
View File
@@ -1,185 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use miette::{IntoDiagnostic, Result};
use std::net::SocketAddr;
use std::path::PathBuf;
use tracing::info;
use tracing_subscriber::EnvFilter;
use openshell_core::VERSION;
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
use openshell_driver_podman::config::{
DEFAULT_NETWORK_NAME, DEFAULT_PODMAN_STOP_TIMEOUT_SECS, DEFAULT_SANDBOX_PIDS_LIMIT,
ImagePullPolicy,
};
use openshell_driver_podman::{ComputeDriverService, PodmanComputeConfig, PodmanComputeDriver};
#[derive(Parser)]
#[command(name = "openshell-driver-podman")]
#[command(version = VERSION)]
struct Args {
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_BIND",
default_value = "127.0.0.1:50061"
)]
bind_address: SocketAddr,
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
log_level: String,
/// Path to the Podman API Unix socket.
#[arg(long, env = "OPENSHELL_PODMAN_SOCKET")]
podman_socket: Option<PathBuf>,
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
sandbox_image: Option<String>,
#[arg(
long,
env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY",
default_value_t = ImagePullPolicy::Missing
)]
sandbox_image_pull_policy: ImagePullPolicy,
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
grpc_endpoint: Option<String>,
/// Port the gateway server is listening on.
///
/// Used when `--grpc-endpoint` is not set to auto-detect the endpoint
/// that sandbox containers dial back to.
#[arg(
long,
env = "OPENSHELL_GATEWAY_PORT",
default_value_t = openshell_core::config::DEFAULT_SERVER_PORT
)]
gateway_port: u16,
/// Host gateway IP used for sandbox host aliases.
///
/// Empty uses Podman's `host-gateway` resolver.
#[arg(long, env = "OPENSHELL_PODMAN_HOST_GATEWAY_IP")]
host_gateway_ip: Option<String>,
#[arg(
long,
env = "OPENSHELL_SANDBOX_SSH_SOCKET_PATH",
default_value = openshell_core::container_paths::SSH_SOCKET_PATH
)]
sandbox_ssh_socket_path: String,
/// Podman bridge network name.
#[arg(long, env = "OPENSHELL_NETWORK_NAME", default_value = DEFAULT_NETWORK_NAME)]
network_name: String,
/// Container stop timeout in seconds (SIGTERM → SIGKILL).
#[arg(long, env = "OPENSHELL_STOP_TIMEOUT", default_value_t = DEFAULT_PODMAN_STOP_TIMEOUT_SECS)]
stop_timeout: u32,
/// Container cgroup PID limit for sandbox containers. Set 0 to inherit
/// Podman's runtime/default PID limit.
#[arg(
long,
env = "OPENSHELL_SANDBOX_PIDS_LIMIT",
default_value_t = DEFAULT_SANDBOX_PIDS_LIMIT
)]
sandbox_pids_limit: i64,
/// OCI image containing the openshell-sandbox supervisor binary.
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
supervisor_image: Option<String>,
/// Host path to the CA certificate for sandbox mTLS.
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CA")]
podman_tls_ca: Option<PathBuf>,
/// Host path to the client certificate for sandbox mTLS.
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CERT")]
podman_tls_cert: Option<PathBuf>,
/// Host path to the client private key for sandbox mTLS.
#[arg(long, env = "OPENSHELL_PODMAN_TLS_KEY")]
podman_tls_key: Option<PathBuf>,
/// Corporate forward proxy URL for the supervisor's upstream TLS dials,
/// in explicit `http://host:port` form (scheme and port required).
/// Credentials must not be embedded in the URL; use
/// `--sandbox-proxy-auth-file` instead.
#[arg(long, env = "OPENSHELL_SANDBOX_HTTPS_PROXY")]
sandbox_https_proxy: Option<String>,
/// Comma-separated `NO_PROXY` list injected alongside the proxy URL.
#[arg(long, env = "OPENSHELL_SANDBOX_NO_PROXY")]
sandbox_no_proxy: Option<String>,
/// Path to a file containing the corporate proxy credentials as
/// `user:pass`. Delivered to the supervisor through a root-only secret
/// mount so the credentials never appear in config or container metadata.
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_FILE")]
sandbox_proxy_auth_file: Option<String>,
/// Explicit acknowledgement (`true`) that the proxy credential is sent
/// as cleartext Basic auth over the plain-TCP connection to the http://
/// proxy. Required when `--sandbox-proxy-auth-file` is set.
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_ALLOW_INSECURE")]
sandbox_proxy_auth_allow_insecure: Option<bool>,
/// Send the destination hostname in CONNECT requests to the corporate
/// proxy instead of a validated IP. Only for proxies whose ACLs filter
/// on hostnames: the proxy then resolves the name itself, so sandbox
/// SSRF/`allowed_ips` validation no longer binds the connection.
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_CONNECT_BY_HOSTNAME")]
sandbox_proxy_connect_by_hostname: Option<bool>,
#[cfg(target_os = "windows")]
fn main() {
eprintln!("openshell-driver-podman is unsupported on Windows");
std::process::exit(1);
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)),
)
.init();
let driver = PodmanComputeDriver::new(PodmanComputeConfig {
socket_path: args.podman_socket,
default_image: args.sandbox_image.unwrap_or_default(),
image_pull_policy: args.sandbox_image_pull_policy,
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
gateway_port: args.gateway_port,
host_gateway_ip: args
.host_gateway_ip
.unwrap_or_else(PodmanComputeConfig::default_host_gateway_ip),
sandbox_ssh_socket_path: args.sandbox_ssh_socket_path,
network_name: args.network_name,
stop_timeout_secs: args.stop_timeout,
supervisor_image: args
.supervisor_image
.unwrap_or_else(openshell_core::config::default_supervisor_image),
guest_tls_ca: args.podman_tls_ca,
guest_tls_cert: args.podman_tls_cert,
guest_tls_key: args.podman_tls_key,
sandbox_pids_limit: args.sandbox_pids_limit,
https_proxy: args.sandbox_https_proxy,
no_proxy: args.sandbox_no_proxy,
proxy_auth_file: args.sandbox_proxy_auth_file,
proxy_auth_allow_insecure: args.sandbox_proxy_auth_allow_insecure,
proxy_connect_by_hostname: args.sandbox_proxy_connect_by_hostname,
..PodmanComputeConfig::default()
})
.await
.into_diagnostic()?;
info!(address = %args.bind_address, "Starting Podman compute driver");
tonic::transport::Server::builder()
.add_service(ComputeDriverServer::new(ComputeDriverService::new(driver)))
.serve_with_shutdown(args.bind_address, async {
tokio::signal::ctrl_c().await.ok();
info!("Received shutdown signal, draining in-flight requests");
})
.await
.into_diagnostic()
}
#[cfg(not(target_os = "windows"))]
include!("main_unix.rs");
@@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use miette::{IntoDiagnostic, Result};
use std::net::SocketAddr;
use std::path::PathBuf;
use tracing::info;
use tracing_subscriber::EnvFilter;
use openshell_core::VERSION;
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
use openshell_driver_podman::config::{
DEFAULT_NETWORK_NAME, DEFAULT_PODMAN_STOP_TIMEOUT_SECS, DEFAULT_SANDBOX_PIDS_LIMIT,
ImagePullPolicy,
};
use openshell_driver_podman::{ComputeDriverService, PodmanComputeConfig, PodmanComputeDriver};
#[derive(Parser)]
#[command(name = "openshell-driver-podman")]
#[command(version = VERSION)]
struct Args {
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_BIND",
default_value = "127.0.0.1:50061"
)]
bind_address: SocketAddr,
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
log_level: String,
/// Path to the Podman API Unix socket.
#[arg(long, env = "OPENSHELL_PODMAN_SOCKET")]
podman_socket: Option<PathBuf>,
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
sandbox_image: Option<String>,
#[arg(
long,
env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY",
default_value_t = ImagePullPolicy::Missing
)]
sandbox_image_pull_policy: ImagePullPolicy,
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
grpc_endpoint: Option<String>,
/// Port the gateway server is listening on.
///
/// Used when `--grpc-endpoint` is not set to auto-detect the endpoint
/// that sandbox containers dial back to.
#[arg(
long,
env = "OPENSHELL_GATEWAY_PORT",
default_value_t = openshell_core::config::DEFAULT_SERVER_PORT
)]
gateway_port: u16,
/// Host gateway IP used for sandbox host aliases.
///
/// Empty uses Podman's `host-gateway` resolver.
#[arg(long, env = "OPENSHELL_PODMAN_HOST_GATEWAY_IP")]
host_gateway_ip: Option<String>,
#[arg(
long,
env = "OPENSHELL_SANDBOX_SSH_SOCKET_PATH",
default_value = openshell_core::container_paths::SSH_SOCKET_PATH
)]
sandbox_ssh_socket_path: String,
/// Podman bridge network name.
#[arg(long, env = "OPENSHELL_NETWORK_NAME", default_value = DEFAULT_NETWORK_NAME)]
network_name: String,
/// Container stop timeout in seconds (SIGTERM → SIGKILL).
#[arg(long, env = "OPENSHELL_STOP_TIMEOUT", default_value_t = DEFAULT_PODMAN_STOP_TIMEOUT_SECS)]
stop_timeout: u32,
/// Container cgroup PID limit for sandbox containers. Set 0 to inherit
/// Podman's runtime/default PID limit.
#[arg(
long,
env = "OPENSHELL_SANDBOX_PIDS_LIMIT",
default_value_t = DEFAULT_SANDBOX_PIDS_LIMIT
)]
sandbox_pids_limit: i64,
/// OCI image containing the openshell-sandbox supervisor binary.
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
supervisor_image: Option<String>,
/// Host path to the CA certificate for sandbox mTLS.
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CA")]
podman_tls_ca: Option<PathBuf>,
/// Host path to the client certificate for sandbox mTLS.
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CERT")]
podman_tls_cert: Option<PathBuf>,
/// Host path to the client private key for sandbox mTLS.
#[arg(long, env = "OPENSHELL_PODMAN_TLS_KEY")]
podman_tls_key: Option<PathBuf>,
/// Corporate forward proxy URL for the supervisor's upstream TLS dials,
/// in explicit `http://host:port` form (scheme and port required).
/// Credentials must not be embedded in the URL; use
/// `--sandbox-proxy-auth-file` instead.
#[arg(long, env = "OPENSHELL_SANDBOX_HTTPS_PROXY")]
sandbox_https_proxy: Option<String>,
/// Comma-separated `NO_PROXY` list injected alongside the proxy URL.
#[arg(long, env = "OPENSHELL_SANDBOX_NO_PROXY")]
sandbox_no_proxy: Option<String>,
/// Path to a file containing the corporate proxy credentials as
/// `user:pass`. Delivered to the supervisor through a root-only secret
/// mount so the credentials never appear in config or container metadata.
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_FILE")]
sandbox_proxy_auth_file: Option<String>,
/// Explicit acknowledgement (`true`) that the proxy credential is sent
/// as cleartext Basic auth over the plain-TCP connection to the http://
/// proxy. Required when `--sandbox-proxy-auth-file` is set.
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_ALLOW_INSECURE")]
sandbox_proxy_auth_allow_insecure: Option<bool>,
/// Send the destination hostname in CONNECT requests to the corporate
/// proxy instead of a validated IP. Only for proxies whose ACLs filter
/// on hostnames: the proxy then resolves the name itself, so sandbox
/// SSRF/`allowed_ips` validation no longer binds the connection.
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_CONNECT_BY_HOSTNAME")]
sandbox_proxy_connect_by_hostname: Option<bool>,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)),
)
.init();
let driver = PodmanComputeDriver::new(PodmanComputeConfig {
socket_path: args.podman_socket,
default_image: args.sandbox_image.unwrap_or_default(),
image_pull_policy: args.sandbox_image_pull_policy,
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
gateway_port: args.gateway_port,
host_gateway_ip: args
.host_gateway_ip
.unwrap_or_else(PodmanComputeConfig::default_host_gateway_ip),
sandbox_ssh_socket_path: args.sandbox_ssh_socket_path,
network_name: args.network_name,
stop_timeout_secs: args.stop_timeout,
supervisor_image: args
.supervisor_image
.unwrap_or_else(openshell_core::config::default_supervisor_image),
guest_tls_ca: args.podman_tls_ca,
guest_tls_cert: args.podman_tls_cert,
guest_tls_key: args.podman_tls_key,
sandbox_pids_limit: args.sandbox_pids_limit,
https_proxy: args.sandbox_https_proxy,
no_proxy: args.sandbox_no_proxy,
proxy_auth_file: args.sandbox_proxy_auth_file,
proxy_auth_allow_insecure: args.sandbox_proxy_auth_allow_insecure,
proxy_connect_by_hostname: args.sandbox_proxy_connect_by_hostname,
..PodmanComputeConfig::default()
})
.await
.into_diagnostic()?;
info!(address = %args.bind_address, "Starting Podman compute driver");
tonic::transport::Server::builder()
.add_service(ComputeDriverServer::new(ComputeDriverService::new(driver)))
.serve_with_shutdown(args.bind_address, async {
tokio::signal::ctrl_c().await.ok();
info!("Received shutdown signal, draining in-flight requests");
})
.await
.into_diagnostic()
}
@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use std::path::PathBuf;
const DEFAULT_SANDBOX_IMAGE: &str = "ghcr.io/nvidia/openshell/sandbox:latest";
const DEFAULT_SUPERVISOR_IMAGE: &str = "ghcr.io/nvidia/openshell/supervisor:latest";
const DEFAULT_SERVER_PORT: u16 = 8080;
const DEFAULT_STOP_TIMEOUT_SECS: u32 = 10;
const DEFAULT_NETWORK_NAME: &str = "openshell";
const DEFAULT_SANDBOX_PIDS_LIMIT: i64 = 2048;
const DEFAULT_HEALTH_CHECK_INTERVAL_SECS: u64 = 10;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImagePullPolicy {
Always,
#[default]
Missing,
Never,
Newer,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PodmanComputeConfig {
pub socket_path: Option<PathBuf>,
pub default_image: String,
pub image_pull_policy: ImagePullPolicy,
pub grpc_endpoint: String,
pub gateway_port: u16,
pub host_gateway_ip: String,
pub sandbox_ssh_socket_path: String,
pub network_name: String,
pub stop_timeout_secs: u32,
pub supervisor_image: String,
pub guest_tls_ca: Option<PathBuf>,
pub guest_tls_cert: Option<PathBuf>,
pub guest_tls_key: Option<PathBuf>,
pub sandbox_pids_limit: i64,
pub enable_bind_mounts: bool,
pub health_check_interval_secs: u64,
}
impl Default for PodmanComputeConfig {
fn default() -> Self {
Self {
socket_path: None,
default_image: DEFAULT_SANDBOX_IMAGE.to_string(),
image_pull_policy: ImagePullPolicy::default(),
grpc_endpoint: String::new(),
gateway_port: DEFAULT_SERVER_PORT,
host_gateway_ip: String::new(),
sandbox_ssh_socket_path: String::new(),
network_name: DEFAULT_NETWORK_NAME.to_string(),
stop_timeout_secs: DEFAULT_STOP_TIMEOUT_SECS,
supervisor_image: DEFAULT_SUPERVISOR_IMAGE.to_string(),
guest_tls_ca: None,
guest_tls_cert: None,
guest_tls_key: None,
sandbox_pids_limit: DEFAULT_SANDBOX_PIDS_LIMIT,
enable_bind_mounts: false,
health_check_interval_secs: DEFAULT_HEALTH_CHECK_INTERVAL_SECS,
}
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ path = "src/lib.rs"
name = "openshell-driver-vm"
path = "src/main.rs"
[dependencies]
[target.'cfg(not(target_os = "windows"))'.dependencies]
openshell-core = { path = "../openshell-core", default-features = false }
openshell-otel = { path = "../openshell-otel" }
openshell-policy = { path = "../openshell-policy" }
+6 -20
View File
@@ -1,24 +1,10 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
pub mod driver;
mod embedded_runtime;
mod ffi;
pub mod gpu;
pub mod lifecycle;
mod nft_ruleset;
pub mod otel_tracing;
pub mod procguard;
mod rootfs;
mod runtime;
#[cfg(target_os = "windows")]
pub fn windows_unsupported() -> &'static str {
"openshell-driver-vm is unsupported on Windows"
}
pub use driver::{VmDriver, VmDriverConfig};
pub use lifecycle::{
BackendFeature, ExtensionCapabilities, ExtensionDescriptor, GuestInitDropin, LaunchAbortReason,
LaunchPlan, LifecycleError, LifecycleExtension, LifecycleExtensionRegistry, LifecycleResult,
RestoreContext,
};
pub use runtime::{
VM_RUNTIME_DIR_ENV, VmBackend, VmLaunchConfig, cleanup_stale_tap_interfaces,
configured_runtime_dir, run_vm,
};
#[cfg(not(target_os = "windows"))]
include!("lib_unix.rs");
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
pub mod driver;
mod embedded_runtime;
mod ffi;
pub mod gpu;
pub mod lifecycle;
mod nft_ruleset;
pub mod otel_tracing;
pub mod procguard;
mod rootfs;
mod runtime;
pub use driver::{VmDriver, VmDriverConfig};
pub use lifecycle::{
BackendFeature, ExtensionCapabilities, ExtensionDescriptor, GuestInitDropin, LaunchAbortReason,
LaunchPlan, LifecycleError, LifecycleExtension, LifecycleExtensionRegistry, LifecycleResult,
RestoreContext,
};
pub use runtime::{
VM_RUNTIME_DIR_ENV, VmBackend, VmLaunchConfig, cleanup_stale_tap_interfaces,
configured_runtime_dir, run_vm,
};
+6 -777
View File
@@ -1,782 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use futures::Stream;
use miette::{IntoDiagnostic, Result};
use openshell_core::VERSION;
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
use openshell_driver_vm::otel_tracing::compute_driver_rpc_layer;
#[cfg(target_os = "macos")]
use openshell_driver_vm::{VM_RUNTIME_DIR_ENV, configured_runtime_dir};
use openshell_driver_vm::{VmBackend, VmDriver, VmDriverConfig, VmLaunchConfig, procguard, run_vm};
use std::io;
use std::net::SocketAddr;
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::net::{UnixListener, UnixStream};
use tracing::info;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
#[derive(Parser, Debug)]
#[command(name = "openshell-driver-vm")]
#[command(version = VERSION)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
#[arg(long, hide = true, default_value_t = false)]
internal_run_vm: bool,
#[arg(long = "vm-root-disk", hide = true, alias = "vm-rootfs")]
vm_root_disk: Option<PathBuf>,
#[arg(long = "vm-overlay-disk", hide = true)]
vm_overlay_disk: Option<PathBuf>,
#[arg(long = "vm-image-disk", hide = true)]
vm_image_disk: Option<PathBuf>,
#[arg(long = "vm-kernel-image", hide = true)]
vm_kernel_image: Option<PathBuf>,
#[arg(long, hide = true)]
vm_exec: Option<String>,
#[arg(long, hide = true, default_value = "/")]
vm_workdir: String,
#[arg(long, hide = true)]
vm_env: Vec<String>,
#[arg(long, hide = true)]
vm_console_output: Option<PathBuf>,
#[arg(long, hide = true, default_value_t = 2)]
vm_vcpus: u8,
#[arg(long, hide = true, default_value_t = 2048)]
vm_mem_mib: u32,
#[arg(long, hide = true, default_value_t = 1)]
vm_krun_log_level: u32,
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_BIND")]
bind_address: Option<SocketAddr>,
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
bind_socket: Option<PathBuf>,
#[arg(long, hide = true)]
expected_peer_pid: Option<u32>,
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_UNAUTHENTICATED_TCP",
default_value_t = false
)]
allow_unauthenticated_tcp: bool,
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_SAME_UID_PEER",
default_value_t = false
)]
allow_same_uid_peer: bool,
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
log_level: String,
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
otlp_endpoint: Option<String>,
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
openshell_endpoint: Option<String>,
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE", default_value = "")]
default_image: String,
#[arg(long, env = "OPENSHELL_VM_BOOTSTRAP_IMAGE", default_value = "")]
bootstrap_image: String,
#[arg(
long,
env = "OPENSHELL_VM_DRIVER_STATE_DIR",
default_value = "target/openshell-vm-driver"
)]
state_dir: PathBuf,
#[arg(long = "guest-tls-ca", env = "OPENSHELL_VM_TLS_CA")]
guest_tls_ca: Option<PathBuf>,
#[arg(long = "guest-tls-cert", env = "OPENSHELL_VM_TLS_CERT")]
guest_tls_cert: Option<PathBuf>,
#[arg(long = "guest-tls-key", env = "OPENSHELL_VM_TLS_KEY")]
guest_tls_key: Option<PathBuf>,
#[arg(long, env = "OPENSHELL_VM_KRUN_LOG_LEVEL", default_value_t = 1)]
krun_log_level: u32,
#[arg(long, env = "OPENSHELL_VM_DRIVER_VCPUS", default_value_t = 2)]
vcpus: u8,
#[arg(long, env = "OPENSHELL_VM_DRIVER_MEM_MIB", default_value_t = 2048)]
mem_mib: u32,
#[arg(long, env = "OPENSHELL_VM_OVERLAY_DISK_MIB", default_value_t = 4096)]
overlay_disk_mib: u64,
#[arg(long, env = "OPENSHELL_VM_GPU")]
gpu: bool,
#[arg(long, env = "OPENSHELL_VM_GPU_MEM_MIB", default_value_t = 8192)]
gpu_mem_mib: u32,
#[arg(long, env = "OPENSHELL_VM_GPU_VCPUS", default_value_t = 4)]
gpu_vcpus: u8,
#[arg(long, env = "OPENSHELL_VM_SANDBOX_UID")]
sandbox_uid: Option<u32>,
#[arg(long, env = "OPENSHELL_VM_SANDBOX_GID")]
sandbox_gid: Option<u32>,
#[arg(long, hide = true)]
vm_backend: Option<String>,
#[arg(long, hide = true)]
vm_gpu_bdf: Option<String>,
#[arg(long, hide = true)]
vm_tap_device: Option<String>,
#[arg(long, hide = true)]
vm_guest_ip: Option<String>,
#[arg(long, hide = true)]
vm_host_ip: Option<String>,
#[arg(long, hide = true)]
vm_vsock_cid: Option<u32>,
#[arg(long, hide = true)]
vm_guest_mac: Option<String>,
#[arg(long, hide = true)]
vm_gateway_port: Option<u16>,
#[cfg(target_os = "windows")]
fn main() {
eprintln!("openshell-driver-vm is unsupported on Windows");
std::process::exit(1);
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if args.internal_run_vm {
// We intentionally defer procguard arming until `run_vm()` so
// that the only arm is the one that knows how to clean up
// gvproxy. Racing two watchers against the same parent-death
// event causes the bare arm's `exit(1)` to win, skipping the
// gvproxy cleanup and leaking the helper. The risk window
// before `run_vm` arms procguard is ~a few syscalls long
// (`build_vm_launch_config`, `configured_runtime_dir`), which
// is negligible next to the parent gRPC server's uptime.
maybe_reexec_internal_vm_with_runtime_env()?;
let config = build_vm_launch_config(&args).map_err(|err| miette::miette!("{err}"))?;
run_vm(&config).map_err(|err| miette::miette!("{err}"))?;
return Ok(());
}
let (tracer_provider, setup_error) =
openshell_driver_vm::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
.with(tracing_subscriber::fmt::layer())
.with(
tracer_provider
.as_ref()
.map(openshell_driver_vm::otel_tracing::layer),
)
.init();
if let Some(error) = setup_error {
tracing::error!(%error, "OTLP exporting could not be started");
} else if let Some(endpoint) = &args.otlp_endpoint {
info!(endpoint, "OTLP exporting enabled");
}
let listen_mode = compute_driver_listen_mode(&args).map_err(|err| miette::miette!("{err}"))?;
// Arm procguard so that if the gateway is killed (SIGKILL or crash)
// we also die. Without this the driver is reparented to init and
// keeps its per-sandbox VM launchers alive forever. Launchers have
// their own procguards (armed in `run_vm`) which cascade cleanup of
// gvproxy and the libkrun worker the moment this driver exits.
if let Err(err) = procguard::die_with_parent() {
tracing::warn!(
error = %err,
"procguard arm failed; gateway crashes may orphan this driver"
);
}
let driver = VmDriver::new(VmDriverConfig {
openshell_endpoint: args
.openshell_endpoint
.ok_or_else(|| miette::miette!("OPENSHELL_GRPC_ENDPOINT is required"))?,
state_dir: args.state_dir.clone(),
launcher_bin: None,
default_image: args.default_image.clone(),
bootstrap_image: args.bootstrap_image.clone(),
log_level: args.log_level.clone(),
krun_log_level: args.krun_log_level,
vcpus: args.vcpus,
mem_mib: args.mem_mib,
overlay_disk_mib: args.overlay_disk_mib,
guest_tls_ca: args.guest_tls_ca.clone(),
guest_tls_cert: args.guest_tls_cert.clone(),
guest_tls_key: args.guest_tls_key.clone(),
gpu_enabled: args.gpu,
gpu_mem_mib: args.gpu_mem_mib,
gpu_vcpus: args.gpu_vcpus,
sandbox_uid: args.sandbox_uid,
sandbox_gid: args.sandbox_gid,
})
.await
.map_err(|err| miette::miette!("{err}"))?;
let result = match listen_mode {
ComputeDriverListenMode::Unix {
socket_path,
expected_peer_pid,
} => {
prepare_compute_driver_socket(&socket_path).map_err(|err| miette::miette!("{err}"))?;
info!(socket = %socket_path.display(), "Starting vm compute driver");
let listener = UnixListener::bind(&socket_path).into_diagnostic()?;
restrict_socket_permissions(&socket_path).map_err(|err| miette::miette!("{err}"))?;
let result = tonic::transport::Server::builder()
.layer(compute_driver_rpc_layer())
.add_service(ComputeDriverServer::new(driver))
.serve_with_incoming_shutdown(
AuthenticatedUnixIncoming::new(listener, expected_peer_pid),
shutdown_signal(),
)
.await
.into_diagnostic();
let _ = std::fs::remove_file(&socket_path);
result
}
ComputeDriverListenMode::Tcp(bind_address) => {
info!(address = %bind_address, "Starting unauthenticated dev vm compute driver");
tonic::transport::Server::builder()
.layer(compute_driver_rpc_layer())
.add_service(ComputeDriverServer::new(driver))
.serve_with_shutdown(bind_address, shutdown_signal())
.await
.into_diagnostic()
}
};
if let Some(provider) = &tracer_provider
&& let Err(error) = provider.shutdown()
{
tracing::warn!(%error, "OTLP tracer provider shutdown failed");
}
result
}
async fn shutdown_signal() {
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler");
tokio::select! {
result = tokio::signal::ctrl_c() => {
if let Err(error) = result {
tracing::warn!(%error, "failed to listen for Ctrl-C");
}
}
_ = terminate.recv() => {}
}
info!("Shutdown signal received; stopping vm compute driver");
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ComputeDriverListenMode {
Unix {
socket_path: PathBuf,
expected_peer_pid: Option<u32>,
},
Tcp(SocketAddr),
}
fn compute_driver_listen_mode(args: &Args) -> std::result::Result<ComputeDriverListenMode, String> {
if let Some(socket_path) = args.bind_socket.clone() {
if args.expected_peer_pid.is_none() && !args.allow_same_uid_peer {
return Err(
"--expected-peer-pid is required with --bind-socket; use --allow-same-uid-peer only for local development"
.to_string(),
);
}
return Ok(ComputeDriverListenMode::Unix {
socket_path,
expected_peer_pid: args.expected_peer_pid,
});
}
if !args.allow_unauthenticated_tcp {
return Err(
"--bind-socket is required; unauthenticated TCP mode is disabled unless --allow-unauthenticated-tcp is set for local development"
.to_string(),
);
}
let Some(bind_address) = args.bind_address else {
return Err("--bind-address is required with --allow-unauthenticated-tcp".to_string());
};
Ok(ComputeDriverListenMode::Tcp(bind_address))
}
fn prepare_compute_driver_socket(socket_path: &Path) -> std::result::Result<(), String> {
let Some(parent) = socket_path.parent() else {
return Err(format!(
"vm compute driver socket path '{}' has no parent directory",
socket_path.display()
));
};
let expected_uid = current_euid();
prepare_private_socket_dir(parent, expected_uid)?;
remove_stale_socket(socket_path, expected_uid)
}
fn current_euid() -> u32 {
rustix::process::geteuid().as_raw()
}
fn prepare_private_socket_dir(
socket_dir: &Path,
expected_uid: u32,
) -> std::result::Result<(), String> {
std::fs::create_dir_all(socket_dir)
.map_err(|err| format!("create socket dir {}: {err}", socket_dir.display()))?;
let metadata = std::fs::symlink_metadata(socket_dir)
.map_err(|err| format!("stat socket dir {}: {err}", socket_dir.display()))?;
let file_type = metadata.file_type();
if file_type.is_symlink() {
return Err(format!(
"socket dir {} is a symlink; refusing to use it",
socket_dir.display()
));
}
if !file_type.is_dir() {
return Err(format!(
"socket dir {} is not a directory",
socket_dir.display()
));
}
if metadata.uid() != expected_uid {
return Err(format!(
"socket dir {} is owned by uid {} but current euid is {}",
socket_dir.display(),
metadata.uid(),
expected_uid
));
}
std::fs::set_permissions(socket_dir, std::fs::Permissions::from_mode(0o700))
.map_err(|err| format!("chmod socket dir {}: {err}", socket_dir.display()))
}
fn remove_stale_socket(socket_path: &Path, expected_uid: u32) -> std::result::Result<(), String> {
let metadata = match std::fs::symlink_metadata(socket_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(format!("stat socket {}: {err}", socket_path.display())),
};
let file_type = metadata.file_type();
if file_type.is_symlink() {
return Err(format!(
"socket {} is a symlink; refusing to remove it",
socket_path.display()
));
}
if metadata.uid() != expected_uid {
return Err(format!(
"socket {} is owned by uid {} but current euid is {}",
socket_path.display(),
metadata.uid(),
expected_uid
));
}
if !file_type.is_socket() {
return Err(format!(
"socket path {} exists but is not a Unix socket",
socket_path.display()
));
}
std::fs::remove_file(socket_path)
.map_err(|err| format!("remove stale socket {}: {err}", socket_path.display()))
}
fn restrict_socket_permissions(socket_path: &Path) -> std::result::Result<(), String> {
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))
.map_err(|err| format!("chmod socket {}: {err}", socket_path.display()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PeerCredentials {
uid: u32,
pid: Option<i32>,
}
fn peer_credentials(stream: &UnixStream) -> std::result::Result<PeerCredentials, String> {
let credentials = stream
.peer_cred()
.map_err(|err| format!("read peer credentials: {err}"))?;
Ok(PeerCredentials {
uid: credentials.uid(),
pid: credentials.pid(),
})
}
fn authorize_peer_credentials(
peer: PeerCredentials,
driver_uid: u32,
gateway_pid: Option<u32>,
) -> std::result::Result<(), String> {
if peer.uid != driver_uid {
return Err(format!(
"peer uid {} does not match current euid {}",
peer.uid, driver_uid
));
}
let Some(gateway_pid) = gateway_pid else {
return Ok(());
};
let Some(peer_process_id) = peer.pid.and_then(|pid| u32::try_from(pid).ok()) else {
return Err(format!(
"peer pid is unavailable; expected gateway pid {gateway_pid}"
));
};
if peer_process_id != gateway_pid {
return Err(format!(
"peer pid {peer_process_id} does not match expected gateway pid {gateway_pid}"
));
}
Ok(())
}
struct AuthenticatedUnixIncoming {
listener: UnixListener,
expected_uid: u32,
expected_peer_pid: Option<u32>,
}
impl AuthenticatedUnixIncoming {
fn new(listener: UnixListener, expected_peer_pid: Option<u32>) -> Self {
Self {
listener,
expected_uid: current_euid(),
expected_peer_pid,
}
}
}
impl Stream for AuthenticatedUnixIncoming {
type Item = io::Result<UnixStream>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
match this.listener.poll_accept(cx) {
Poll::Ready(Ok((stream, _addr))) => {
let authorized = peer_credentials(&stream).and_then(|peer| {
authorize_peer_credentials(peer, this.expected_uid, this.expected_peer_pid)
});
match authorized {
Ok(()) => return Poll::Ready(Some(Ok(stream))),
Err(err) => {
tracing::warn!(
error = %err,
"rejected vm compute driver UDS client"
);
}
}
}
Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))),
Poll::Pending => return Poll::Pending,
}
}
}
}
fn build_vm_launch_config(args: &Args) -> std::result::Result<VmLaunchConfig, String> {
let root_disk = args
.vm_root_disk
.clone()
.ok_or_else(|| "--vm-root-disk is required in internal VM mode".to_string())?;
let overlay_disk = args
.vm_overlay_disk
.clone()
.ok_or_else(|| "--vm-overlay-disk is required in internal VM mode".to_string())?;
let image_disk = args.vm_image_disk.clone();
let exec_path = args
.vm_exec
.clone()
.ok_or_else(|| "--vm-exec is required in internal VM mode".to_string())?;
let console_output = args
.vm_console_output
.clone()
.ok_or_else(|| "--vm-console-output is required in internal VM mode".to_string())?;
let backend = match args.vm_backend.as_deref() {
Some("qemu") => VmBackend::Qemu,
Some("libkrun") | None => VmBackend::Libkrun,
Some(other) => return Err(format!("unknown VM backend: {other}")),
};
Ok(VmLaunchConfig {
root_disk,
overlay_disk,
image_disk,
kernel_image: args.vm_kernel_image.clone(),
vcpus: args.vm_vcpus,
mem_mib: args.vm_mem_mib,
exec_path,
args: Vec::new(),
env: args.vm_env.clone(),
workdir: args.vm_workdir.clone(),
log_level: args.vm_krun_log_level,
console_output,
backend,
gpu_bdf: args.vm_gpu_bdf.clone(),
tap_device: args.vm_tap_device.clone(),
guest_ip: args.vm_guest_ip.clone(),
host_ip: args.vm_host_ip.clone(),
vsock_cid: args.vm_vsock_cid,
guest_mac: args.vm_guest_mac.clone(),
gateway_port: args.vm_gateway_port,
})
}
#[cfg(target_os = "macos")]
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
use std::os::unix::process::CommandExt as _;
const REEXEC_ENV: &str = "__OPENSHELL_DRIVER_VM_REEXEC";
if std::env::var_os(REEXEC_ENV).is_some() {
return Ok(());
}
let runtime_dir = configured_runtime_dir().map_err(|err| miette::miette!("{err}"))?;
let runtime_str = runtime_dir.to_string_lossy();
let needs_reexec = std::env::var_os("DYLD_LIBRARY_PATH")
.is_none_or(|value| !value.to_string_lossy().contains(runtime_str.as_ref()));
if !needs_reexec {
return Ok(());
}
let mut dyld_paths = vec![runtime_dir.clone()];
if let Some(existing) = std::env::var_os("DYLD_LIBRARY_PATH") {
dyld_paths.extend(std::env::split_paths(&existing));
}
let joined = std::env::join_paths(&dyld_paths)
.map_err(|err| miette::miette!("join DYLD_LIBRARY_PATH: {err}"))?;
let exe = std::env::current_exe().into_diagnostic()?;
let args: Vec<String> = std::env::args().skip(1).collect();
// Use execvp() so the current process is *replaced* by the re-exec'd
// binary — no wrapper process sits between the compute driver and
// the actually-running VM launcher. That avoids two problems:
// 1. An extra process level that survives SIGKILL of the driver
// (the wrapper was reparenting the re-exec'd child to init).
// 2. Signal forwarding: with a wrapper, a SIGTERM to the wrapper
// doesn't reach the child unless we hand-roll forwarding.
// After exec, the child inherits our PID and our procguard arming.
let err = std::process::Command::new(exe)
.args(&args)
.env("DYLD_LIBRARY_PATH", &joined)
.env(VM_RUNTIME_DIR_ENV, runtime_dir)
.env(REEXEC_ENV, "1")
.exec();
// `exec()` only returns on failure.
Err(miette::miette!("failed to re-exec with runtime env: {err}"))
}
#[cfg(not(target_os = "macos"))]
// Signature must match the macOS variant which can fail.
#[allow(clippy::unnecessary_wraps)]
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
Args, ComputeDriverListenMode, PeerCredentials, authorize_peer_credentials,
compute_driver_listen_mode,
};
use clap::Parser;
use std::path::PathBuf;
#[test]
fn peer_authorization_accepts_matching_uid_and_pid() {
authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: Some(42),
},
1000,
Some(42),
)
.unwrap();
}
#[test]
fn peer_authorization_rejects_wrong_pid() {
let err = authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: Some(7),
},
1000,
Some(42),
)
.expect_err("wrong pid should be rejected");
assert!(err.contains("does not match expected gateway pid"));
}
#[test]
fn peer_authorization_rejects_wrong_uid() {
let err = authorize_peer_credentials(
PeerCredentials {
uid: 1001,
pid: Some(42),
},
1000,
Some(42),
)
.expect_err("wrong uid should be rejected");
assert!(err.contains("does not match current euid"));
}
#[test]
fn peer_authorization_rejects_missing_pid_when_expected() {
let err = authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: None,
},
1000,
Some(42),
)
.expect_err("missing pid should be rejected");
assert!(err.contains("peer pid is unavailable"));
}
#[test]
fn peer_authorization_accepts_matching_uid_without_expected_pid() {
authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: None,
},
1000,
None,
)
.unwrap();
}
#[test]
fn listen_mode_rejects_default_tcp() {
let args = Args::parse_from(["openshell-driver-vm"]);
let err = compute_driver_listen_mode(&args).expect_err("default TCP should be disabled");
assert!(err.contains("--bind-socket is required"));
}
#[test]
fn accepts_gateway_otlp_endpoint() {
let args = Args::try_parse_from([
"openshell-driver-vm",
"--otlp-endpoint",
"http://127.0.0.1:4317",
]);
assert!(
args.is_ok(),
"VM driver should accept the gateway OTLP endpoint"
);
}
#[test]
fn listen_mode_rejects_bind_address_without_tcp_opt_in() {
let args = Args::parse_from(["openshell-driver-vm", "--bind-address", "127.0.0.1:50061"]);
let err =
compute_driver_listen_mode(&args).expect_err("TCP bind should require explicit opt-in");
assert!(err.contains("--allow-unauthenticated-tcp"));
}
#[test]
fn listen_mode_requires_bind_address_with_tcp_opt_in() {
let args = Args::parse_from(["openshell-driver-vm", "--allow-unauthenticated-tcp"]);
let err =
compute_driver_listen_mode(&args).expect_err("TCP opt-in should require an address");
assert!(err.contains("--bind-address is required"));
}
#[test]
fn listen_mode_accepts_explicit_unauthenticated_tcp() {
let args = Args::parse_from([
"openshell-driver-vm",
"--allow-unauthenticated-tcp",
"--bind-address",
"127.0.0.1:50061",
]);
assert_eq!(
compute_driver_listen_mode(&args).unwrap(),
ComputeDriverListenMode::Tcp("127.0.0.1:50061".parse().unwrap())
);
}
#[test]
fn listen_mode_requires_expected_peer_pid_for_uds() {
let args = Args::parse_from([
"openshell-driver-vm",
"--bind-socket",
"/tmp/compute-driver.sock",
]);
let err = compute_driver_listen_mode(&args)
.expect_err("UDS should require gateway peer pid by default");
assert!(err.contains("--expected-peer-pid is required"));
}
#[test]
fn listen_mode_accepts_uds_with_expected_peer_pid() {
let args = Args::parse_from([
"openshell-driver-vm",
"--bind-socket",
"/tmp/compute-driver.sock",
"--expected-peer-pid",
"42",
]);
assert_eq!(
compute_driver_listen_mode(&args).unwrap(),
ComputeDriverListenMode::Unix {
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
expected_peer_pid: Some(42),
}
);
}
#[test]
fn listen_mode_accepts_explicit_same_uid_uds_dev_mode() {
let args = Args::parse_from([
"openshell-driver-vm",
"--bind-socket",
"/tmp/compute-driver.sock",
"--allow-same-uid-peer",
]);
assert_eq!(
compute_driver_listen_mode(&args).unwrap(),
ComputeDriverListenMode::Unix {
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
expected_peer_pid: None,
}
);
}
}
#[cfg(not(target_os = "windows"))]
include!("main_unix.rs");
+782
View File
@@ -0,0 +1,782 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use futures::Stream;
use miette::{IntoDiagnostic, Result};
use openshell_core::VERSION;
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
use openshell_driver_vm::otel_tracing::compute_driver_rpc_layer;
#[cfg(target_os = "macos")]
use openshell_driver_vm::{VM_RUNTIME_DIR_ENV, configured_runtime_dir};
use openshell_driver_vm::{VmBackend, VmDriver, VmDriverConfig, VmLaunchConfig, procguard, run_vm};
use std::io;
use std::net::SocketAddr;
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::net::{UnixListener, UnixStream};
use tracing::info;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
#[derive(Parser, Debug)]
#[command(name = "openshell-driver-vm")]
#[command(version = VERSION)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
#[arg(long, hide = true, default_value_t = false)]
internal_run_vm: bool,
#[arg(long = "vm-root-disk", hide = true, alias = "vm-rootfs")]
vm_root_disk: Option<PathBuf>,
#[arg(long = "vm-overlay-disk", hide = true)]
vm_overlay_disk: Option<PathBuf>,
#[arg(long = "vm-image-disk", hide = true)]
vm_image_disk: Option<PathBuf>,
#[arg(long = "vm-kernel-image", hide = true)]
vm_kernel_image: Option<PathBuf>,
#[arg(long, hide = true)]
vm_exec: Option<String>,
#[arg(long, hide = true, default_value = "/")]
vm_workdir: String,
#[arg(long, hide = true)]
vm_env: Vec<String>,
#[arg(long, hide = true)]
vm_console_output: Option<PathBuf>,
#[arg(long, hide = true, default_value_t = 2)]
vm_vcpus: u8,
#[arg(long, hide = true, default_value_t = 2048)]
vm_mem_mib: u32,
#[arg(long, hide = true, default_value_t = 1)]
vm_krun_log_level: u32,
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_BIND")]
bind_address: Option<SocketAddr>,
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
bind_socket: Option<PathBuf>,
#[arg(long, hide = true)]
expected_peer_pid: Option<u32>,
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_UNAUTHENTICATED_TCP",
default_value_t = false
)]
allow_unauthenticated_tcp: bool,
#[arg(
long,
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_SAME_UID_PEER",
default_value_t = false
)]
allow_same_uid_peer: bool,
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
log_level: String,
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
otlp_endpoint: Option<String>,
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
openshell_endpoint: Option<String>,
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE", default_value = "")]
default_image: String,
#[arg(long, env = "OPENSHELL_VM_BOOTSTRAP_IMAGE", default_value = "")]
bootstrap_image: String,
#[arg(
long,
env = "OPENSHELL_VM_DRIVER_STATE_DIR",
default_value = "target/openshell-vm-driver"
)]
state_dir: PathBuf,
#[arg(long = "guest-tls-ca", env = "OPENSHELL_VM_TLS_CA")]
guest_tls_ca: Option<PathBuf>,
#[arg(long = "guest-tls-cert", env = "OPENSHELL_VM_TLS_CERT")]
guest_tls_cert: Option<PathBuf>,
#[arg(long = "guest-tls-key", env = "OPENSHELL_VM_TLS_KEY")]
guest_tls_key: Option<PathBuf>,
#[arg(long, env = "OPENSHELL_VM_KRUN_LOG_LEVEL", default_value_t = 1)]
krun_log_level: u32,
#[arg(long, env = "OPENSHELL_VM_DRIVER_VCPUS", default_value_t = 2)]
vcpus: u8,
#[arg(long, env = "OPENSHELL_VM_DRIVER_MEM_MIB", default_value_t = 2048)]
mem_mib: u32,
#[arg(long, env = "OPENSHELL_VM_OVERLAY_DISK_MIB", default_value_t = 4096)]
overlay_disk_mib: u64,
#[arg(long, env = "OPENSHELL_VM_GPU")]
gpu: bool,
#[arg(long, env = "OPENSHELL_VM_GPU_MEM_MIB", default_value_t = 8192)]
gpu_mem_mib: u32,
#[arg(long, env = "OPENSHELL_VM_GPU_VCPUS", default_value_t = 4)]
gpu_vcpus: u8,
#[arg(long, env = "OPENSHELL_VM_SANDBOX_UID")]
sandbox_uid: Option<u32>,
#[arg(long, env = "OPENSHELL_VM_SANDBOX_GID")]
sandbox_gid: Option<u32>,
#[arg(long, hide = true)]
vm_backend: Option<String>,
#[arg(long, hide = true)]
vm_gpu_bdf: Option<String>,
#[arg(long, hide = true)]
vm_tap_device: Option<String>,
#[arg(long, hide = true)]
vm_guest_ip: Option<String>,
#[arg(long, hide = true)]
vm_host_ip: Option<String>,
#[arg(long, hide = true)]
vm_vsock_cid: Option<u32>,
#[arg(long, hide = true)]
vm_guest_mac: Option<String>,
#[arg(long, hide = true)]
vm_gateway_port: Option<u16>,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if args.internal_run_vm {
// We intentionally defer procguard arming until `run_vm()` so
// that the only arm is the one that knows how to clean up
// gvproxy. Racing two watchers against the same parent-death
// event causes the bare arm's `exit(1)` to win, skipping the
// gvproxy cleanup and leaking the helper. The risk window
// before `run_vm` arms procguard is ~a few syscalls long
// (`build_vm_launch_config`, `configured_runtime_dir`), which
// is negligible next to the parent gRPC server's uptime.
maybe_reexec_internal_vm_with_runtime_env()?;
let config = build_vm_launch_config(&args).map_err(|err| miette::miette!("{err}"))?;
run_vm(&config).map_err(|err| miette::miette!("{err}"))?;
return Ok(());
}
let (tracer_provider, setup_error) =
openshell_driver_vm::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
.with(tracing_subscriber::fmt::layer())
.with(
tracer_provider
.as_ref()
.map(openshell_driver_vm::otel_tracing::layer),
)
.init();
if let Some(error) = setup_error {
tracing::error!(%error, "OTLP exporting could not be started");
} else if let Some(endpoint) = &args.otlp_endpoint {
info!(endpoint, "OTLP exporting enabled");
}
let listen_mode = compute_driver_listen_mode(&args).map_err(|err| miette::miette!("{err}"))?;
// Arm procguard so that if the gateway is killed (SIGKILL or crash)
// we also die. Without this the driver is reparented to init and
// keeps its per-sandbox VM launchers alive forever. Launchers have
// their own procguards (armed in `run_vm`) which cascade cleanup of
// gvproxy and the libkrun worker the moment this driver exits.
if let Err(err) = procguard::die_with_parent() {
tracing::warn!(
error = %err,
"procguard arm failed; gateway crashes may orphan this driver"
);
}
let driver = VmDriver::new(VmDriverConfig {
openshell_endpoint: args
.openshell_endpoint
.ok_or_else(|| miette::miette!("OPENSHELL_GRPC_ENDPOINT is required"))?,
state_dir: args.state_dir.clone(),
launcher_bin: None,
default_image: args.default_image.clone(),
bootstrap_image: args.bootstrap_image.clone(),
log_level: args.log_level.clone(),
krun_log_level: args.krun_log_level,
vcpus: args.vcpus,
mem_mib: args.mem_mib,
overlay_disk_mib: args.overlay_disk_mib,
guest_tls_ca: args.guest_tls_ca.clone(),
guest_tls_cert: args.guest_tls_cert.clone(),
guest_tls_key: args.guest_tls_key.clone(),
gpu_enabled: args.gpu,
gpu_mem_mib: args.gpu_mem_mib,
gpu_vcpus: args.gpu_vcpus,
sandbox_uid: args.sandbox_uid,
sandbox_gid: args.sandbox_gid,
})
.await
.map_err(|err| miette::miette!("{err}"))?;
let result = match listen_mode {
ComputeDriverListenMode::Unix {
socket_path,
expected_peer_pid,
} => {
prepare_compute_driver_socket(&socket_path).map_err(|err| miette::miette!("{err}"))?;
info!(socket = %socket_path.display(), "Starting vm compute driver");
let listener = UnixListener::bind(&socket_path).into_diagnostic()?;
restrict_socket_permissions(&socket_path).map_err(|err| miette::miette!("{err}"))?;
let result = tonic::transport::Server::builder()
.layer(compute_driver_rpc_layer())
.add_service(ComputeDriverServer::new(driver))
.serve_with_incoming_shutdown(
AuthenticatedUnixIncoming::new(listener, expected_peer_pid),
shutdown_signal(),
)
.await
.into_diagnostic();
let _ = std::fs::remove_file(&socket_path);
result
}
ComputeDriverListenMode::Tcp(bind_address) => {
info!(address = %bind_address, "Starting unauthenticated dev vm compute driver");
tonic::transport::Server::builder()
.layer(compute_driver_rpc_layer())
.add_service(ComputeDriverServer::new(driver))
.serve_with_shutdown(bind_address, shutdown_signal())
.await
.into_diagnostic()
}
};
if let Some(provider) = &tracer_provider
&& let Err(error) = provider.shutdown()
{
tracing::warn!(%error, "OTLP tracer provider shutdown failed");
}
result
}
async fn shutdown_signal() {
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler");
tokio::select! {
result = tokio::signal::ctrl_c() => {
if let Err(error) = result {
tracing::warn!(%error, "failed to listen for Ctrl-C");
}
}
_ = terminate.recv() => {}
}
info!("Shutdown signal received; stopping vm compute driver");
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ComputeDriverListenMode {
Unix {
socket_path: PathBuf,
expected_peer_pid: Option<u32>,
},
Tcp(SocketAddr),
}
fn compute_driver_listen_mode(args: &Args) -> std::result::Result<ComputeDriverListenMode, String> {
if let Some(socket_path) = args.bind_socket.clone() {
if args.expected_peer_pid.is_none() && !args.allow_same_uid_peer {
return Err(
"--expected-peer-pid is required with --bind-socket; use --allow-same-uid-peer only for local development"
.to_string(),
);
}
return Ok(ComputeDriverListenMode::Unix {
socket_path,
expected_peer_pid: args.expected_peer_pid,
});
}
if !args.allow_unauthenticated_tcp {
return Err(
"--bind-socket is required; unauthenticated TCP mode is disabled unless --allow-unauthenticated-tcp is set for local development"
.to_string(),
);
}
let Some(bind_address) = args.bind_address else {
return Err("--bind-address is required with --allow-unauthenticated-tcp".to_string());
};
Ok(ComputeDriverListenMode::Tcp(bind_address))
}
fn prepare_compute_driver_socket(socket_path: &Path) -> std::result::Result<(), String> {
let Some(parent) = socket_path.parent() else {
return Err(format!(
"vm compute driver socket path '{}' has no parent directory",
socket_path.display()
));
};
let expected_uid = current_euid();
prepare_private_socket_dir(parent, expected_uid)?;
remove_stale_socket(socket_path, expected_uid)
}
fn current_euid() -> u32 {
rustix::process::geteuid().as_raw()
}
fn prepare_private_socket_dir(
socket_dir: &Path,
expected_uid: u32,
) -> std::result::Result<(), String> {
std::fs::create_dir_all(socket_dir)
.map_err(|err| format!("create socket dir {}: {err}", socket_dir.display()))?;
let metadata = std::fs::symlink_metadata(socket_dir)
.map_err(|err| format!("stat socket dir {}: {err}", socket_dir.display()))?;
let file_type = metadata.file_type();
if file_type.is_symlink() {
return Err(format!(
"socket dir {} is a symlink; refusing to use it",
socket_dir.display()
));
}
if !file_type.is_dir() {
return Err(format!(
"socket dir {} is not a directory",
socket_dir.display()
));
}
if metadata.uid() != expected_uid {
return Err(format!(
"socket dir {} is owned by uid {} but current euid is {}",
socket_dir.display(),
metadata.uid(),
expected_uid
));
}
std::fs::set_permissions(socket_dir, std::fs::Permissions::from_mode(0o700))
.map_err(|err| format!("chmod socket dir {}: {err}", socket_dir.display()))
}
fn remove_stale_socket(socket_path: &Path, expected_uid: u32) -> std::result::Result<(), String> {
let metadata = match std::fs::symlink_metadata(socket_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(format!("stat socket {}: {err}", socket_path.display())),
};
let file_type = metadata.file_type();
if file_type.is_symlink() {
return Err(format!(
"socket {} is a symlink; refusing to remove it",
socket_path.display()
));
}
if metadata.uid() != expected_uid {
return Err(format!(
"socket {} is owned by uid {} but current euid is {}",
socket_path.display(),
metadata.uid(),
expected_uid
));
}
if !file_type.is_socket() {
return Err(format!(
"socket path {} exists but is not a Unix socket",
socket_path.display()
));
}
std::fs::remove_file(socket_path)
.map_err(|err| format!("remove stale socket {}: {err}", socket_path.display()))
}
fn restrict_socket_permissions(socket_path: &Path) -> std::result::Result<(), String> {
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))
.map_err(|err| format!("chmod socket {}: {err}", socket_path.display()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PeerCredentials {
uid: u32,
pid: Option<i32>,
}
fn peer_credentials(stream: &UnixStream) -> std::result::Result<PeerCredentials, String> {
let credentials = stream
.peer_cred()
.map_err(|err| format!("read peer credentials: {err}"))?;
Ok(PeerCredentials {
uid: credentials.uid(),
pid: credentials.pid(),
})
}
fn authorize_peer_credentials(
peer: PeerCredentials,
driver_uid: u32,
gateway_pid: Option<u32>,
) -> std::result::Result<(), String> {
if peer.uid != driver_uid {
return Err(format!(
"peer uid {} does not match current euid {}",
peer.uid, driver_uid
));
}
let Some(gateway_pid) = gateway_pid else {
return Ok(());
};
let Some(peer_process_id) = peer.pid.and_then(|pid| u32::try_from(pid).ok()) else {
return Err(format!(
"peer pid is unavailable; expected gateway pid {gateway_pid}"
));
};
if peer_process_id != gateway_pid {
return Err(format!(
"peer pid {peer_process_id} does not match expected gateway pid {gateway_pid}"
));
}
Ok(())
}
struct AuthenticatedUnixIncoming {
listener: UnixListener,
expected_uid: u32,
expected_peer_pid: Option<u32>,
}
impl AuthenticatedUnixIncoming {
fn new(listener: UnixListener, expected_peer_pid: Option<u32>) -> Self {
Self {
listener,
expected_uid: current_euid(),
expected_peer_pid,
}
}
}
impl Stream for AuthenticatedUnixIncoming {
type Item = io::Result<UnixStream>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
match this.listener.poll_accept(cx) {
Poll::Ready(Ok((stream, _addr))) => {
let authorized = peer_credentials(&stream).and_then(|peer| {
authorize_peer_credentials(peer, this.expected_uid, this.expected_peer_pid)
});
match authorized {
Ok(()) => return Poll::Ready(Some(Ok(stream))),
Err(err) => {
tracing::warn!(
error = %err,
"rejected vm compute driver UDS client"
);
}
}
}
Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))),
Poll::Pending => return Poll::Pending,
}
}
}
}
fn build_vm_launch_config(args: &Args) -> std::result::Result<VmLaunchConfig, String> {
let root_disk = args
.vm_root_disk
.clone()
.ok_or_else(|| "--vm-root-disk is required in internal VM mode".to_string())?;
let overlay_disk = args
.vm_overlay_disk
.clone()
.ok_or_else(|| "--vm-overlay-disk is required in internal VM mode".to_string())?;
let image_disk = args.vm_image_disk.clone();
let exec_path = args
.vm_exec
.clone()
.ok_or_else(|| "--vm-exec is required in internal VM mode".to_string())?;
let console_output = args
.vm_console_output
.clone()
.ok_or_else(|| "--vm-console-output is required in internal VM mode".to_string())?;
let backend = match args.vm_backend.as_deref() {
Some("qemu") => VmBackend::Qemu,
Some("libkrun") | None => VmBackend::Libkrun,
Some(other) => return Err(format!("unknown VM backend: {other}")),
};
Ok(VmLaunchConfig {
root_disk,
overlay_disk,
image_disk,
kernel_image: args.vm_kernel_image.clone(),
vcpus: args.vm_vcpus,
mem_mib: args.vm_mem_mib,
exec_path,
args: Vec::new(),
env: args.vm_env.clone(),
workdir: args.vm_workdir.clone(),
log_level: args.vm_krun_log_level,
console_output,
backend,
gpu_bdf: args.vm_gpu_bdf.clone(),
tap_device: args.vm_tap_device.clone(),
guest_ip: args.vm_guest_ip.clone(),
host_ip: args.vm_host_ip.clone(),
vsock_cid: args.vm_vsock_cid,
guest_mac: args.vm_guest_mac.clone(),
gateway_port: args.vm_gateway_port,
})
}
#[cfg(target_os = "macos")]
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
use std::os::unix::process::CommandExt as _;
const REEXEC_ENV: &str = "__OPENSHELL_DRIVER_VM_REEXEC";
if std::env::var_os(REEXEC_ENV).is_some() {
return Ok(());
}
let runtime_dir = configured_runtime_dir().map_err(|err| miette::miette!("{err}"))?;
let runtime_str = runtime_dir.to_string_lossy();
let needs_reexec = std::env::var_os("DYLD_LIBRARY_PATH")
.is_none_or(|value| !value.to_string_lossy().contains(runtime_str.as_ref()));
if !needs_reexec {
return Ok(());
}
let mut dyld_paths = vec![runtime_dir.clone()];
if let Some(existing) = std::env::var_os("DYLD_LIBRARY_PATH") {
dyld_paths.extend(std::env::split_paths(&existing));
}
let joined = std::env::join_paths(&dyld_paths)
.map_err(|err| miette::miette!("join DYLD_LIBRARY_PATH: {err}"))?;
let exe = std::env::current_exe().into_diagnostic()?;
let args: Vec<String> = std::env::args().skip(1).collect();
// Use execvp() so the current process is *replaced* by the re-exec'd
// binary — no wrapper process sits between the compute driver and
// the actually-running VM launcher. That avoids two problems:
// 1. An extra process level that survives SIGKILL of the driver
// (the wrapper was reparenting the re-exec'd child to init).
// 2. Signal forwarding: with a wrapper, a SIGTERM to the wrapper
// doesn't reach the child unless we hand-roll forwarding.
// After exec, the child inherits our PID and our procguard arming.
let err = std::process::Command::new(exe)
.args(&args)
.env("DYLD_LIBRARY_PATH", &joined)
.env(VM_RUNTIME_DIR_ENV, runtime_dir)
.env(REEXEC_ENV, "1")
.exec();
// `exec()` only returns on failure.
Err(miette::miette!("failed to re-exec with runtime env: {err}"))
}
#[cfg(not(target_os = "macos"))]
// Signature must match the macOS variant which can fail.
#[allow(clippy::unnecessary_wraps)]
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
Args, ComputeDriverListenMode, PeerCredentials, authorize_peer_credentials,
compute_driver_listen_mode,
};
use clap::Parser;
use std::path::PathBuf;
#[test]
fn peer_authorization_accepts_matching_uid_and_pid() {
authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: Some(42),
},
1000,
Some(42),
)
.unwrap();
}
#[test]
fn peer_authorization_rejects_wrong_pid() {
let err = authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: Some(7),
},
1000,
Some(42),
)
.expect_err("wrong pid should be rejected");
assert!(err.contains("does not match expected gateway pid"));
}
#[test]
fn peer_authorization_rejects_wrong_uid() {
let err = authorize_peer_credentials(
PeerCredentials {
uid: 1001,
pid: Some(42),
},
1000,
Some(42),
)
.expect_err("wrong uid should be rejected");
assert!(err.contains("does not match current euid"));
}
#[test]
fn peer_authorization_rejects_missing_pid_when_expected() {
let err = authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: None,
},
1000,
Some(42),
)
.expect_err("missing pid should be rejected");
assert!(err.contains("peer pid is unavailable"));
}
#[test]
fn peer_authorization_accepts_matching_uid_without_expected_pid() {
authorize_peer_credentials(
PeerCredentials {
uid: 1000,
pid: None,
},
1000,
None,
)
.unwrap();
}
#[test]
fn listen_mode_rejects_default_tcp() {
let args = Args::parse_from(["openshell-driver-vm"]);
let err = compute_driver_listen_mode(&args).expect_err("default TCP should be disabled");
assert!(err.contains("--bind-socket is required"));
}
#[test]
fn accepts_gateway_otlp_endpoint() {
let args = Args::try_parse_from([
"openshell-driver-vm",
"--otlp-endpoint",
"http://127.0.0.1:4317",
]);
assert!(
args.is_ok(),
"VM driver should accept the gateway OTLP endpoint"
);
}
#[test]
fn listen_mode_rejects_bind_address_without_tcp_opt_in() {
let args = Args::parse_from(["openshell-driver-vm", "--bind-address", "127.0.0.1:50061"]);
let err =
compute_driver_listen_mode(&args).expect_err("TCP bind should require explicit opt-in");
assert!(err.contains("--allow-unauthenticated-tcp"));
}
#[test]
fn listen_mode_requires_bind_address_with_tcp_opt_in() {
let args = Args::parse_from(["openshell-driver-vm", "--allow-unauthenticated-tcp"]);
let err =
compute_driver_listen_mode(&args).expect_err("TCP opt-in should require an address");
assert!(err.contains("--bind-address is required"));
}
#[test]
fn listen_mode_accepts_explicit_unauthenticated_tcp() {
let args = Args::parse_from([
"openshell-driver-vm",
"--allow-unauthenticated-tcp",
"--bind-address",
"127.0.0.1:50061",
]);
assert_eq!(
compute_driver_listen_mode(&args).unwrap(),
ComputeDriverListenMode::Tcp("127.0.0.1:50061".parse().unwrap())
);
}
#[test]
fn listen_mode_requires_expected_peer_pid_for_uds() {
let args = Args::parse_from([
"openshell-driver-vm",
"--bind-socket",
"/tmp/compute-driver.sock",
]);
let err = compute_driver_listen_mode(&args)
.expect_err("UDS should require gateway peer pid by default");
assert!(err.contains("--expected-peer-pid is required"));
}
#[test]
fn listen_mode_accepts_uds_with_expected_peer_pid() {
let args = Args::parse_from([
"openshell-driver-vm",
"--bind-socket",
"/tmp/compute-driver.sock",
"--expected-peer-pid",
"42",
]);
assert_eq!(
compute_driver_listen_mode(&args).unwrap(),
ComputeDriverListenMode::Unix {
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
expected_peer_pid: Some(42),
}
);
}
#[test]
fn listen_mode_accepts_explicit_same_uid_uds_dev_mode() {
let args = Args::parse_from([
"openshell-driver-vm",
"--bind-socket",
"/tmp/compute-driver.sock",
"--allow-same-uid-peer",
]);
assert_eq!(
compute_driver_listen_mode(&args).unwrap(),
ComputeDriverListenMode::Unix {
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
expected_peer_pid: None,
}
);
}
}
+6 -2
View File
@@ -1207,11 +1207,15 @@ mod tests {
let expected = format!(
"sqlite:{}",
tmp.path().join("openshell/gateway/openshell.db").display()
tmp.path()
.join("openshell")
.join("gateway")
.join("openshell.db")
.display()
);
assert!(local_tls.is_none());
assert_eq!(args.db_url.as_deref(), Some(expected.as_str()));
assert!(tmp.path().join("openshell/gateway").is_dir());
assert!(tmp.path().join("openshell").join("gateway").is_dir());
}
#[test]
+72 -2
View File
@@ -42,11 +42,21 @@ use openshell_core::proto::{
SandboxTemplate, ServiceEndpoint, SshSession,
};
use openshell_core::{ObjectLabels, ObjectWorkspace};
#[cfg(not(target_os = "windows"))]
use openshell_driver_docker::DockerComputeDriver;
#[cfg(target_os = "windows")]
use openshell_driver_kubernetes::KubernetesComputeConfig;
#[cfg(not(target_os = "windows"))]
use openshell_driver_kubernetes::{
ComputeDriverService as KubernetesDriverService, KubernetesComputeDriver,
ComputeDriverService as KubernetesDriverService, KubernetesComputeConfig,
KubernetesComputeDriver,
};
#[cfg(target_os = "windows")]
use openshell_driver_podman::PodmanComputeConfig;
#[cfg(not(target_os = "windows"))]
use openshell_driver_podman::{
ComputeDriverService as PodmanDriverService, PodmanComputeConfig, PodmanComputeDriver,
};
use openshell_driver_podman::{ComputeDriverService as PodmanDriverService, PodmanComputeDriver};
use prost::Message;
use std::collections::HashMap;
use std::fmt;
@@ -268,6 +278,7 @@ trait ShutdownCleanup: Send + Sync {
}
#[tonic::async_trait]
#[cfg(not(target_os = "windows"))]
impl ShutdownCleanup for DockerComputeDriver {
async fn cleanup_on_shutdown(&self) -> Result<(), String> {
let stopped = self
@@ -293,6 +304,7 @@ trait StartupResume: Send + Sync {
}
#[tonic::async_trait]
#[cfg(not(target_os = "windows"))]
impl StartupResume for DockerComputeDriver {
async fn resume_sandbox(&self, sandbox_id: &str, sandbox_name: &str) -> Result<bool, String> {
Self::resume_sandbox(self, sandbox_id, sandbox_name)
@@ -310,6 +322,11 @@ const ORPHAN_GRACE_PERIOD: Duration = Duration::from_secs(300);
// Re-export the shared error type under the name used by this module.
pub use openshell_core::ComputeDriverError as ComputeError;
#[cfg(target_os = "windows")]
fn unsupported_compute_driver_error(driver: &str) -> ComputeError {
ComputeError::Message(format!("{driver} compute driver is unsupported on Windows"))
}
#[derive(Debug)]
pub struct ManagedDriverProcess {
child: std::sync::Mutex<Option<tokio::process::Child>>,
@@ -317,6 +334,7 @@ pub struct ManagedDriverProcess {
}
impl ManagedDriverProcess {
#[cfg(unix)]
pub(crate) fn new(child: tokio::process::Child, socket_path: PathBuf) -> Self {
Self {
child: std::sync::Mutex::new(Some(child)),
@@ -710,6 +728,7 @@ impl ComputeRuntime {
self.delete_gates.entry_count()
}
#[cfg(not(target_os = "windows"))]
pub async fn new_docker(
config: openshell_core::Config,
docker_config: DockerComputeConfig,
@@ -742,6 +761,20 @@ impl ComputeRuntime {
.await
}
#[cfg(target_os = "windows")]
pub async fn new_docker(
_config: openshell_core::Config,
_docker_config: DockerComputeConfig,
_store: Arc<Store>,
_sandbox_index: SandboxIndex,
_sandbox_watch_bus: SandboxWatchBus,
_tracing_log_bus: TracingLogBus,
_supervisor_sessions: Arc<SupervisorSessionRegistry>,
) -> Result<Self, ComputeError> {
Err(unsupported_compute_driver_error("Docker"))
}
#[cfg(not(target_os = "windows"))]
pub async fn new_kubernetes(
config: KubernetesComputeConfig,
store: Arc<Store>,
@@ -769,6 +802,18 @@ impl ComputeRuntime {
.await
}
#[cfg(target_os = "windows")]
pub async fn new_kubernetes(
_config: KubernetesComputeConfig,
_store: Arc<Store>,
_sandbox_index: SandboxIndex,
_sandbox_watch_bus: SandboxWatchBus,
_tracing_log_bus: TracingLogBus,
_supervisor_sessions: Arc<SupervisorSessionRegistry>,
) -> Result<Self, ComputeError> {
Err(unsupported_compute_driver_error("Kubernetes"))
}
pub(crate) async fn new_remote_driver(
endpoint: AcquiredRemoteDriverEndpoint,
store: Arc<Store>,
@@ -793,6 +838,7 @@ impl ComputeRuntime {
.await
}
#[cfg(not(target_os = "windows"))]
pub async fn new_podman(
config: PodmanComputeConfig,
store: Arc<Store>,
@@ -820,6 +866,18 @@ impl ComputeRuntime {
.await
}
#[cfg(target_os = "windows")]
pub async fn new_podman(
_config: PodmanComputeConfig,
_store: Arc<Store>,
_sandbox_index: SandboxIndex,
_sandbox_watch_bus: SandboxWatchBus,
_tracing_log_bus: TracingLogBus,
_supervisor_sessions: Arc<SupervisorSessionRegistry>,
) -> Result<Self, ComputeError> {
Err(unsupported_compute_driver_error("Podman"))
}
#[must_use]
pub fn default_image(&self) -> &str {
&self.default_image
@@ -7246,4 +7304,16 @@ mod tests {
.unwrap();
assert_eq!(stored.object_workspace(), "alpha");
}
#[cfg(target_os = "windows")]
#[test]
fn windows_compute_driver_stubs_report_unsupported() {
for driver in ["Docker", "Kubernetes", "Podman"] {
let message = unsupported_compute_driver_error(driver).to_string();
assert!(
message.contains("unsupported on Windows"),
"{driver} stub message should be explicit, got: {message}"
);
}
}
}
+31 -1
View File
@@ -59,8 +59,11 @@ use tonic::transport::Endpoint;
#[cfg(unix)]
use tower::service_fn;
#[cfg(unix)]
const DRIVER_BIN_NAME: &str = "openshell-driver-vm";
#[cfg(unix)]
const COMPUTE_DRIVER_SOCKET_RUN_DIR: &str = "run";
#[cfg(unix)]
const COMPUTE_DRIVER_SOCKET_NAME: &str = "compute-driver.sock";
/// Configuration for launching and talking to the VM compute driver.
@@ -139,6 +142,7 @@ impl VmComputeConfig {
4096
}
#[cfg(unix)]
#[must_use]
fn default_driver_search_dirs(home: Option<PathBuf>) -> Vec<PathBuf> {
let mut dirs = Vec::new();
@@ -189,6 +193,7 @@ pub struct VmGuestTlsPaths {
/// `/usr/local/libexec/openshell`, `/usr/local/libexec`.
/// 3. Sibling of the gateway's own executable (last-resort fallback so
/// local development builds still work out of the box).
#[cfg(unix)]
pub fn resolve_compute_driver_bin(vm_config: &VmComputeConfig) -> Result<PathBuf> {
let mut searched: Vec<PathBuf> = Vec::new();
@@ -227,6 +232,7 @@ pub fn resolve_compute_driver_bin(vm_config: &VmComputeConfig) -> Result<PathBuf
)))
}
#[cfg(unix)]
fn resolve_driver_search_dirs(vm_config: &VmComputeConfig) -> Vec<PathBuf> {
vm_config.driver_dir.clone().map_or_else(
|| {
@@ -248,6 +254,7 @@ fn resolve_driver_search_dirs(vm_config: &VmComputeConfig) -> Vec<PathBuf> {
)
}
#[cfg(unix)]
fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
if !paths.iter().any(|existing| existing == &path) {
paths.push(path);
@@ -255,6 +262,7 @@ fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
}
/// Path of the Unix domain socket the driver will listen on.
#[cfg(unix)]
pub fn compute_driver_socket_path(vm_config: &VmComputeConfig) -> PathBuf {
vm_config
.state_dir
@@ -527,7 +535,21 @@ fn append_otlp_args(command: &mut Command, otlp_config: Option<&OtlpConfig>) {
}
}
#[cfg(not(unix))]
#[cfg(target_os = "windows")]
fn unsupported_vm_message() -> &'static str {
"VM compute driver is unsupported on Windows"
}
#[cfg(target_os = "windows")]
pub async fn spawn(
_config: &Config,
_vm_config: &VmComputeConfig,
_otlp_config: Option<&OtlpConfig>,
) -> Result<AcquiredRemoteDriverEndpoint> {
Err(Error::config(unsupported_vm_message()))
}
#[cfg(all(not(unix), not(target_os = "windows")))]
pub async fn spawn(
_config: &Config,
_vm_config: &VmComputeConfig,
@@ -947,3 +969,11 @@ mod tests {
assert!(!socket_path.exists());
}
}
#[cfg(all(test, target_os = "windows"))]
mod windows_tests {
#[test]
fn windows_spawn_reports_unsupported() {
assert!(super::unsupported_vm_message().contains("unsupported on Windows"));
}
}