mirror of
https://github.com/openai/codex.git
synced 2026-09-28 08:43:01 +08:00
## Why Private IP destinations always bypassed inherited upstream proxies, preventing their use for private networks reachable through an upstream VPN proxy. ## What changed - Add `codex exec-server --proxy-private-ips-via-upstream`, also configurable with `CODEX_EXEC_SERVER_PROXY_PRIVATE_IPS_VIA_UPSTREAM=true`. The setting defaults to disabled. - Allow permitted RFC 1918, carrier-grade NAT, and IPv6 unique-local destinations to use an applicable upstream proxy. Loopback and link-local destinations retain direct routing, and destination access policy still applies. - Keep connections direct when no valid upstream proxy applies or `allow_upstream_proxy=false`. Errors after selecting an upstream proxy do not trigger a direct retry. - Rename `ExecServerRuntimePaths` to `ExecServerRuntimeOptions` and carry the routing setting from executor startup into the managed network proxy. ## Testing Add routing coverage for private address ranges, special-use addresses, and public targets with the option enabled and disabled. Verify that HTTP and CONNECT requests still enforce destination allowlists and denylists, and update the CLI help snapshot. GitOrigin-RevId: b7c9cc7da0e0f545694a6521b74c9b36b7b92769
194 lines
8.2 KiB
Rust
194 lines
8.2 KiB
Rust
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use test_case::test_case;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum SnapshotSandbox {
|
|
None,
|
|
WorkspaceWrite,
|
|
DenyFdPath,
|
|
}
|
|
|
|
#[test_case("bash", false, SnapshotSandbox::None; "bash")]
|
|
#[test_case("bash", true, SnapshotSandbox::None; "bash_tty")]
|
|
#[cfg_attr(target_os = "macos", test_case("zsh", false, SnapshotSandbox::None; "zsh"))]
|
|
#[cfg_attr(target_os = "macos", test_case("zsh", true, SnapshotSandbox::None; "zsh_tty"))]
|
|
#[test_case("bash", false, SnapshotSandbox::DenyFdPath; "blocked_descriptor_path")]
|
|
#[cfg_attr(target_os = "macos", test_case("zsh", false, SnapshotSandbox::DenyFdPath; "zsh_blocked_descriptor_path"))]
|
|
#[cfg_attr(target_os = "macos", test_case("zsh", true, SnapshotSandbox::DenyFdPath; "zsh_blocked_descriptor_path_tty"))]
|
|
#[test_case("bash", false, SnapshotSandbox::WorkspaceWrite; "bash_protected_transport")]
|
|
#[test_case("bash", true, SnapshotSandbox::WorkspaceWrite; "bash_protected_transport_tty")]
|
|
#[cfg_attr(target_os = "macos", test_case("zsh", false, SnapshotSandbox::WorkspaceWrite; "zsh_protected_transport"))]
|
|
#[cfg_attr(target_os = "macos", test_case("zsh", true, SnapshotSandbox::WorkspaceWrite; "zsh_protected_transport_tty"))]
|
|
#[serial_test::serial(remote_exec_server)]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn shell_snapshot_concurrent_replays_keep_independent_readers(
|
|
shell: &str,
|
|
tty: bool,
|
|
sandbox_mode: SnapshotSandbox,
|
|
) -> Result<()> {
|
|
let deny_fd_path = sandbox_mode == SnapshotSandbox::DenyFdPath;
|
|
let use_sandbox = sandbox_mode != SnapshotSandbox::None;
|
|
if use_sandbox
|
|
&& let Some(warning) =
|
|
codex_sandboxing::system_bwrap_warning(&PermissionProfile::read_only())
|
|
{
|
|
eprintln!("skipping sandbox test: {warning}");
|
|
return Ok(());
|
|
}
|
|
let context = if sandbox_mode == SnapshotSandbox::WorkspaceWrite {
|
|
let (exe, linux_sandbox) = current_test_binary_helper_paths()?;
|
|
let environment = Environment::create(
|
|
/*exec_server_url*/ None,
|
|
codex_exec_server::ExecServerRuntimeOptions::new(exe, linux_sandbox)?,
|
|
codex_http_client::HttpClientFactory::new(
|
|
codex_http_client::OutboundProxyPolicy::ReqwestDefault,
|
|
),
|
|
)?;
|
|
ProcessContext {
|
|
backend: environment.get_exec_backend(),
|
|
_server: None,
|
|
}
|
|
} else {
|
|
create_process_context(deny_fd_path).await?
|
|
};
|
|
let home = TempDir::new()?;
|
|
let cwd = PathUri::from_host_native_path(home.path())?;
|
|
// Keep full-size state in a function, below the 512 KiB state + environment cap.
|
|
// The blocked-path case still exercises the smaller environment fallback.
|
|
let payload_len = if deny_fd_path { 1 } else { 480 * 1024 };
|
|
let payload = "x".repeat(payload_len);
|
|
let source = if shell == "bash" {
|
|
"${BASH_SOURCE[0]}"
|
|
} else {
|
|
"${(%):-%x}"
|
|
};
|
|
let source_check = if deny_fd_path {
|
|
String::new()
|
|
} else {
|
|
format!("case \"{source}\" in /dev/fd/*) ;; *) return 42 ;; esac; ")
|
|
};
|
|
let aliases = if shell == "zsh" {
|
|
"module_path=()\nsetopt RC_QUOTES\nalias snapshot_quoted=\"printf '%s|' 'one''two'\"\nalias eval='exit 44'\nalias case='exit 45'\n"
|
|
} else {
|
|
""
|
|
};
|
|
std::fs::write(
|
|
home.path().join(format!(".{shell}rc")),
|
|
format!(
|
|
"printf x >> \"$HOME/captures\"\nprofile_helper() {{ {source_check}local payload='{payload}'; [ \"${{#payload}}\" = {payload_len} ] || return 43; printf 'restored:%s' \"$1\"; }}\nexec() {{ exit 41; }}\nset -u\n{aliases}"
|
|
),
|
|
)?;
|
|
let protected_file =
|
|
tempfile::NamedTempFile::new_in(codex_uds::prepare_shared_daemon_socket_directory()?)?;
|
|
std::fs::write(protected_file.path(), "protected")?;
|
|
let command = if sandbox_mode == SnapshotSandbox::WorkspaceWrite {
|
|
// Broad /tmp grants must not expose the file while it still has a name.
|
|
"if /bin/cat \"$SNAPSHOT_TRANSPORT_PROBE\" >/dev/null 2>&1 || (printf poisoned >> \"$SNAPSHOT_TRANSPORT_PROBE\") 2>/dev/null || /bin/mv \"$SNAPSHOT_TRANSPORT_PROBE\" \"$HOME/stolen\" 2>/dev/null; then exit 44; fi\n"
|
|
} else {
|
|
""
|
|
};
|
|
let replay_checks = if shell == "zsh" {
|
|
"eval snapshot_quoted\n[[ -o rcquotes ]] || exit 46\n"
|
|
} else {
|
|
""
|
|
};
|
|
let command =
|
|
format!("{command}IFS= read -r line\n{replay_checks}profile_helper \"$line\"; exit 7");
|
|
let sandbox = if use_sandbox {
|
|
let mut policy = FileSystemSandboxPolicy::read_only();
|
|
policy.entries.push(FileSystemSandboxEntry::new(
|
|
cwd.clone().into(),
|
|
FileSystemAccessMode::Write,
|
|
));
|
|
if deny_fd_path {
|
|
policy.entries.push(FileSystemSandboxEntry::new(
|
|
PathUri::from_host_native_path("/dev/fd")?.into(),
|
|
FileSystemAccessMode::Deny,
|
|
));
|
|
} else {
|
|
policy.entries.push(FileSystemSandboxEntry::new(
|
|
PathUri::from_host_native_path(std::fs::canonicalize("/tmp")?)?.into(),
|
|
FileSystemAccessMode::Write,
|
|
));
|
|
}
|
|
Some(FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted),
|
|
cwd.clone(),
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
let commands = (0..8).map(|index| {
|
|
let backend = &context.backend;
|
|
let cwd = cwd.clone();
|
|
let home = home.path();
|
|
let sandbox = sandbox.clone();
|
|
let command = command.clone();
|
|
let protected_path = protected_file.path();
|
|
async move {
|
|
let started = backend
|
|
.start(ExecParams {
|
|
metadata: Default::default(),
|
|
process_id: format!("parallel-{index}").into(),
|
|
argv: vec![format!("/bin/{shell}"), "-lc".to_string(), command],
|
|
cwd,
|
|
env: HashMap::new(),
|
|
env_policy: Some(ExecEnvPolicy {
|
|
inherit: ShellEnvironmentPolicyInherit::None,
|
|
ignore_default_excludes: false,
|
|
exclude: Vec::new(),
|
|
r#set: HashMap::from([
|
|
("HOME".to_string(), home.to_string_lossy().into_owned()),
|
|
("PATH".to_string(), "/usr/bin:/bin".to_string()),
|
|
(
|
|
"SNAPSHOT_TRANSPORT_PROBE".to_string(),
|
|
protected_path.to_string_lossy().into_owned(),
|
|
),
|
|
]),
|
|
include_only: Vec::new(),
|
|
}),
|
|
shell_snapshot: Some(ShellSnapshotRequest {
|
|
scope_id: "parallel".to_string(),
|
|
shell: ShellInfo {
|
|
name: shell.to_string(),
|
|
path: format!("/bin/{shell}"),
|
|
},
|
|
}),
|
|
tty,
|
|
pipe_stdin: true,
|
|
arg0: None,
|
|
sandbox,
|
|
enforce_managed_network: false,
|
|
managed_network: None,
|
|
network_proxy: None,
|
|
})
|
|
.await?;
|
|
started
|
|
.process
|
|
.write(format!("input-{index}\n").into_bytes())
|
|
.await?;
|
|
let (output, errors, status, closed) =
|
|
collect_process_output_from_events(started.process).await?;
|
|
assert!(
|
|
output.ends_with(&format!("restored:input-{index}")),
|
|
"{output:?}"
|
|
);
|
|
if shell == "zsh" {
|
|
assert!(
|
|
output.ends_with(&format!("one'two|restored:input-{index}")),
|
|
"{output:?}"
|
|
);
|
|
}
|
|
assert_eq!((errors, status, closed), (String::new(), Some(7), true));
|
|
Ok::<_, anyhow::Error>(())
|
|
}
|
|
});
|
|
for result in futures::future::join_all(commands).await {
|
|
result?;
|
|
}
|
|
assert_eq!(std::fs::read_to_string(home.path().join("captures"))?, "x");
|
|
assert_eq!(std::fs::read_to_string(protected_file.path())?, "protected");
|
|
Ok(())
|
|
}
|