mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Honor explicit Unix socket grants in the Linux managed sandbox (#45534)
## Why Linux proxy-routed sandboxing denied standalone Unix sockets even when the effective network policy enabled `dangerously_allow_all_unix_sockets`. ## What changed - Carry Unix socket permissions in `ManagedNetworkSandboxContext` and pass the prepared context through Linux sandbox launches with `--managed-network`. - Allow `AF_UNIX` socket creation in proxy-routed mode when `dangerously_allow_all_unix_sockets` is enabled, while preserving network namespace isolation and restrictions on other socket families. - Keep standalone Unix sockets denied by default and for path-only grants. Default missing fields in older serialized contexts to restrictive values. ## Testing Add coverage for policy preparation and transport, legacy deserialization, and malformed policy rejection. Add a Linux integration test covering default denial, path-only denial, and explicit allow-all access, while checking that direct TCP access and `AF_NETLINK`/`AF_VSOCK` sockets remain blocked. GitOrigin-RevId: 2695b945ad3e59fcb3faf7662d852a26650af16c
This commit is contained in:
@@ -23,7 +23,6 @@ use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams;
|
||||
@@ -364,6 +363,7 @@ async fn run_command_under_sandbox(
|
||||
.map(codex_core::config::StartedNetworkProxy::proxy);
|
||||
// Proxy containment depends on whether a proxy is active, not whether its
|
||||
// policy came from managed requirements.
|
||||
#[cfg(target_os = "macos")]
|
||||
let enforce_managed_network = network.is_some();
|
||||
let managed_mitm_ca_trust_bundle_path = match network.as_ref() {
|
||||
Some(network) => network.managed_mitm_ca_trust_bundle_path(),
|
||||
@@ -423,13 +423,20 @@ async fn run_command_under_sandbox(
|
||||
.codex_linux_sandbox_exe
|
||||
.expect("codex-linux-sandbox executable not found");
|
||||
let network_sandbox_policy = runtime_permission_profile.network_sandbox_policy();
|
||||
let (env, managed_network) = if let Some(network) = network.as_ref() {
|
||||
let prepared =
|
||||
network.prepare_for_optional_environment(env, /*environment_id*/ None)?;
|
||||
(prepared.env, Some(prepared.sandbox_context))
|
||||
} else {
|
||||
(env, None)
|
||||
};
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
command,
|
||||
cwd.as_path(),
|
||||
&runtime_permission_profile,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(enforce_managed_network),
|
||||
managed_network.as_ref(),
|
||||
);
|
||||
spawn_debug_sandbox_child(
|
||||
codex_linux_sandbox_exe,
|
||||
@@ -438,11 +445,7 @@ async fn run_command_under_sandbox(
|
||||
cwd.to_path_buf(),
|
||||
network_sandbox_policy,
|
||||
env,
|
||||
|env_map| {
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env(env_map);
|
||||
}
|
||||
},
|
||||
|_| {},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
@@ -336,6 +336,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
..Default::default()
|
||||
};
|
||||
let command = || SandboxCommand {
|
||||
program: "/bin/bash".into(),
|
||||
|
||||
@@ -168,6 +168,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
..Default::default()
|
||||
};
|
||||
let mut request = ExecRequest {
|
||||
command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
|
||||
@@ -962,6 +962,8 @@ mod tests {
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 48081],
|
||||
allow_local_binding: false,
|
||||
allow_unix_sockets: vec!["/tmp/allowed.sock".to_string()],
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
}),
|
||||
network_proxy: Some(
|
||||
RemoteNetworkProxyLaunchConfig::new(
|
||||
@@ -990,6 +992,8 @@ mod tests {
|
||||
serde_json::json!({
|
||||
"loopbackPorts": [43123, 48081],
|
||||
"allowLocalBinding": false,
|
||||
"allowUnixSockets": ["/tmp/allowed.sock"],
|
||||
"dangerouslyAllowAllUnixSockets": true,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1023,6 +1027,52 @@ mod tests {
|
||||
assert!(legacy_serialized.get("metadata").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_params_defaults_legacy_managed_network_unix_socket_policy() {
|
||||
let cwd =
|
||||
PathUri::from_host_native_path(std::env::current_dir().expect("current directory"))
|
||||
.expect("cwd URI");
|
||||
let legacy: ExecParams = serde_json::from_value(serde_json::json!({
|
||||
"processId": "legacy-managed-network",
|
||||
"argv": ["true"],
|
||||
"cwd": cwd,
|
||||
"env": {},
|
||||
"tty": false,
|
||||
"arg0": null,
|
||||
"enforceManagedNetwork": true,
|
||||
"managedNetwork": {
|
||||
"loopbackPorts": [43123],
|
||||
"allowLocalBinding": true,
|
||||
},
|
||||
}))
|
||||
.expect("deserialize legacy managed network context");
|
||||
|
||||
assert_eq!(
|
||||
legacy,
|
||||
ExecParams {
|
||||
process_id: ProcessId::from("legacy-managed-network"),
|
||||
metadata: None,
|
||||
argv: vec!["true".to_string()],
|
||||
cwd,
|
||||
env_policy: None,
|
||||
shell_snapshot: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: true,
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: true,
|
||||
allow_unix_sockets: Vec::new(),
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
}),
|
||||
network_proxy: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_info_accepts_legacy_response_without_cwd() {
|
||||
let info: EnvironmentInfo = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -240,6 +240,7 @@ async fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
..Default::default()
|
||||
}),
|
||||
network_proxy: None,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
@@ -43,15 +44,15 @@ pub(crate) fn apply_permission_profile_to_current_thread(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
apply_landlock_fs: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_routed_network: bool,
|
||||
managed_network: Option<&ManagedNetworkSandboxContext>,
|
||||
proxy_routing_active: bool,
|
||||
) -> Result<()> {
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let network_seccomp_mode = network_seccomp_mode(
|
||||
network_sandbox_policy,
|
||||
allow_network_for_proxy,
|
||||
proxy_routed_network,
|
||||
managed_network.is_some(),
|
||||
proxy_routing_active,
|
||||
)
|
||||
.or_else(|| {
|
||||
// VM sockets can reach host services outside the filesystem sandbox.
|
||||
@@ -73,7 +74,7 @@ pub(crate) fn apply_permission_profile_to_current_thread(
|
||||
}
|
||||
|
||||
if let Some(mode) = network_seccomp_mode {
|
||||
install_network_seccomp_filter_on_current_thread(mode)?;
|
||||
install_network_seccomp_filter_on_current_thread(mode, managed_network)?;
|
||||
}
|
||||
|
||||
if apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
@@ -177,6 +178,7 @@ fn install_filesystem_landlock_rules_on_current_thread(
|
||||
/// inherits it.
|
||||
fn install_network_seccomp_filter_on_current_thread(
|
||||
mode: NetworkSeccompMode,
|
||||
managed_network: Option<&ManagedNetworkSandboxContext>,
|
||||
) -> std::result::Result<(), SandboxErr> {
|
||||
fn deny_syscall(rules: &mut BTreeMap<i64, Vec<SeccompRule>>, nr: i64) {
|
||||
rules.insert(nr, vec![]); // empty rule vec = unconditional match
|
||||
@@ -230,12 +232,10 @@ fn install_network_seccomp_filter_on_current_thread(
|
||||
}
|
||||
NetworkSeccompMode::ProxyRouted => {
|
||||
// In proxy-routed mode we allow IP sockets in the isolated
|
||||
// namespace (used to reach the local TCP bridge) but deny socket()
|
||||
// for all other families, including AF_UNIX. Only AF_UNIX
|
||||
// socketpair() remains available for process-local IPC because it
|
||||
// cannot connect to a socket outside the sandbox or bypass the
|
||||
// bridge.
|
||||
let deny_non_ip_socket = SeccompRule::new(vec![
|
||||
// namespace (used to reach the local TCP bridge). Standalone Unix
|
||||
// sockets require an explicit managed-policy grant; all other
|
||||
// socket families remain denied.
|
||||
let mut denied_socket_conditions = vec![
|
||||
SeccompCondition::new(
|
||||
0,
|
||||
SeccompCmpArgLen::Dword,
|
||||
@@ -248,7 +248,16 @@ fn install_network_seccomp_filter_on_current_thread(
|
||||
SeccompCmpOp::Ne,
|
||||
libc::AF_INET6 as u64,
|
||||
)?,
|
||||
])?;
|
||||
];
|
||||
if managed_network.is_some_and(|context| context.dangerously_allow_all_unix_sockets) {
|
||||
denied_socket_conditions.push(SeccompCondition::new(
|
||||
0,
|
||||
SeccompCmpArgLen::Dword,
|
||||
SeccompCmpOp::Ne,
|
||||
libc::AF_UNIX as u64,
|
||||
)?);
|
||||
}
|
||||
let deny_non_ip_socket = SeccompRule::new(denied_socket_conditions)?;
|
||||
let deny_non_unix_socketpair = SeccompRule::new(vec![SeccompCondition::new(
|
||||
0,
|
||||
SeccompCmpArgLen::Dword,
|
||||
|
||||
@@ -29,6 +29,7 @@ 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 codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::FileSystemAccessMode;
|
||||
@@ -123,13 +124,13 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)]
|
||||
pub apply_seccomp_then_exec: bool,
|
||||
|
||||
/// Internal compatibility flag.
|
||||
///
|
||||
/// By default, restricted-network sandboxing uses isolated networking.
|
||||
/// If set, sandbox setup switches to proxy-only network mode with
|
||||
/// managed routing bridges.
|
||||
#[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)]
|
||||
pub allow_network_for_proxy: bool,
|
||||
/// Effective managed-network policy prepared for this command launch.
|
||||
#[arg(
|
||||
long,
|
||||
hide = true,
|
||||
value_parser = parse_managed_network
|
||||
)]
|
||||
pub managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
|
||||
/// Internal route spec used for managed proxy routing in bwrap mode.
|
||||
#[arg(long = "proxy-route-spec", hide = true)]
|
||||
@@ -164,12 +165,13 @@ pub fn run_main() -> ! {
|
||||
permission_profile,
|
||||
use_legacy_landlock,
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
managed_network,
|
||||
proxy_route_spec,
|
||||
verify_fd_mounts,
|
||||
no_proc,
|
||||
command,
|
||||
} = LandlockCommand::parse();
|
||||
let allow_network_for_proxy = managed_network.is_some();
|
||||
|
||||
if command.is_empty() {
|
||||
panic!("No command specified to execute.");
|
||||
@@ -236,7 +238,7 @@ pub fn run_main() -> ! {
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
/*apply_landlock_fs*/ false,
|
||||
allow_network_for_proxy,
|
||||
managed_network.as_ref(),
|
||||
proxy_routing_active,
|
||||
) {
|
||||
panic!("error applying Linux sandbox restrictions: {e:?}");
|
||||
@@ -282,8 +284,8 @@ pub fn run_main() -> ! {
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
/*apply_landlock_fs*/ false,
|
||||
allow_network_for_proxy,
|
||||
/*proxy_routed_network*/ false,
|
||||
managed_network.as_ref(),
|
||||
/*proxy_routing_active*/ false,
|
||||
) {
|
||||
panic!("error applying Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
@@ -305,7 +307,7 @@ pub fn run_main() -> ! {
|
||||
sandbox_policy_cwd: &sandbox_policy_cwd,
|
||||
command_cwd: command_cwd.as_deref(),
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy,
|
||||
managed_network,
|
||||
proxy_route_spec,
|
||||
command,
|
||||
});
|
||||
@@ -325,8 +327,8 @@ pub fn run_main() -> ! {
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
/*apply_landlock_fs*/ true,
|
||||
allow_network_for_proxy,
|
||||
/*proxy_routed_network*/ false,
|
||||
managed_network.as_ref(),
|
||||
/*proxy_routing_active*/ false,
|
||||
) {
|
||||
panic!("error applying legacy Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
@@ -357,6 +359,11 @@ fn parse_permission_profile(value: &str) -> std::result::Result<PermissionProfil
|
||||
serde_json::from_str(value).map_err(|err| format!("invalid permission profile JSON: {err}"))
|
||||
}
|
||||
|
||||
fn parse_managed_network(value: &str) -> std::result::Result<ManagedNetworkSandboxContext, String> {
|
||||
serde_json::from_str(value)
|
||||
.map_err(|err| format!("invalid managed network context JSON: {err}"))
|
||||
}
|
||||
|
||||
fn resolve_permission_profile(
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
) -> Result<EffectivePermissions, ResolvePermissionProfileError> {
|
||||
@@ -1521,7 +1528,7 @@ struct InnerSeccompCommandArgs<'a> {
|
||||
sandbox_policy_cwd: &'a Path,
|
||||
command_cwd: Option<&'a Path>,
|
||||
permission_profile: &'a PermissionProfile,
|
||||
allow_network_for_proxy: bool,
|
||||
managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
proxy_route_spec: Option<String>,
|
||||
command: Vec<String>,
|
||||
}
|
||||
@@ -1532,7 +1539,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
permission_profile,
|
||||
allow_network_for_proxy,
|
||||
managed_network,
|
||||
proxy_route_spec,
|
||||
command,
|
||||
} = args;
|
||||
@@ -1559,8 +1566,12 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
permission_profile_json,
|
||||
"--apply-seccomp-then-exec".to_string(),
|
||||
]);
|
||||
if allow_network_for_proxy {
|
||||
inner.push("--allow-network-for-proxy".to_string());
|
||||
if let Some(managed_network) = managed_network {
|
||||
inner.push("--managed-network".to_string());
|
||||
inner.push(
|
||||
serde_json::to_string(&managed_network)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize managed network context: {err}")),
|
||||
);
|
||||
let proxy_route_spec = proxy_route_spec
|
||||
.unwrap_or_else(|| panic!("managed proxy mode requires a proxy route spec"));
|
||||
inner.push("--proxy-route-spec".to_string());
|
||||
|
||||
@@ -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 codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
@@ -553,17 +555,63 @@ fn run_bwrap_signal_forwarder_test_supervisor() -> ! {
|
||||
#[test]
|
||||
fn managed_proxy_inner_command_includes_route_spec() {
|
||||
let permission_profile = read_only_permission_profile();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: Some("{\"routes\":[]}".to_string()),
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
for managed_network in [
|
||||
ManagedNetworkSandboxContext::default(),
|
||||
ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![8080, 9090],
|
||||
allow_local_binding: true,
|
||||
allow_unix_sockets: vec!["/tmp/daemon.sock".to_string()],
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
},
|
||||
] {
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
permission_profile: &permission_profile,
|
||||
managed_network: Some(managed_network.clone()),
|
||||
proxy_route_spec: Some("{\"routes\":[]}".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.iter().any(|arg| arg == "--proxy-route-spec"));
|
||||
assert!(args.iter().any(|arg| arg == "{\"routes\":[]}"));
|
||||
let parsed = LandlockCommand::try_parse_from(args)
|
||||
.expect("inner command should preserve the managed network policy");
|
||||
assert_eq!(parsed.managed_network, Some(managed_network));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_network_policy_alone_enables_proxy_mode() {
|
||||
let parsed = LandlockCommand::try_parse_from([
|
||||
"codex-linux-sandbox",
|
||||
"--sandbox-policy-cwd",
|
||||
"/tmp",
|
||||
"--managed-network",
|
||||
"{}",
|
||||
"--",
|
||||
"/bin/true",
|
||||
])
|
||||
.expect("managed network context should enable proxy mode on its own");
|
||||
assert_eq!(
|
||||
parsed.managed_network,
|
||||
Some(ManagedNetworkSandboxContext::default())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_managed_network_policy_is_rejected() {
|
||||
let error = LandlockCommand::try_parse_from([
|
||||
"codex-linux-sandbox",
|
||||
"--sandbox-policy-cwd",
|
||||
"/tmp",
|
||||
"--managed-network",
|
||||
"{\"dangerouslyAllowAllUnixSockets\":\"true\"}",
|
||||
"--",
|
||||
"/bin/true",
|
||||
])
|
||||
.expect_err("managed network policy should reject a non-boolean grant");
|
||||
assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -573,7 +621,7 @@ fn inner_command_includes_permission_profile_flag() {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: false,
|
||||
managed_network: None,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
@@ -592,12 +640,15 @@ fn non_managed_inner_command_omits_route_spec() {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: false,
|
||||
managed_network: None,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
|
||||
assert!(!args.iter().any(|arg| arg == "--proxy-route-spec"));
|
||||
let parsed = LandlockCommand::try_parse_from(args)
|
||||
.expect("unmanaged inner command should preserve ordinary sandbox mode");
|
||||
assert_eq!(parsed.managed_network, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -608,7 +659,7 @@ fn managed_proxy_inner_command_requires_route_spec() {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: true,
|
||||
managed_network: Some(ManagedNetworkSandboxContext::default()),
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
})
|
||||
|
||||
@@ -36,6 +36,9 @@ use tempfile::NamedTempFile;
|
||||
use tokio::process::Command;
|
||||
use url::Url;
|
||||
|
||||
#[path = "managed_proxy_unix_sockets_tests.rs"]
|
||||
mod unix_sockets;
|
||||
|
||||
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable: no system bwrap was found";
|
||||
const NETWORK_TIMEOUT_MS: u64 = 4_000;
|
||||
const OPERATION_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 8);
|
||||
@@ -171,7 +174,8 @@ fn linux_sandbox_command(
|
||||
permission_profile_json,
|
||||
];
|
||||
if allow_network_for_proxy {
|
||||
args.push("--allow-network-for-proxy".to_string());
|
||||
args.push("--managed-network".to_string());
|
||||
args.push("{}".to_string());
|
||||
}
|
||||
args.push("--".to_string());
|
||||
args.extend(command.iter().map(|entry| (*entry).to_string()));
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Explicit Unix-socket grants preserve managed-proxy network isolation.
|
||||
|
||||
use super::*;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigReloaderFuture;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct TestConfigReloader(ConfigState);
|
||||
|
||||
impl ConfigReloader for TestConfigReloader {
|
||||
fn source_label(&self) -> String {
|
||||
"managed proxy Unix socket test".to_string()
|
||||
}
|
||||
|
||||
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
|
||||
Box::pin(async { Ok(None) })
|
||||
}
|
||||
|
||||
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
|
||||
Box::pin(async { Ok(self.0.clone()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_proxy_allow_all_unix_sockets_preserves_network_isolation() {
|
||||
if let Some(reason) = managed_proxy_skip_reason().await {
|
||||
eprintln!("skipping managed proxy Unix socket test: {reason}");
|
||||
return;
|
||||
}
|
||||
if !Command::new("python3")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.is_ok_and(|output| output.status.success())
|
||||
{
|
||||
eprintln!("skipping managed proxy Unix socket test: python3 is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let temp = tempfile::tempdir().expect("socket directory");
|
||||
let socket_path = temp.path().join("daemon.sock");
|
||||
let listener = UnixListener::bind(&socket_path).expect("host Unix listener");
|
||||
let mut control = UnixStream::connect(&socket_path).expect("host Unix control connection");
|
||||
let (mut control_peer, _) = listener.accept().expect("accept host Unix control");
|
||||
control.write_all(b"ok").expect("write host control");
|
||||
let mut control_payload = [0; 2];
|
||||
control_peer
|
||||
.read_exact(&mut control_payload)
|
||||
.expect("read host control");
|
||||
assert_eq!(control_payload, *b"ok");
|
||||
|
||||
// Use another loopback address so the namespace's proxy listener cannot
|
||||
// collide with this host-only endpoint even if it selects the same port.
|
||||
let tcp_listener =
|
||||
TcpListener::bind((Ipv4Addr::new(127, 0, 0, 2), 0)).expect("host TCP listener");
|
||||
let tcp_addr = tcp_listener.local_addr().expect("host TCP address");
|
||||
let _tcp_control = TcpStream::connect(tcp_addr).expect("host TCP control connection");
|
||||
let _tcp_peer = tcp_listener.accept().expect("accept host TCP control");
|
||||
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("nonblocking listener");
|
||||
let server = std::thread::spawn(move || {
|
||||
let deadline = Instant::now() + OPERATION_TIMEOUT * 2;
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((mut stream, _)) => {
|
||||
stream
|
||||
.set_read_timeout(Some(OPERATION_TIMEOUT))
|
||||
.expect("set Unix read timeout");
|
||||
let mut request = [0; 4];
|
||||
stream.read_exact(&mut request).expect("read Unix request");
|
||||
assert_eq!(request, *b"ping");
|
||||
stream.write_all(b"pong").expect("write Unix response");
|
||||
break;
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
assert!(Instant::now() < deadline, "Unix request timed out");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(error) => panic!("accept Unix request: {error}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let probe = r#"
|
||||
import errno, socket, sys
|
||||
path, port, mode = sys.argv[1:]
|
||||
try:
|
||||
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
except OSError as error:
|
||||
assert mode == 'deny' and error.errno == errno.EPERM, error
|
||||
else:
|
||||
assert mode == 'allow', 'Unix socket unexpectedly allowed'
|
||||
with client:
|
||||
client.settimeout(2)
|
||||
client.connect(path)
|
||||
client.sendall(b'ping')
|
||||
assert client.recv(4, socket.MSG_WAITALL) == b'pong'
|
||||
for address in [('127.0.0.2', int(port)), ('192.0.2.1', 80)]:
|
||||
try:
|
||||
connection = socket.create_connection(address, timeout=0.5)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
connection.close()
|
||||
raise AssertionError(('direct network unexpectedly allowed', address))
|
||||
for family in (socket.AF_NETLINK, getattr(socket, 'AF_VSOCK', 40)):
|
||||
try:
|
||||
unexpected = socket.socket(family, socket.SOCK_STREAM)
|
||||
except OSError as error:
|
||||
assert error.errno == errno.EPERM, (family, error)
|
||||
else:
|
||||
unexpected.close()
|
||||
raise AssertionError(('socket family unexpectedly allowed', family))
|
||||
"#;
|
||||
let cwd = std::env::current_dir().expect("current directory");
|
||||
for (label, dangerously_allow_all_unix_sockets, allow_path) in [
|
||||
("default denial", false, false),
|
||||
("path grant stays restricted", false, true),
|
||||
("explicit allow-all", true, false),
|
||||
] {
|
||||
let mut env = create_env_from_core_vars();
|
||||
strip_proxy_env(&mut env);
|
||||
let mut config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
proxy_url: "http://127.0.0.1:9".to_string(),
|
||||
dangerously_allow_all_unix_sockets,
|
||||
..Default::default()
|
||||
};
|
||||
if allow_path {
|
||||
config.set_allow_unix_sockets(vec![socket_path.to_string_lossy().into_owned()]);
|
||||
}
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default())
|
||||
.expect("valid managed network configuration");
|
||||
let network = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
state.clone(),
|
||||
Arc::new(TestConfigReloader(state)),
|
||||
)))
|
||||
.managed_by_codex(/*managed_by_codex*/ false)
|
||||
.build()
|
||||
.await
|
||||
.expect("build managed network proxy");
|
||||
let prepared = network
|
||||
.prepare_for_optional_environment(env, /*environment_id*/ None)
|
||||
.expect("prepare managed network policy");
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_codex-linux-sandbox"));
|
||||
command
|
||||
.arg("--sandbox-policy-cwd")
|
||||
.arg(&cwd)
|
||||
.arg("--permission-profile")
|
||||
.arg(serde_json::to_string(&PermissionProfile::Disabled).unwrap())
|
||||
.arg("--managed-network")
|
||||
.arg(serde_json::to_string(&prepared.sandbox_context).unwrap())
|
||||
.args(["--", "python3", "-c", probe])
|
||||
.arg(&socket_path)
|
||||
.arg(tcp_addr.port().to_string())
|
||||
.arg(if dangerously_allow_all_unix_sockets {
|
||||
"allow"
|
||||
} else {
|
||||
"deny"
|
||||
})
|
||||
.env_clear()
|
||||
.envs(prepared.env)
|
||||
.kill_on_drop(true);
|
||||
let output = tokio::time::timeout(OPERATION_TIMEOUT, command.output())
|
||||
.await
|
||||
.expect("Unix socket probe timed out")
|
||||
.expect("Unix socket probe should execute");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{label}; stderr={}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
server.join().expect("Unix server should finish");
|
||||
}
|
||||
@@ -411,6 +411,7 @@ fn managed_network_allows_authorized_loopback_without_lan_or_dns_access() -> Res
|
||||
let proxy = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 43124],
|
||||
allow_local_binding: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut env = HashMap::new();
|
||||
create_command_args(CreateMxcCommandArgsParams {
|
||||
@@ -466,6 +467,7 @@ fn invalid_managed_network_is_rejected_at_both_boundaries() -> Result<()> {
|
||||
let proxy = ManagedNetworkSandboxContext {
|
||||
loopback_ports,
|
||||
allow_local_binding,
|
||||
..Default::default()
|
||||
};
|
||||
let mut env = HashMap::new();
|
||||
assert!(
|
||||
|
||||
@@ -483,6 +483,12 @@ pub struct ManagedNetworkSandboxContext {
|
||||
/// Whether the command may bind local sockets and exchange loopback traffic.
|
||||
#[serde(default)]
|
||||
pub allow_local_binding: bool,
|
||||
/// Unix-domain socket paths allowed by the effective managed-network policy.
|
||||
#[serde(default)]
|
||||
pub allow_unix_sockets: Vec<String>,
|
||||
/// Whether the effective policy permits connections to all Unix-domain sockets.
|
||||
#[serde(default)]
|
||||
pub dangerously_allow_all_unix_sockets: bool,
|
||||
}
|
||||
|
||||
/// Environment-specific managed-network settings prepared for one command launch.
|
||||
@@ -1133,6 +1139,9 @@ impl NetworkProxy {
|
||||
sandbox_context: ManagedNetworkSandboxContext {
|
||||
loopback_ports,
|
||||
allow_local_binding: runtime_settings.allow_local_binding,
|
||||
allow_unix_sockets: runtime_settings.allow_unix_sockets.to_vec(),
|
||||
dangerously_allow_all_unix_sockets: runtime_settings
|
||||
.dangerously_allow_all_unix_sockets,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2109,6 +2118,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepare_for_optional_environment_preserves_effective_unix_socket_permissions()
|
||||
-> Result<()> {
|
||||
let mut config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
proxy_url: "http://127.0.0.1:43128".to_string(),
|
||||
socks_url: "http://127.0.0.1:48081".to_string(),
|
||||
allow_local_binding: true,
|
||||
unix_sockets: Some(crate::config::NetworkUnixSocketPermissions {
|
||||
entries: [
|
||||
(
|
||||
"/tmp/allowed.sock".to_string(),
|
||||
crate::config::NetworkUnixSocketPermission::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/denied.sock".to_string(),
|
||||
crate::config::NetworkUnixSocketPermission::Deny,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
let proxy = NetworkProxy::builder()
|
||||
.state(Arc::new(network_proxy_state_for_policy(config.clone())))
|
||||
.managed_by_codex(/*managed_by_codex*/ false)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
for allow_all in [false, true] {
|
||||
config.dangerously_allow_all_unix_sockets = allow_all;
|
||||
let replacement = crate::state::build_config_state(config.clone(), Default::default())?;
|
||||
proxy.replace_config_state(replacement).await?;
|
||||
let prepared = proxy
|
||||
.prepare_for_optional_environment(HashMap::new(), /*environment_id*/ None)?;
|
||||
assert_eq!(
|
||||
prepared.sandbox_context,
|
||||
ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43128, 48081],
|
||||
allow_local_binding: true,
|
||||
allow_unix_sockets: if cfg!(target_os = "windows") {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec!["/tmp/allowed.sock".to_string()]
|
||||
},
|
||||
dangerously_allow_all_unix_sockets: !cfg!(target_os = "windows") && allow_all,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepare_for_environment_keeps_env_and_sandbox_ports_in_sync() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -2173,6 +2236,7 @@ mod tests {
|
||||
ManagedNetworkSandboxContext {
|
||||
loopback_ports: expected_ports,
|
||||
allow_local_binding: false,
|
||||
..ManagedNetworkSandboxContext::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ async fn dedicated_listeners_preserve_http_and_socks_policy_without_restricted_t
|
||||
ManagedNetworkSandboxContext {
|
||||
loopback_ports: ports,
|
||||
allow_local_binding: false,
|
||||
..ManagedNetworkSandboxContext::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -15,18 +16,20 @@ pub fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
|
||||
/// Converts the permission profile into the CLI invocation for
|
||||
/// `codex-linux-sandbox`.
|
||||
///
|
||||
/// A managed context selects proxy isolation; a default context keeps its
|
||||
/// restrictive policy when an older caller supplies no additional details.
|
||||
///
|
||||
/// The helper performs the actual sandboxing (bubblewrap by default + seccomp)
|
||||
/// after parsing these arguments. The profile JSON flag is emitted before
|
||||
/// helper feature flags so the argv order matches the helper's CLI shape. See
|
||||
/// `docs/linux_sandbox.md` for the Linux semantics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_linux_sandbox_command_args_for_permission_profile(
|
||||
command: Vec<String>,
|
||||
command_cwd: &Path,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_legacy_landlock: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
managed_network: Option<&ManagedNetworkSandboxContext>,
|
||||
) -> Vec<String> {
|
||||
let permission_profile_json = serde_json::to_string(permission_profile)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize permission profile: {err}"));
|
||||
@@ -48,11 +51,15 @@ pub fn create_linux_sandbox_command_args_for_permission_profile(
|
||||
permission_profile_json,
|
||||
];
|
||||
// Proxy-only networking requires bubblewrap's isolated network namespace.
|
||||
if use_legacy_landlock && !allow_network_for_proxy {
|
||||
if use_legacy_landlock && managed_network.is_none() {
|
||||
linux_cmd.push("--use-legacy-landlock".to_string());
|
||||
}
|
||||
if allow_network_for_proxy {
|
||||
linux_cmd.push("--allow-network-for-proxy".to_string());
|
||||
if let Some(managed_network) = managed_network {
|
||||
linux_cmd.push("--managed-network".to_string());
|
||||
linux_cmd.push(
|
||||
serde_json::to_string(managed_network)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize managed network context: {err}")),
|
||||
);
|
||||
}
|
||||
linux_cmd.push("--".to_string());
|
||||
linux_cmd.extend(command);
|
||||
@@ -89,7 +96,8 @@ fn create_linux_sandbox_command_args(
|
||||
linux_cmd.push("--use-legacy-landlock".to_string());
|
||||
}
|
||||
if allow_network_for_proxy {
|
||||
linux_cmd.push("--allow-network-for-proxy".to_string());
|
||||
linux_cmd.push("--managed-network".to_string());
|
||||
linux_cmd.push("{}".to_string());
|
||||
}
|
||||
|
||||
// Separator so that command arguments starting with `-` are not parsed as
|
||||
|
||||
@@ -33,7 +33,7 @@ fn legacy_landlock_flag_is_included_when_requested() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_flag_takes_precedence_over_legacy_landlock() {
|
||||
fn managed_context_takes_precedence_over_legacy_landlock() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let command_cwd = Path::new("/tmp/link");
|
||||
let cwd = Path::new("/tmp");
|
||||
@@ -45,12 +45,9 @@ fn proxy_flag_takes_precedence_over_legacy_landlock() {
|
||||
&permission_profile,
|
||||
cwd,
|
||||
/*use_legacy_landlock*/ true,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
);
|
||||
assert_eq!(
|
||||
args.contains(&"--allow-network-for-proxy".to_string()),
|
||||
true
|
||||
Some(&ManagedNetworkSandboxContext::default()),
|
||||
);
|
||||
assert_eq!(args.contains(&"--managed-network".to_string()), true);
|
||||
assert_eq!(args.contains(&"--use-legacy-landlock".to_string()), false);
|
||||
}
|
||||
|
||||
@@ -67,7 +64,7 @@ fn permission_profile_flag_is_included() {
|
||||
&permission_profile,
|
||||
cwd,
|
||||
/*use_legacy_landlock*/ true,
|
||||
/*allow_network_for_proxy*/ false,
|
||||
/*managed_network*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::bwrap::WSL1_BWRAP_WARNING;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::bwrap::is_wsl1;
|
||||
use crate::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use crate::landlock::allow_network_for_proxy;
|
||||
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
use crate::policy_transforms::effective_permission_profile;
|
||||
use crate::policy_transforms::should_require_platform_sandbox;
|
||||
@@ -496,14 +495,30 @@ impl SandboxManager {
|
||||
let pending = pending_sandboxed_request?;
|
||||
let exe =
|
||||
sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
|
||||
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
|
||||
if enforce_managed_network
|
||||
&& command.managed_network.is_none()
|
||||
&& let Some(network) = network
|
||||
{
|
||||
let prepared = network
|
||||
.prepare_for_optional_environment(
|
||||
std::mem::take(&mut command.env),
|
||||
environment_id,
|
||||
)
|
||||
.map_err(|err| {
|
||||
SandboxTransformError::EnvironmentNetworkProxy(err.to_string())
|
||||
})?;
|
||||
command.env = prepared.env;
|
||||
command.managed_network = Some(prepared.sandbox_context);
|
||||
}
|
||||
let managed_network =
|
||||
enforce_managed_network.then(|| command.managed_network.unwrap_or_default());
|
||||
#[cfg(target_os = "linux")]
|
||||
ensure_linux_bubblewrap_is_supported(
|
||||
&pending
|
||||
.effective_permission_profile
|
||||
.file_system_sandbox_policy(),
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
managed_network.is_some(),
|
||||
is_wsl1(),
|
||||
)?;
|
||||
let mut args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
@@ -512,7 +527,7 @@ impl SandboxManager {
|
||||
&pending.effective_permission_profile,
|
||||
pending.native_sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
managed_network.as_ref(),
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
|
||||
|
||||
@@ -530,6 +530,131 @@ fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path()
|
||||
assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string()));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn linux_unix_socket_grant_uses_effective_managed_policy() -> anyhow::Result<()> {
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigReloaderFuture;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct TestConfigReloader;
|
||||
impl ConfigReloader for TestConfigReloader {
|
||||
fn source_label(&self) -> String {
|
||||
"sandbox manager test config".to_string()
|
||||
}
|
||||
|
||||
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
|
||||
Box::pin(async { Ok(None) })
|
||||
}
|
||||
|
||||
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
|
||||
Box::pin(async { Err(anyhow::anyhow!("test config cannot reload")) })
|
||||
}
|
||||
}
|
||||
|
||||
let state = build_config_state(
|
||||
NetworkProxyConfig {
|
||||
enabled: true,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..Default::default()
|
||||
},
|
||||
NetworkProxyConstraints::default(),
|
||||
)?;
|
||||
let network = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
state,
|
||||
Arc::new(TestConfigReloader),
|
||||
)))
|
||||
.managed_by_codex(/*managed_by_codex*/ false)
|
||||
.build()
|
||||
.await?;
|
||||
let prepared =
|
||||
network.prepare_for_optional_environment(HashMap::new(), /*environment_id*/ None)?;
|
||||
let allow_all = prepared.sandbox_context;
|
||||
let path_only = ManagedNetworkSandboxContext {
|
||||
allow_unix_sockets: vec!["/tmp/daemon.sock".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let denied = ManagedNetworkSandboxContext::default();
|
||||
let cwd = AbsolutePathBuf::current_dir()?;
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let manager = SandboxManager::new();
|
||||
for (label, context, live_proxy, enforce_managed_network, expected) in [
|
||||
(
|
||||
"missing policy stays managed with restrictive defaults",
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
Some(denied.clone()),
|
||||
),
|
||||
(
|
||||
"path grant stays restricted",
|
||||
Some(path_only.clone()),
|
||||
false,
|
||||
true,
|
||||
Some(path_only),
|
||||
),
|
||||
(
|
||||
"prepared allow-all",
|
||||
Some(allow_all.clone()),
|
||||
false,
|
||||
true,
|
||||
Some(allow_all.clone()),
|
||||
),
|
||||
(
|
||||
"prepared denial overrides live allow-all",
|
||||
Some(denied.clone()),
|
||||
true,
|
||||
true,
|
||||
Some(denied),
|
||||
),
|
||||
(
|
||||
"live proxy fallback",
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
Some(allow_all.clone()),
|
||||
),
|
||||
("no managed network", Some(allow_all), true, false, None),
|
||||
] {
|
||||
let request = manager.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd_uri.clone(),
|
||||
env: HashMap::new(),
|
||||
managed_network: context,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &PermissionProfile::Disabled,
|
||||
sandbox: SandboxType::LinuxSeccomp,
|
||||
enforce_managed_network,
|
||||
environment_id: None,
|
||||
network: live_proxy.then_some(&network),
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
sandbox_exe: Some(std::path::Path::new("/tmp/codex-linux-sandbox")),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})?;
|
||||
let transported_context = request
|
||||
.command
|
||||
.windows(2)
|
||||
.find(|args| args[0] == "--managed-network")
|
||||
.map(|args| serde_json::from_str::<ManagedNetworkSandboxContext>(&args[1]))
|
||||
.transpose()?;
|
||||
assert_eq!(transported_context, expected, "{label}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn transform_for_direct_spawn_windows_preserves_only_wrapper_setup_environment() {
|
||||
|
||||
@@ -777,6 +777,7 @@ fn prepared_managed_network_context_allows_only_its_proxy_ports() {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 48081],
|
||||
allow_local_binding: false,
|
||||
..Default::default()
|
||||
};
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/bin/true".to_string()],
|
||||
|
||||
Reference in New Issue
Block a user