mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
feat(terminal-browser): support managed proxy sandbox
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user