feat(terminal-browser): support managed proxy sandbox

This commit is contained in:
Felipe Coury
2026-07-06 16:42:07 -03:00
parent 2046175b2c
commit 70d4255207
12 changed files with 524 additions and 28 deletions
+2
View File
@@ -19,6 +19,8 @@ mod launcher;
mod linux_run_main;
#[cfg(target_os = "linux")]
mod proxy_routing;
#[cfg(target_os = "linux")]
mod runtime_proxy_argument;
#[cfg(target_os = "linux")]
pub fn run_main() -> ! {
@@ -25,10 +25,13 @@ use crate::launcher::exec_bwrap;
use crate::launcher::preferred_bwrap_supports_argv0;
use crate::proxy_routing::activate_proxy_routes_in_netns;
use crate::proxy_routing::prepare_host_proxy_route_spec;
use crate::runtime_proxy_argument::rewrite_http_proxy_argument_from_env;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_sandboxing::SandboxDirectSpawnRuntime;
use codex_sandboxing::SandboxRuntimeProxyArgument;
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
static BWRAP_CHILD_PID: AtomicI32 = AtomicI32::new(0);
@@ -126,6 +129,14 @@ pub struct LandlockCommand {
#[arg(long = "proxy-route-spec", hide = true)]
pub proxy_route_spec: Option<String>,
/// Internal: rewrite one command argument from the effective in-namespace HTTP proxy.
#[arg(
long = "rewrite-http-proxy-argument-prefix",
hide = true,
allow_hyphen_values = true
)]
pub rewrite_http_proxy_argument_prefix: Option<String>,
/// When set, skip mounting a fresh `/proc` even though PID isolation is
/// still enabled. This is primarily intended for restrictive container
/// environments that deny `--proc /proc`.
@@ -153,6 +164,7 @@ pub fn run_main() -> ! {
apply_seccomp_then_exec,
allow_network_for_proxy,
proxy_route_spec,
rewrite_http_proxy_argument_prefix,
no_proc,
command,
} = LandlockCommand::parse();
@@ -161,6 +173,15 @@ pub fn run_main() -> ! {
panic!("No command specified to execute.");
}
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_legacy_landlock);
let runtime = SandboxDirectSpawnRuntime {
proxy_argument: match rewrite_http_proxy_argument_prefix {
Some(argument_prefix) => {
SandboxRuntimeProxyArgument::RewriteFromHttpProxy { argument_prefix }
}
None => SandboxRuntimeProxyArgument::Unchanged,
},
};
ensure_direct_spawn_runtime_is_valid(&runtime, allow_network_for_proxy);
let EffectivePermissions {
permission_profile,
file_system_sandbox_policy,
@@ -176,6 +197,7 @@ pub fn run_main() -> ! {
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
// established the filesystem view.
if apply_seccomp_then_exec {
let mut command = command;
if allow_network_for_proxy {
let spec = proxy_route_spec
.as_deref()
@@ -183,6 +205,13 @@ pub fn run_main() -> ! {
if let Err(err) = activate_proxy_routes_in_netns(spec) {
panic!("error activating Linux proxy routing bridge: {err}");
}
if let SandboxRuntimeProxyArgument::RewriteFromHttpProxy { argument_prefix } =
&runtime.proxy_argument
&& let Err(err) =
rewrite_http_proxy_argument_from_env(&mut command, argument_prefix)
{
panic!("error applying effective proxy endpoint to command: {err}");
}
}
let proxy_routing_active = allow_network_for_proxy;
if let Err(e) = apply_permission_profile_to_current_thread(
@@ -228,6 +257,7 @@ pub fn run_main() -> ! {
permission_profile: &permission_profile,
allow_network_for_proxy,
proxy_route_spec,
runtime: runtime.clone(),
command,
});
run_bwrap_with_proc_fallback(
@@ -254,6 +284,20 @@ pub fn run_main() -> ! {
exec_or_panic(command);
}
fn ensure_direct_spawn_runtime_is_valid(
runtime: &SandboxDirectSpawnRuntime,
allow_network_for_proxy: bool,
) {
if !allow_network_for_proxy
&& !matches!(
&runtime.proxy_argument,
SandboxRuntimeProxyArgument::Unchanged
)
{
panic!("runtime proxy argument rewrite requires managed proxy routing");
}
}
#[derive(Debug, Clone)]
struct EffectivePermissions {
permission_profile: PermissionProfile,
@@ -1394,6 +1438,7 @@ struct InnerSeccompCommandArgs<'a> {
permission_profile: &'a PermissionProfile,
allow_network_for_proxy: bool,
proxy_route_spec: Option<String>,
runtime: SandboxDirectSpawnRuntime,
command: Vec<String>,
}
@@ -1405,6 +1450,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
permission_profile,
allow_network_for_proxy,
proxy_route_spec,
runtime,
command,
} = args;
let current_exe = match std::env::current_exe() {
@@ -1437,6 +1483,12 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
inner.push("--proxy-route-spec".to_string());
inner.push(proxy_route_spec);
}
if let SandboxRuntimeProxyArgument::RewriteFromHttpProxy { argument_prefix } =
runtime.proxy_argument
{
inner.push("--rewrite-http-proxy-argument-prefix".to_string());
inner.push(argument_prefix);
}
inner.push("--".to_string());
inner.extend(command);
inner
@@ -5,6 +5,8 @@ use crate::linux_run_main::install_bwrap_signal_forwarders;
#[cfg(test)]
use crate::linux_run_main::wait_for_bwrap_child;
#[cfg(test)]
use clap::Parser as _;
#[cfg(test)]
use codex_protocol::models::PermissionProfile;
#[cfg(test)]
use codex_protocol::protocol::FileSystemSandboxPolicy;
@@ -468,11 +470,49 @@ fn managed_proxy_inner_command_includes_route_spec() {
permission_profile: &permission_profile,
allow_network_for_proxy: true,
proxy_route_spec: Some("{\"routes\":[]}".to_string()),
runtime: SandboxDirectSpawnRuntime {
proxy_argument: SandboxRuntimeProxyArgument::RewriteFromHttpProxy {
argument_prefix: "--proxy-server=".to_string(),
},
},
command: vec!["/bin/true".to_string()],
});
assert!(args.iter().any(|arg| arg == "--proxy-route-spec"));
assert!(args.iter().any(|arg| arg == "{\"routes\":[]}"));
assert!(
args.windows(2).any(|window| {
window == ["--rewrite-http-proxy-argument-prefix", "--proxy-server="]
})
);
}
#[test]
fn helper_cli_accepts_hyphen_prefixed_runtime_proxy_argument_prefix() {
let parsed = LandlockCommand::try_parse_from([
"codex-linux-sandbox",
"--sandbox-policy-cwd",
"/tmp",
"--allow-network-for-proxy",
"--rewrite-http-proxy-argument-prefix",
"--proxy-server=",
"--",
"carbonyl",
"--proxy-server=http://127.0.0.1:43128",
])
.expect("parse Linux sandbox helper arguments");
assert_eq!(
parsed.rewrite_http_proxy_argument_prefix.as_deref(),
Some("--proxy-server=")
);
assert_eq!(
parsed.command,
vec![
"carbonyl".to_string(),
"--proxy-server=http://127.0.0.1:43128".to_string(),
]
);
}
#[test]
@@ -484,6 +524,7 @@ fn inner_command_includes_permission_profile_flag() {
permission_profile: &permission_profile,
allow_network_for_proxy: false,
proxy_route_spec: None,
runtime: SandboxDirectSpawnRuntime::default(),
command: vec!["/bin/true".to_string()],
});
@@ -503,6 +544,7 @@ fn non_managed_inner_command_omits_route_spec() {
permission_profile: &permission_profile,
allow_network_for_proxy: false,
proxy_route_spec: None,
runtime: SandboxDirectSpawnRuntime::default(),
command: vec!["/bin/true".to_string()],
});
@@ -519,6 +561,7 @@ fn managed_proxy_inner_command_requires_route_spec() {
permission_profile: &permission_profile,
allow_network_for_proxy: true,
proxy_route_spec: None,
runtime: SandboxDirectSpawnRuntime::default(),
command: vec!["/bin/true".to_string()],
})
});
@@ -0,0 +1,80 @@
use std::io;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use url::Url;
/// Replaces exactly one non-program argument with the effective loopback HTTP proxy endpoint.
///
/// This runs only after the network-namespace bridge rewrites `HTTP_PROXY`. The argument prefix
/// comes from trusted direct-spawn orchestration metadata; no shell interpolation occurs.
pub(crate) fn rewrite_http_proxy_argument_from_env(
command: &mut [String],
argument_prefix: &str,
) -> io::Result<()> {
let proxy_url = std::env::var("HTTP_PROXY").map_err(|_| {
io::Error::new(
io::ErrorKind::NotFound,
"runtime proxy argument rewrite requires HTTP_PROXY",
)
})?;
rewrite_http_proxy_argument(command, argument_prefix, &proxy_url)
}
fn rewrite_http_proxy_argument(
command: &mut [String],
argument_prefix: &str,
proxy_url: &str,
) -> io::Result<()> {
if argument_prefix.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"runtime proxy argument prefix must not be empty",
));
}
let endpoint = parse_loopback_http_proxy_endpoint(proxy_url).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"runtime HTTP proxy must be a parseable nonzero loopback endpoint",
)
})?;
let matching_indices = command
.iter()
.enumerate()
.skip(1)
.filter_map(|(index, argument)| argument.starts_with(argument_prefix).then_some(index))
.collect::<Vec<_>>();
let [argument_index] = matching_indices.as_slice() else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"runtime proxy argument rewrite expected exactly one `{argument_prefix}` argument"
),
));
};
command[*argument_index] = format!("{argument_prefix}http://{endpoint}");
Ok(())
}
fn parse_loopback_http_proxy_endpoint(proxy_url: &str) -> Option<SocketAddr> {
let parsed = Url::parse(proxy_url).ok()?;
if parsed.scheme() != "http" {
return None;
}
let host = parsed.host_str()?;
let port = parsed.port_or_known_default()?;
if port == 0 {
return None;
}
let ip = if host.eq_ignore_ascii_case("localhost") {
IpAddr::V4(Ipv4Addr::LOCALHOST)
} else {
host.parse::<IpAddr>().ok()?
};
ip.is_loopback().then_some(SocketAddr::new(ip, port))
}
#[cfg(test)]
#[path = "runtime_proxy_argument_tests.rs"]
mod tests;
@@ -0,0 +1,59 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn rewrites_runtime_proxy_argument_to_effective_loopback_endpoint() {
let mut command = vec![
"carbonyl".to_string(),
"--proxy-server=http://127.0.0.1:43128".to_string(),
"--remote-debugging-pipe".to_string(),
];
rewrite_http_proxy_argument(&mut command, "--proxy-server=", "http://127.0.0.1:45219")
.expect("rewrite proxy argument");
assert_eq!(
command,
vec![
"carbonyl".to_string(),
"--proxy-server=http://127.0.0.1:45219".to_string(),
"--remote-debugging-pipe".to_string(),
]
);
}
#[test]
fn runtime_proxy_argument_rewrite_rejects_ambiguous_or_non_loopback_inputs() {
let mut duplicate = vec![
"carbonyl".to_string(),
"--proxy-server=http://127.0.0.1:1".to_string(),
"--proxy-server=http://127.0.0.1:2".to_string(),
];
let duplicate_error =
rewrite_http_proxy_argument(&mut duplicate, "--proxy-server=", "http://127.0.0.1:45219")
.expect_err("duplicate proxy arguments must be rejected");
assert_eq!(duplicate_error.kind(), std::io::ErrorKind::InvalidInput);
let mut command = vec![
"carbonyl".to_string(),
"--proxy-server=http://127.0.0.1:1".to_string(),
];
let endpoint_error =
rewrite_http_proxy_argument(&mut command, "--proxy-server=", "http://192.0.2.10:45219")
.expect_err("non-loopback effective proxy must be rejected");
assert_eq!(endpoint_error.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn runtime_proxy_argument_rewrite_rejects_non_http_proxy_schemes() {
let mut command = vec![
"carbonyl".to_string(),
"--proxy-server=http://127.0.0.1:1".to_string(),
];
let error =
rewrite_http_proxy_argument(&mut command, "--proxy-server=", "socks5h://127.0.0.1:45219")
.expect_err("non-HTTP effective proxy must be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
}