Skip to content
Open
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
6 changes: 6 additions & 0 deletions crates/openshell-core/src/sandbox_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ pub const LOG_LEVEL: &str = "OPENSHELL_LOG_LEVEL";
/// Shell command to run inside the sandbox.
pub const SANDBOX_COMMAND: &str = "OPENSHELL_SANDBOX_COMMAND";

/// Sandbox-local loopback HTTP proxy URL managed by the supervisor.
///
/// This is distinct from `HTTP_PROXY`/`HTTPS_PROXY`, which continue to point
/// at the gateway-side proxy address for ordinary proxy-aware clients.
pub const LOOPBACK_PROXY_URL: &str = "OPENSHELL_LOOPBACK_PROXY_URL";

/// Path to the CA certificate for mTLS communication with the gateway.
pub const TLS_CA: &str = "OPENSHELL_TLS_CA";

Expand Down
28 changes: 28 additions & 0 deletions crates/openshell-sandbox/src/child_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub fn proxy_env_vars(proxy_url: &str) -> [(&'static str, String); 9] {
]
}

pub fn loopback_proxy_env_vars(proxy_url: &str) -> [(&'static str, String); 1] {
[(
openshell_core::sandbox_env::LOOPBACK_PROXY_URL,
proxy_url.to_owned(),
)]
}

pub fn tls_env_vars(
ca_cert_path: &Path,
combined_bundle_path: &Path,
Expand Down Expand Up @@ -65,6 +72,27 @@ mod tests {
assert!(stdout.contains("no_proxy=127.0.0.1,localhost,::1"));
}

#[test]
fn apply_loopback_proxy_env_exposes_managed_url_without_changing_proxy_vars() {
let mut cmd = Command::new("/usr/bin/env");
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());

for (key, value) in proxy_env_vars("http://10.200.0.1:3128") {
cmd.env(key, value);
}
for (key, value) in loopback_proxy_env_vars("http://127.0.0.1:3128") {
cmd.env(key, value);
}

let output = cmd.output().expect("spawn env");
let stdout = String::from_utf8(output.stdout).expect("utf8");

assert!(stdout.contains("HTTP_PROXY=http://10.200.0.1:3128"));
assert!(stdout.contains("OPENSHELL_LOOPBACK_PROXY_URL=http://127.0.0.1:3128"));
}

#[test]
fn apply_tls_env_sets_node_and_bundle_paths() {
let mut cmd = Command::new("/usr/bin/env");
Expand Down
71 changes: 62 additions & 9 deletions crates/openshell-sandbox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ use crate::l7::tls::{
};
use crate::opa::OpaEngine;
use crate::policy::{NetworkMode, NetworkPolicy, ProxyPolicy, SandboxPolicy};
#[cfg(target_os = "linux")]
use crate::proxy::LoopbackProxyHandle;
use crate::proxy::ProxyHandle;
#[cfg(target_os = "linux")]
use crate::sandbox::linux::netns::NetworkNamespace;
Expand Down Expand Up @@ -571,8 +573,10 @@ pub async fn run_sandbox(
// the entrypoint process's /proc/net/tcp for identity binding.
let entrypoint_pid = Arc::new(AtomicU32::new(0));

let (_proxy, denial_rx, bypass_denial_tx) = if matches!(policy.network.mode, NetworkMode::Proxy)
{
let (_proxy, loopback_proxy, denial_rx, bypass_denial_tx) = if matches!(
policy.network.mode,
NetworkMode::Proxy
) {
let proxy_policy = policy.network.proxy.as_ref().ok_or_else(|| {
miette::miette!("Network mode is set to proxy but no proxy configuration was provided")
})?;
Expand Down Expand Up @@ -617,21 +621,66 @@ pub async fn run_sandbox(
let proxy_handle = ProxyHandle::start_with_bind_addr(
proxy_policy,
bind_addr,
engine,
cache,
engine.clone(),
cache.clone(),
entrypoint_pid.clone(),
tls_state,
inference_ctx,
tls_state.clone(),
inference_ctx.clone(),
Some(provider_credentials.clone()),
Some(policy_local_ctx.clone()),
denial_tx,
denial_tx.clone(),
)
.await?;
(Some(proxy_handle), denial_rx, bypass_denial_tx)

#[cfg(target_os = "linux")]
let loopback_proxy_handle = if let (Some(ns), Some(_upstream_addr)) =
(netns.as_ref(), bind_addr)
{
let Some(netns_fd) = ns.ns_fd() else {
return Err(miette::miette!(
"Managed loopback proxy requires a sandbox network namespace file descriptor"
));
};
let port = proxy_policy.http_addr.map_or(3128, |addr| addr.port());
let listen_addr: SocketAddr = ([127, 0, 0, 1], port).into();
Some(LoopbackProxyHandle::start_in_netns(
netns_fd,
listen_addr,
engine,
cache,
entrypoint_pid.clone(),
tls_state,
inference_ctx,
Some(provider_credentials.clone()),
Some(policy_local_ctx.clone()),
denial_tx,
)?)
} else {
None
};

#[cfg(not(target_os = "linux"))]
let loopback_proxy_handle: Option<()> = None;

(
Some(proxy_handle),
loopback_proxy_handle,
denial_rx,
bypass_denial_tx,
)
} else {
(None, None, None)
(None, None, None, None)
};

#[cfg(target_os = "linux")]
let loopback_proxy_url = loopback_proxy.as_ref().map(LoopbackProxyHandle::proxy_url);

#[cfg(not(target_os = "linux"))]
let _ = &loopback_proxy;

#[cfg(not(target_os = "linux"))]
let loopback_proxy_url: Option<String> = None;

// Spawn bypass detection monitor (Linux only, proxy mode only).
// Reads /dev/kmsg for nftables log entries and emits structured
// tracing events for direct connection attempts that bypass the proxy.
Expand Down Expand Up @@ -758,6 +807,7 @@ pub async fn run_sandbox(
let policy_clone = policy.clone();
let workdir_clone = workdir.clone();
let proxy_url = ssh_proxy_url;
let loopback_proxy_url = loopback_proxy_url.clone();
let netns_fd = ssh_netns_fd;
let ca_paths = ca_file_paths.clone();
let provider_credentials_clone = provider_credentials.clone();
Expand All @@ -772,6 +822,7 @@ pub async fn run_sandbox(
workdir_clone,
netns_fd,
proxy_url,
loopback_proxy_url,
ca_paths,
provider_credentials_clone,
)
Expand Down Expand Up @@ -838,6 +889,7 @@ pub async fn run_sandbox(
interactive,
&policy,
netns.as_ref(),
loopback_proxy_url.as_deref(),
ca_file_paths.as_ref(),
&provider_env,
)?;
Expand All @@ -849,6 +901,7 @@ pub async fn run_sandbox(
workdir.as_deref(),
interactive,
&policy,
loopback_proxy_url.as_deref(),
ca_file_paths.as_ref(),
&provider_env,
)?;
Expand Down
20 changes: 20 additions & 0 deletions crates/openshell-sandbox/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl ProcessHandle {
interactive: bool,
policy: &SandboxPolicy,
netns: Option<&NetworkNamespace>,
loopback_proxy_url: Option<&str>,
ca_paths: Option<&(PathBuf, PathBuf)>,
provider_env: &HashMap<String, String>,
) -> Result<Self> {
Expand All @@ -104,6 +105,7 @@ impl ProcessHandle {
interactive,
policy,
netns.and_then(NetworkNamespace::ns_fd),
loopback_proxy_url,
ca_paths,
provider_env,
)
Expand All @@ -115,12 +117,14 @@ impl ProcessHandle {
///
/// Returns an error if the process fails to start.
#[cfg(not(target_os = "linux"))]
#[allow(clippy::too_many_arguments)]
pub fn spawn(
program: &str,
args: &[String],
workdir: Option<&str>,
interactive: bool,
policy: &SandboxPolicy,
loopback_proxy_url: Option<&str>,
ca_paths: Option<&(PathBuf, PathBuf)>,
provider_env: &HashMap<String, String>,
) -> Result<Self> {
Expand All @@ -130,6 +134,7 @@ impl ProcessHandle {
workdir,
interactive,
policy,
loopback_proxy_url,
ca_paths,
provider_env,
)
Expand All @@ -144,6 +149,7 @@ impl ProcessHandle {
interactive: bool,
policy: &SandboxPolicy,
netns_fd: Option<RawFd>,
loopback_proxy_url: Option<&str>,
ca_paths: Option<&(PathBuf, PathBuf)>,
provider_env: &HashMap<String, String>,
) -> Result<Self> {
Expand Down Expand Up @@ -185,6 +191,12 @@ impl ProcessHandle {
}
}

if let Some(url) = loopback_proxy_url {
for (key, value) in child_env::loopback_proxy_env_vars(url) {
cmd.env(key, value);
}
}

// Set TLS trust store env vars so sandbox processes trust the ephemeral CA
if let Some((ca_cert_path, combined_bundle_path)) = ca_paths {
for (key, value) in child_env::tls_env_vars(ca_cert_path, combined_bundle_path) {
Expand Down Expand Up @@ -264,12 +276,14 @@ impl ProcessHandle {
}

#[cfg(not(target_os = "linux"))]
#[allow(clippy::too_many_arguments)]
fn spawn_impl(
program: &str,
args: &[String],
workdir: Option<&str>,
interactive: bool,
policy: &SandboxPolicy,
loopback_proxy_url: Option<&str>,
ca_paths: Option<&(PathBuf, PathBuf)>,
provider_env: &HashMap<String, String>,
) -> Result<Self> {
Expand Down Expand Up @@ -301,6 +315,12 @@ impl ProcessHandle {
}
}

if let Some(url) = loopback_proxy_url {
for (key, value) in child_env::loopback_proxy_env_vars(url) {
cmd.env(key, value);
}
}

// Set TLS trust store env vars so sandbox processes trust the ephemeral CA
if let Some((ca_cert_path, combined_bundle_path)) = ca_paths {
for (key, value) in child_env::tls_env_vars(ca_cert_path, combined_bundle_path) {
Expand Down
Loading
Loading