Skip to content
Closed
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
5 changes: 5 additions & 0 deletions src/core/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ To target a specific backend instead of the host default, use
[`build_request_with_containment`] with a [`Containment`] β€” the same choice the
TypeScript SDK makes with `createConfigFromPolicy(policy, containment)`.

`Containment::Lxc` carries the Linux distribution and release, defaulting to
the TypeScript SDK's `alpine`/`3.23` values. The request can be used with the
engine's run-to-completion surface; `mxc-sdk::run` and `spawn_sandbox` do not
yet support LXC because they require a streaming backend.

Filesystem-policy discovery helpers (ports of the SDK's `policy.ts`) are also
available to feed a policy: [`available_tools_policy`] (PATH + tool/SDK env
dirs), [`user_profile_policy`], and [`temporary_files_policy`].
Expand Down
9 changes: 6 additions & 3 deletions src/core/mxc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
//! | Seatbelt | macOS | [`Containment::Process`] |
//! | ProcessContainer (AppContainer / BaseContainer) | Windows | [`Containment::Process`] |
//! | Explicit ProcessContainer settings | Windows | [`Containment::ProcessContainer`] |
//! | LXC request construction | Linux | [`Containment::Lxc`] |
//! | WSLC (WSL Container) | Windows | [`Containment::Wslc`] |
//!
//! WSLC is **experimental**: build with the crate's `wslc` feature, and call
Expand All @@ -59,8 +60,10 @@
//! process-input API), so [`Sandbox::take_stdin`] returns `None` for it.
//!
//! Other backends (Windows Sandbox, IsolationSession, MicroVM, Hyperlight,
//! LXC) return an [`Error`] with [`ErrorCode::UnsupportedContainment`]; drive
//! the standalone executor binaries for those.
//! LXC) return an [`Error`] with [`ErrorCode::UnsupportedContainment`] from
//! [`run`] / [`spawn_sandbox`]; drive the standalone executor binaries for
//! those. LXC request construction is available for consumers of
//! `mxc_engine`'s run-to-completion surface.
//!
//! ```no_run
//! use mxc_sdk::{build_request_with_containment, run, Containment, SandboxPolicy, WslcSection};
Expand Down Expand Up @@ -116,7 +119,7 @@ pub use mxc_engine::policy;
pub use mxc_engine::{
available_backends, available_tools_policy, build_request, build_request_with_containment,
platform_support, temporary_files_policy, user_profile_policy, AvailableBackend,
BackendCapability, Containment, Error, ErrorCode, FilesystemPolicyResult, PlatformSupport,
BackendCapability, Containment, Error, ErrorCode, FilesystemPolicyResult, Lxc, PlatformSupport,
ProcessContainer, SandboxPolicy, SandboxRequest, WslcSection,
};

Expand Down
2 changes: 1 addition & 1 deletion src/core/mxc_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub use platform::isolation_session_available;
pub use platform::{platform_support, PlatformSupport};
pub use policy::{
available_tools_policy, build_request, build_request_with_containment, temporary_files_policy,
user_profile_policy, Containment, FilesystemPolicyResult, ProcessContainer, SandboxPolicy,
user_profile_policy, Containment, FilesystemPolicyResult, Lxc, ProcessContainer, SandboxPolicy,
SandboxRequest, WslcSection,
};
pub use probe::{available_backends, AvailableBackend, BackendCapability};
Expand Down
111 changes: 105 additions & 6 deletions src/core/mxc_engine/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,9 +556,11 @@ pub struct CaptureDenialsSection {
/// Rust analogue of the SDK's `ContainmentType | ContainmentBackend` argument
/// to `createConfigFromPolicy`.
///
/// Only the backends this library can actually run are listed; select a
/// concrete backend when you specifically need it, and prefer
/// [`Containment::Process`] otherwise.
/// Select a concrete backend when you specifically need it, and prefer
/// [`Containment::Process`] otherwise. Request construction and streaming
/// execution support are separate: [`Containment::Lxc`] can be built here, but
/// the public Rust SDK does not yet expose the engine's LXC run-to-completion
/// path.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Containment {
/// Abstract "OS-native process isolation" intent, resolved per host:
Expand All @@ -567,6 +569,8 @@ pub enum Containment {
Process,
/// Windows ProcessContainer with AppContainer-specific settings.
ProcessContainer(ProcessContainer),
/// Linux LXC container settings.
Lxc(Lxc),
/// WSL Container backend: a Linux container on a Windows host, via the WSLC
/// SDK, configured by the carried [`WslcSection`]
/// (`WslcSection::default()` matches the SDK's defaults).
Expand All @@ -586,6 +590,24 @@ pub struct ProcessContainer {
pub capabilities: Vec<String>,
}

/// Linux LXC settings carried by [`Containment::Lxc`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lxc {
/// Linux distribution name.
pub distribution: String,
/// Distribution release.
pub release: String,
}

impl Default for Lxc {
fn default() -> Self {
Self {
distribution: "alpine".to_string(),
release: "3.23".to_string(),
}
}
}

/// WSL Container settings, mirroring the SDK's `experimental.wslc` config
/// block; carried by [`Containment::Wslc`].
///
Expand Down Expand Up @@ -923,6 +945,7 @@ fn build_wire_config(
let accepts_host_rules_without_outbound = match containment {
Containment::Process => cfg!(any(target_os = "linux", target_os = "macos")),
Containment::ProcessContainer(_) => false,
Containment::Lxc(_) => true,
Containment::Wslc(_) => true,
};

Expand Down Expand Up @@ -955,6 +978,7 @@ fn build_wire_config(
Containment::ProcessContainer(process_container) => {
apply_process_container_backend(&mut config, policy, process_container)
}
Containment::Lxc(lxc) => apply_lxc_backend(&mut config, lxc),
Containment::Wslc(wslc) => apply_wslc_backend(&mut config, wslc),
}
Ok(config)
Expand Down Expand Up @@ -1065,6 +1089,17 @@ fn apply_process_container_backend(
}
}

fn apply_lxc_backend(config: &mut serde_json::Value, lxc: &Lxc) {
use serde_json::json;

config["containment"] = json!("lxc");
config["lxc"] = json!({
"distribution": lxc.distribution,
"release": lxc.release,
});
apply_linux_network_policy(config);
}

/// True when the network section carries any host allow/deny rules, deciding
/// whether host-level enforcement is engaged.
fn has_host_rules(network: &serde_json::Value) -> bool {
Expand All @@ -1091,7 +1126,6 @@ fn apply_wslc_backend(config: &mut serde_json::Value, wslc: &WslcSection) {
/// Promote network enforcement to `firewall` when host rules are present and
/// no cooperative proxy is configured β€” the Linux counterpart of the SDK's
/// `applyLinuxNetworkPolicy`.
#[cfg(target_os = "linux")]
fn apply_linux_network_policy(config: &mut serde_json::Value) {
use serde_json::json;
let Some(network) = config.get_mut("network") else {
Expand Down Expand Up @@ -1642,10 +1676,10 @@ mod tests {
}

use super::{
build_request_with_containment, build_wire_config, Containment, ProcessContainer,
build_request_with_containment, build_wire_config, Containment, Lxc, ProcessContainer,
WslcSection,
};
use wxc_common::models::ContainmentBackend;
use wxc_common::models::{ContainmentBackend, NetworkEnforcementMode};

fn minimal_policy() -> SandboxPolicy {
SandboxPolicy {
Expand Down Expand Up @@ -1782,6 +1816,71 @@ mod tests {
);
}

#[test]
fn lxc_containment_maps_config_to_the_request() {
let lxc = Lxc {
distribution: "ubuntu".to_string(),
release: "24.04".to_string(),
};

let request =
build_request_with_containment(&minimal_policy(), &Containment::Lxc(lxc), None)
.expect("LXC settings should satisfy the wire contract");

assert_eq!(request.inner.containment, ContainmentBackend::Lxc);
assert_eq!(request.inner.lxc_config.distribution, "ubuntu");
assert_eq!(request.inner.lxc_config.release, "24.04");
}

#[test]
fn lxc_defaults_match_the_typescript_sdk() {
let request = build_request_with_containment(
&minimal_policy(),
&Containment::Lxc(Lxc::default()),
None,
)
.expect("default LXC settings should build");

assert_eq!(request.inner.lxc_config.distribution, "alpine");
assert_eq!(request.inner.lxc_config.release, "3.23");
}

#[test]
fn lxc_host_rules_select_firewall_enforcement() {
let policy = policy_with_network(NetworkSection {
allow_outbound: true,
allowed_hosts: vec!["example.com".to_string()],
..Default::default()
});

let request =
build_request_with_containment(&policy, &Containment::Lxc(Lxc::default()), None)
.expect("LXC host filtering should build");

assert_eq!(
request.inner.policy.network_enforcement_mode,
NetworkEnforcementMode::Firewall
);
}

#[test]
fn lxc_wire_shape_stays_compatible_with_versioned_contracts() {
let config = build_wire_config(
&minimal_policy(),
&Containment::Lxc(Lxc {
distribution: "alpine".to_string(),
release: "3.23".to_string(),
}),
None,
)
.expect("build LXC wire config");

assert_eq!(config["containment"], "lxc");
assert_eq!(config["lxc"]["distribution"], "alpine");
assert_eq!(config["lxc"]["release"], "3.23");
assert!(config.get("processContainer").is_none());
}

#[test]
fn wslc_containment_maps_config_to_the_request() {
// Mirrors `createConfigFromPolicy(policy, 'wslc')` plus a tweaked
Expand Down