Files
open-matt b8d5e3f12e Allow exec-server to proxy permitted private IPs upstream (#48568)
## 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
2026-09-26 23:17:41 +00:00

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(())
}