fix(windows-sandbox): stabilize managed proxy ports

Co-authored-by: Codex noreply@openai.com
This commit is contained in:
viyatb-oai
2026-07-06 10:11:21 -07:00
co-authored by Codex noreply@openai.com
parent 7094fa467e
commit b4da9f3690
15 changed files with 454 additions and 37 deletions
@@ -270,6 +270,7 @@ impl CommandExecRequestProcessor {
};
let started_network_proxy = match network_proxy_spec.as_ref() {
Some(spec) => match spec
.with_base_environment_id(Some(codex_exec_server::LOCAL_ENVIRONMENT_ID))
.start_proxy(
&network_proxy_permission_profile,
/*policy_decider*/ None,
+5 -1
View File
@@ -340,9 +340,13 @@ async fn run_command_under_sandbox(
let _ = log_denials;
let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
let network_proxy_spec =
config.permissions.network.as_ref().map(|spec| {
spec.with_base_environment_id(Some(codex_exec_server::LOCAL_ENVIRONMENT_ID))
});
// This proxy should only live for the lifetime of the child process.
let network_proxy = match config.permissions.network.as_ref() {
let network_proxy = match network_proxy_spec.as_ref() {
Some(spec) => Some(
spec.start_proxy(
&permission_profile,
+9
View File
@@ -1,8 +1,10 @@
use std::path::PathBuf;
use anyhow::Context;
use clap::ArgAction;
use clap::ArgGroup;
use clap::Parser;
use codex_core::config::ConfigBuilder;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::find_codex_home;
@@ -77,10 +79,17 @@ pub(crate) fn parse_setup_command(
async fn run_elevated(cmd: SandboxSetupCommand) -> anyhow::Result<()> {
let identity = resolve_sandbox_setup_identity(&cmd)?;
let config = ConfigBuilder::default()
.codex_home(identity.codex_home.clone())
.fallback_cwd(Some(identity.codex_home.clone()))
.build()
.await
.context("failed to load target user's Codex config for sandbox provisioning")?;
codex_core::windows_sandbox::run_elevated_provisioning_setup(
identity.codex_home.as_path(),
identity.real_user.as_str(),
config.permissions.network.as_ref(),
)?;
ConfigEditsBuilder::new(identity.codex_home.as_path())
.set_windows_sandbox_mode("elevated")
+29 -2
View File
@@ -14,6 +14,8 @@ use codex_network_proxy::NetworkProxyHandle;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::build_config_state;
use codex_network_proxy::host_and_port_from_network_addr;
#[cfg(any(target_os = "windows", test))]
use codex_network_proxy::managed_proxy_ports;
use codex_network_proxy::normalize_host;
use codex_network_proxy::validate_policy_against_constraints;
use codex_protocol::models::PermissionProfile;
@@ -27,6 +29,7 @@ pub struct NetworkProxySpec {
config: NetworkProxyConfig,
constraints: NetworkProxyConstraints,
hard_deny_allowlist_misses: bool,
base_environment_id: Option<String>,
}
pub struct StartedNetworkProxy {
@@ -77,6 +80,13 @@ impl NetworkProxySpec {
self.config.network.enabled
}
/// Returns a copy whose primary listeners are attributed to one execution environment.
pub fn with_base_environment_id(&self, environment_id: Option<&str>) -> Self {
let mut spec = self.clone();
spec.base_environment_id = environment_id.map(str::to_string);
spec
}
pub fn proxy_host_and_port(&self) -> String {
host_and_port_from_network_addr(&self.config.network.proxy_url, /*default_port*/ 3128)
}
@@ -85,6 +95,16 @@ impl NetworkProxySpec {
self.config.network.enable_socks5
}
#[cfg(any(target_os = "windows", test))]
pub(crate) fn configured_proxy_ports(&self) -> std::io::Result<Vec<u16>> {
managed_proxy_ports(&self.config).map_err(std::io::Error::other)
}
#[cfg(any(target_os = "windows", test))]
pub(crate) fn allow_local_binding(&self) -> bool {
self.config.network.allow_local_binding
}
pub(crate) fn from_config_and_constraints(
config: NetworkProxyConfig,
requirements: Option<NetworkConstraints>,
@@ -116,6 +136,7 @@ impl NetworkProxySpec {
config,
constraints,
hard_deny_allowlist_misses,
base_environment_id: None,
})
}
@@ -129,6 +150,10 @@ impl NetworkProxySpec {
) -> std::io::Result<StartedNetworkProxy> {
let state = self.build_state_with_audit_metadata(audit_metadata)?;
let mut builder = NetworkProxy::builder().state(Arc::new(state));
#[cfg(target_os = "windows")]
if let Some(base_environment_id) = self.base_environment_id.as_deref() {
builder = builder.base_environment_id(base_environment_id);
}
if enable_network_approval_flow && !self.hard_deny_allowlist_misses {
if let Some(policy_decider) = policy_decider {
builder = builder.policy_decider_arc(policy_decider);
@@ -154,11 +179,13 @@ impl NetworkProxySpec {
&self,
permission_profile: &PermissionProfile,
) -> std::io::Result<Self> {
Self::from_config_and_constraints(
let mut spec = Self::from_config_and_constraints(
self.base_config.clone(),
self.requirements.clone(),
permission_profile,
)
)?;
spec.base_environment_id = self.base_environment_id.clone();
Ok(spec)
}
pub(crate) fn with_exec_policy_network_rules(
@@ -26,6 +26,7 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() {
config: NetworkProxyConfig::default(),
constraints: NetworkProxyConstraints::default(),
hard_deny_allowlist_misses: false,
base_environment_id: None,
};
let metadata = NetworkProxyAuditMetadata {
conversation_id: Some("conversation-1".to_string()),
@@ -40,6 +41,41 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() {
assert_eq!(state.audit_metadata(), &metadata);
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn base_environment_uses_the_configured_windows_port_after_recompute() -> std::io::Result<()>
{
let occupied = std::net::TcpListener::bind("127.0.0.1:0").expect("bind occupied port");
let busy_port = occupied.local_addr().expect("read occupied port").port();
let permission_profile = PermissionProfile::workspace_write();
let mut config = NetworkProxyConfig::default();
config.network.proxy_url = format!("http://127.0.0.1:{busy_port}");
config.network.enable_socks5 = false;
let spec = NetworkProxySpec::from_config_and_constraints(
config,
/*requirements*/ None,
&permission_profile,
)?
.with_base_environment_id(Some(codex_exec_server::LOCAL_ENVIRONMENT_ID))
.recompute_for_permission_profile(&permission_profile)?;
let result = spec
.start_proxy(
&permission_profile,
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
/*enable_network_approval_flow*/ false,
NetworkProxyAuditMetadata::default(),
)
.await;
assert!(
result.is_err(),
"stable Windows proxy ingress must not use an ephemeral fallback"
);
Ok(())
}
#[test]
fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
let mut config = NetworkProxyConfig::default();
+13
View File
@@ -1025,6 +1025,13 @@ impl Session {
}
}
fn base_proxy_environment_id(environment_manager: &EnvironmentManager) -> Option<&'static str> {
environment_manager
.try_local_environment()
.is_some()
.then_some(codex_exec_server::LOCAL_ENVIRONMENT_ID)
}
async fn start_managed_network_proxy(
spec: &crate::config::NetworkProxySpec,
exec_policy: &codex_execpolicy::Policy,
@@ -1109,6 +1116,12 @@ impl Session {
return;
}
let spec = spec.with_base_environment_id(Self::base_proxy_environment_id(
self.services
.turn_environments
.environment_manager()
.as_ref(),
));
match Self::start_managed_network_proxy(
&spec,
current_exec_policy.as_ref(),
+3 -1
View File
@@ -891,6 +891,7 @@ impl Session {
} else {
ShellSnapshot::disabled()
};
let base_proxy_environment_id = Self::base_proxy_environment_id(&environment_manager);
let turn_environments = Arc::new(ThreadEnvironments::new(
environment_manager,
default_shell.clone(),
@@ -974,8 +975,9 @@ impl Session {
let (network_proxy, session_network_proxy) =
if let Some(spec) = config.permissions.network.as_ref() {
let current_exec_policy = exec_policy.current();
let spec = spec.with_base_environment_id(base_proxy_environment_id);
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
&spec,
current_exec_policy.as_ref(),
config.permissions.permission_profile(),
network_policy_decider.as_ref().map(Arc::clone),
+12
View File
@@ -755,6 +755,18 @@ fn validated_network_policy_amendment_host_rejects_mismatch() {
assert!(message.contains("does not match approved host"));
}
#[tokio::test]
async fn base_proxy_environment_requires_a_local_environment() {
assert_eq!(
Session::base_proxy_environment_id(&EnvironmentManager::default_for_tests()),
Some(codex_exec_server::LOCAL_ENVIRONMENT_ID)
);
assert_eq!(
Session::base_proxy_environment_id(&EnvironmentManager::without_environments()),
None
);
}
#[tokio::test]
async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyhow::Result<()> {
let permission_profile = PermissionProfile::workspace_write();
+28 -3
View File
@@ -169,9 +169,30 @@ pub fn run_elevated_setup(
)
}
#[cfg(any(target_os = "windows", test))]
fn provisioning_settings(
network: Option<&crate::config::NetworkProxySpec>,
) -> std::io::Result<codex_windows_sandbox::WindowsSandboxProvisioningSettings> {
let Some(network) = network.filter(|network| network.enabled()) else {
return Ok(codex_windows_sandbox::WindowsSandboxProvisioningSettings::default());
};
Ok(codex_windows_sandbox::WindowsSandboxProvisioningSettings {
proxy_ports: network.configured_proxy_ports()?,
allow_local_binding: network.allow_local_binding(),
})
}
#[cfg(target_os = "windows")]
pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> anyhow::Result<()> {
codex_windows_sandbox::run_elevated_provisioning_setup(codex_home, real_user)
pub fn run_elevated_provisioning_setup(
codex_home: &Path,
real_user: &str,
network: Option<&crate::config::NetworkProxySpec>,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_elevated_provisioning_setup(
codex_home,
real_user,
provisioning_settings(network)?,
)
}
#[cfg(not(target_os = "windows"))]
@@ -186,7 +207,11 @@ pub fn run_elevated_setup(
}
#[cfg(not(target_os = "windows"))]
pub fn run_elevated_provisioning_setup(_codex_home: &Path, _real_user: &str) -> anyhow::Result<()> {
pub fn run_elevated_provisioning_setup(
_codex_home: &Path,
_real_user: &str,
_network: Option<&crate::config::NetworkProxySpec>,
) -> anyhow::Result<()> {
anyhow::bail!("elevated Windows sandbox setup is only supported on Windows")
}
@@ -2,6 +2,8 @@ use super::*;
use codex_config::types::WindowsToml;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_network_proxy::NetworkProxyConfig;
use codex_protocol::models::PermissionProfile;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -115,3 +117,64 @@ fn resolve_windows_sandbox_private_desktop_respects_explicit_cfg_value() {
assert!(!resolve_windows_sandbox_private_desktop(&cfg));
}
#[test]
fn provisioning_settings_match_the_configured_managed_proxy() {
let mut config = NetworkProxyConfig::default();
config.network.enabled = true;
config.network.proxy_url = "http://127.0.0.1:43128".to_string();
config.network.socks_url = "socks5h://127.0.0.1:48081".to_string();
config.network.allow_local_binding = true;
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
config,
/*requirements*/ None,
&PermissionProfile::workspace_write(),
)
.expect("managed proxy config should resolve");
assert_eq!(
provisioning_settings(Some(&spec)).expect("provisioning settings should resolve"),
codex_windows_sandbox::WindowsSandboxProvisioningSettings {
proxy_ports: vec![43128, 48081],
allow_local_binding: true,
}
);
}
#[test]
fn provisioning_settings_are_empty_when_managed_network_is_disabled() {
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
/*requirements*/ None,
&PermissionProfile::workspace_write(),
)
.expect("managed proxy config should resolve");
assert_eq!(
provisioning_settings(Some(&spec)).expect("provisioning settings should resolve"),
codex_windows_sandbox::WindowsSandboxProvisioningSettings::default()
);
}
#[test]
fn provisioning_settings_exclude_the_disabled_socks_listener() {
let mut config = NetworkProxyConfig::default();
config.network.enabled = true;
config.network.enable_socks5 = false;
config.network.proxy_url = "http://127.0.0.1:43128".to_string();
config.network.socks_url = "socks5h://127.0.0.1:48081".to_string();
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
config,
/*requirements*/ None,
&PermissionProfile::workspace_write(),
)
.expect("managed proxy config should resolve");
assert_eq!(
provisioning_settings(Some(&spec)).expect("provisioning settings should resolve"),
codex_windows_sandbox::WindowsSandboxProvisioningSettings {
proxy_ports: vec![43128],
allow_local_binding: false,
}
);
}
+38
View File
@@ -438,6 +438,26 @@ pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
})
}
/// Returns the sorted loopback ports used by the configured managed proxy listeners.
pub fn managed_proxy_ports(cfg: &NetworkProxyConfig) -> Result<Vec<u16>> {
let runtime = resolve_runtime(cfg)?;
if runtime.http_addr.port() == 0 {
bail!("network.proxy_url must use a fixed non-zero port for managed proxy provisioning");
}
let mut ports = vec![runtime.http_addr.port()];
if cfg.network.enable_socks5 {
if runtime.socks_addr.port() == 0 {
bail!(
"network.socks_url must use a fixed non-zero port for managed proxy provisioning"
);
}
ports.push(runtime.socks_addr.port());
}
ports.sort_unstable();
ports.dedup();
Ok(ports)
}
fn resolve_addr(url: &str, default_port: u16) -> Result<SocketAddr> {
let addr_parts = parse_host_port(url, default_port)?;
let host = if addr_parts.host.eq_ignore_ascii_case("localhost") {
@@ -613,6 +633,24 @@ mod tests {
);
}
#[test]
fn managed_proxy_ports_reject_ephemeral_ports() {
let mut config = NetworkProxyConfig::default();
config.network.proxy_url = "http://127.0.0.1:0".to_string();
assert_eq!(
managed_proxy_ports(&config).unwrap_err().to_string(),
"network.proxy_url must use a fixed non-zero port for managed proxy provisioning"
);
config.network.proxy_url = "http://127.0.0.1:3128".to_string();
config.network.socks_url = "socks5h://127.0.0.1:0".to_string();
assert_eq!(
managed_proxy_ports(&config).unwrap_err().to_string(),
"network.socks_url must use a fixed non-zero port for managed proxy provisioning"
);
}
#[test]
fn partial_network_config_uses_struct_defaults_for_missing_fields() {
let config: NetworkProxyConfig = serde_json::from_str(
+1
View File
@@ -28,6 +28,7 @@ pub use config::NetworkProxyConfig;
pub use config::NetworkUnixSocketPermission;
pub use config::NetworkUnixSocketPermissions;
pub use config::host_and_port_from_network_addr;
pub use config::managed_proxy_ports;
pub use credential_broker::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
pub use credential_broker::brokered_credential_dummy_env_keys;
pub use credential_broker::brokered_credential_env_keys;
+160 -12
View File
@@ -10,6 +10,8 @@ use crate::socks5;
use crate::state::NetworkProxyState;
use anyhow::Context;
use anyhow::Result;
#[cfg(target_os = "windows")]
use anyhow::bail;
use clap::Parser;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
@@ -100,6 +102,7 @@ pub struct NetworkProxyBuilder {
state: Option<Arc<NetworkProxyState>>,
http_addr: Option<SocketAddr>,
socks_addr: Option<SocketAddr>,
base_environment_id: Option<String>,
managed_by_codex: bool,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
@@ -111,6 +114,7 @@ impl Default for NetworkProxyBuilder {
state: None,
http_addr: None,
socks_addr: None,
base_environment_id: None,
managed_by_codex: true,
policy_decider: None,
blocked_request_observer: None,
@@ -134,6 +138,14 @@ impl NetworkProxyBuilder {
self
}
/// Associates the primary proxy listeners with one execution environment.
///
/// Commands in other environments continue to use isolated listener pairs.
pub fn base_environment_id(mut self, environment_id: impl Into<String>) -> Self {
self.base_environment_id = Some(environment_id.into());
self
}
pub fn managed_by_codex(mut self, managed_by_codex: bool) -> Self {
self.managed_by_codex = managed_by_codex;
self
@@ -193,6 +205,11 @@ impl NetworkProxyBuilder {
managed_http_addr,
managed_socks_addr,
current_cfg.network.enable_socks5,
if self.base_environment_id.is_some() {
WindowsManagedPortSelection::ConfiguredOnly
} else {
WindowsManagedPortSelection::AllowEphemeralFallback
},
)
.context("reserve managed loopback proxy listeners")?;
#[cfg(not(target_os = "windows"))]
@@ -227,6 +244,7 @@ impl NetworkProxyBuilder {
socks_addr,
socks_enabled: current_cfg.network.enable_socks5,
socks5_udp_enabled: current_cfg.network.enable_socks5_udp,
base_environment_id: self.base_environment_id,
runtime_settings: Arc::new(RwLock::new(NetworkProxyRuntimeSettings::from_config(
&current_cfg,
)?)),
@@ -250,23 +268,46 @@ fn reserve_loopback_ephemeral_listeners(
Ok(ReservedListenerSet::new(http_listener, socks_listener))
}
#[cfg(target_os = "windows")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum WindowsManagedPortSelection {
ConfiguredOnly,
AllowEphemeralFallback,
}
#[cfg(target_os = "windows")]
fn reserve_windows_managed_listeners(
http_addr: SocketAddr,
socks_addr: SocketAddr,
reserve_socks_listener: bool,
port_selection: WindowsManagedPortSelection,
) -> Result<ReservedListenerSet> {
let http_addr = windows_managed_loopback_addr(http_addr);
let socks_addr = windows_managed_loopback_addr(socks_addr);
if port_selection == WindowsManagedPortSelection::ConfiguredOnly {
if http_addr.port() == 0 {
bail!("configured Windows HTTP proxy address must use a fixed non-zero port");
}
if reserve_socks_listener && socks_addr.port() == 0 {
bail!("configured Windows SOCKS proxy address must use a fixed non-zero port");
}
}
match try_reserve_windows_managed_listeners(http_addr, socks_addr, reserve_socks_listener) {
Ok(listeners) => Ok(listeners),
Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => {
Err(err)
if err.kind() == std::io::ErrorKind::AddrInUse
&& port_selection == WindowsManagedPortSelection::AllowEphemeralFallback =>
{
warn!("managed Windows proxy ports are busy; falling back to ephemeral loopback ports");
reserve_loopback_ephemeral_listeners(reserve_socks_listener)
.context("reserve fallback loopback proxy listeners")
}
Err(err) => Err(err).context("reserve Windows managed proxy listeners"),
Err(err) => Err(err).with_context(|| {
format!(
"reserve configured Windows managed proxy listeners at {http_addr} and {socks_addr}"
)
}),
}
}
@@ -366,6 +407,7 @@ pub struct NetworkProxy {
socks_addr: SocketAddr,
socks_enabled: bool,
socks5_udp_enabled: bool,
base_environment_id: Option<String>,
runtime_settings: Arc<RwLock<NetworkProxyRuntimeSettings>>,
reserved_listeners: Option<Arc<ReservedListeners>>,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
@@ -387,6 +429,7 @@ impl PartialEq for NetworkProxy {
fn eq(&self, other: &Self) -> bool {
self.http_addr == other.http_addr
&& self.socks_addr == other.socks_addr
&& self.base_environment_id == other.base_environment_id
&& self.runtime_settings() == other.runtime_settings()
}
}
@@ -778,6 +821,13 @@ impl NetworkProxy {
}
fn environment_proxy_addrs(&self, environment_id: &str) -> Result<EnvironmentProxyAddrs> {
if self.base_environment_id.as_deref() == Some(environment_id) {
return Ok(EnvironmentProxyAddrs {
http_addr: self.http_addr,
socks_addr: self.socks_addr,
});
}
let mut proxies = self
.environment_proxies
.lock()
@@ -913,6 +963,7 @@ impl NetworkProxy {
let http_state = self.state.clone();
let http_decider = self.policy_decider.clone();
let http_environment_id = self.base_environment_id.clone();
let http_addr = self.http_addr;
let http_task = tokio::spawn(async move {
match http_listener {
@@ -921,7 +972,7 @@ impl NetworkProxy {
http_state,
listener,
http_decider,
/*environment_id*/ None,
http_environment_id,
)
.await
}
@@ -930,7 +981,7 @@ impl NetworkProxy {
http_state,
http_addr,
http_decider,
/*environment_id*/ None,
http_environment_id,
)
.await
}
@@ -940,6 +991,7 @@ impl NetworkProxy {
let socks_task = if current_cfg.network.enable_socks5 {
let socks_state = self.state.clone();
let socks_decider = self.policy_decider.clone();
let socks_environment_id = self.base_environment_id.clone();
let socks_addr = self.socks_addr;
let enable_socks5_udp = current_cfg.network.enable_socks5_udp;
Some(tokio::spawn(async move {
@@ -949,7 +1001,7 @@ impl NetworkProxy {
socks_state,
listener,
socks_decider,
/*environment_id*/ None,
socks_environment_id,
enable_socks5_udp,
)
.await
@@ -959,7 +1011,7 @@ impl NetworkProxy {
socks_state,
socks_addr,
socks_decider,
/*environment_id*/ None,
socks_environment_id,
enable_socks5_udp,
)
.await
@@ -1070,11 +1122,61 @@ impl Drop for NetworkProxyHandle {
mod tests {
use super::*;
use crate::config::NetworkProxySettings;
use crate::network_policy::NetworkDecision;
use crate::network_policy::NetworkPolicyRequest;
use crate::network_policy::NetworkProtocol;
use crate::state::network_proxy_state_for_policy;
use pretty_assertions::assert_eq;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::path::Path;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::time::Duration;
use tokio::time::timeout;
fn proxy_addr_from_env(env: &HashMap<String, String>, key: &str, scheme: &str) -> SocketAddr {
env.get(key)
.and_then(|value| value.strip_prefix(scheme))
.and_then(|value| value.parse().ok())
.unwrap_or_else(|| panic!("expected {key} to contain a {scheme} proxy address"))
}
async fn issue_denied_http_request(proxy_addr: SocketAddr) -> Result<()> {
let mut stream = tokio::net::TcpStream::connect(proxy_addr).await?;
stream
.write_all(
b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n",
)
.await?;
let mut response = Vec::new();
timeout(Duration::from_secs(2), stream.read_to_end(&mut response)).await??;
assert!(
response.starts_with(b"HTTP/1.1 403 Forbidden"),
"unexpected proxy response: {}",
String::from_utf8_lossy(&response)
);
Ok(())
}
async fn issue_denied_socks_request(proxy_addr: SocketAddr) -> Result<()> {
let mut stream = tokio::net::TcpStream::connect(proxy_addr).await?;
stream.write_all(&[0x05, 0x01, 0x00]).await?;
let mut greeting = [0_u8; 2];
timeout(Duration::from_secs(2), stream.read_exact(&mut greeting)).await??;
assert_eq!(greeting, [0x05, 0x00]);
let host = b"example.com";
let mut request = vec![0x05, 0x01, 0x00, 0x03, host.len() as u8];
request.extend_from_slice(host);
request.extend_from_slice(&80_u16.to_be_bytes());
stream.write_all(&request).await?;
let mut response = [0_u8; 2];
timeout(Duration::from_secs(2), stream.read_exact(&mut response)).await??;
assert_eq!(response[0], 0x05);
assert_ne!(response[1], 0x00);
Ok(())
}
#[tokio::test]
async fn managed_proxy_builder_uses_loopback_ports() {
@@ -1143,11 +1245,37 @@ mod tests {
}
#[tokio::test]
async fn prepare_for_environment_keeps_env_and_sandbox_ports_in_sync() -> Result<()> {
let state = Arc::new(network_proxy_state_for_policy(
NetworkProxySettings::default(),
));
let proxy = NetworkProxy::builder().state(state).build().await?;
async fn base_environment_reuses_primary_listener_and_keeps_other_environments_isolated()
-> Result<()> {
let http_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
let http_addr = http_listener.local_addr()?;
let socks_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
let socks_addr = socks_listener.local_addr()?;
drop(http_listener);
drop(socks_listener);
let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
enabled: true,
proxy_url: format!("http://{http_addr}"),
socks_url: format!("socks5h://{socks_addr}"),
..NetworkProxySettings::default()
}));
let seen_requests = Arc::new(Mutex::new(Vec::new()));
let decider: Arc<dyn NetworkPolicyDecider> = Arc::new({
let seen_requests = Arc::clone(&seen_requests);
move |request: NetworkPolicyRequest| {
seen_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((request.protocol, request.environment_id));
async { NetworkDecision::deny("test denial") }
}
});
let proxy = NetworkProxy::builder()
.state(state)
.base_environment_id("local")
.policy_decider_arc(decider)
.build()
.await?;
let handle = proxy.run().await?;
let base_env = HashMap::from([("PRESERVED".to_string(), "value".to_string())]);
@@ -1159,7 +1287,7 @@ mod tests {
Some("value")
);
assert_ne!(local.env.get("HTTP_PROXY"), remote.env.get("HTTP_PROXY"));
assert_ne!(
assert_eq!(
local.env.get("HTTP_PROXY"),
Some(&format!("http://{}", proxy.http_addr()))
);
@@ -1197,6 +1325,25 @@ mod tests {
proxy.apply_to_env_for_environment(&mut legacy_env, "local")?;
assert_eq!(legacy_env, local.env);
issue_denied_http_request(proxy_addr_from_env(&local.env, "HTTP_PROXY", "http://")).await?;
issue_denied_http_request(proxy_addr_from_env(&remote.env, "HTTP_PROXY", "http://"))
.await?;
issue_denied_socks_request(proxy_addr_from_env(&local.env, "ALL_PROXY", "socks5h://"))
.await?;
issue_denied_socks_request(proxy_addr_from_env(&remote.env, "ALL_PROXY", "socks5h://"))
.await?;
assert_eq!(
*seen_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![
(NetworkProtocol::Http, Some("local".to_string())),
(NetworkProtocol::Http, Some("remote".to_string())),
(NetworkProtocol::Socks5Tcp, Some("local".to_string())),
(NetworkProtocol::Socks5Tcp, Some("remote".to_string())),
]
);
handle.shutdown().await?;
Ok(())
}
@@ -1262,6 +1409,7 @@ mod tests {
SocketAddr::from(([127, 0, 0, 1], busy_port)),
SocketAddr::from(([127, 0, 0, 1], 48081)),
/*reserve_socks_listener*/ false,
WindowsManagedPortSelection::AllowEphemeralFallback,
)
.unwrap();
+9
View File
@@ -44,6 +44,15 @@ pub enum WindowsSandboxProxySettingsMode {
Preserve,
}
/// Network settings installed by an administrator during managed Windows sandbox setup.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WindowsSandboxProvisioningSettings {
/// Loopback proxy ports permitted for the offline sandbox identity.
pub proxy_ports: Vec<u16>,
/// Whether the offline sandbox identity may exchange arbitrary loopback traffic.
pub allow_local_binding: bool,
}
#[cfg(target_os = "windows")]
mod acl;
#[cfg(target_os = "windows")]
+47 -18
View File
@@ -903,7 +903,35 @@ fn run_elevated_setup_inner(
run_setup_exe(&payload, needs_elevation, request.codex_home)
}
pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> Result<()> {
fn build_provisioning_payload(
codex_home: &Path,
real_user: &str,
settings: crate::WindowsSandboxProvisioningSettings,
) -> ElevationPayload {
ElevationPayload {
version: SETUP_VERSION,
offline_username: OFFLINE_USERNAME.to_string(),
online_username: ONLINE_USERNAME.to_string(),
codex_home: codex_home.to_path_buf(),
command_cwd: codex_home.to_path_buf(),
read_roots: Vec::new(),
write_roots: Vec::new(),
deny_read_paths: Vec::new(),
deny_write_paths: Vec::new(),
proxy_ports: settings.proxy_ports,
allow_local_binding: settings.allow_local_binding,
otel: codex_otel::global_statsig_metrics_settings(),
real_user: real_user.to_string(),
mode: SetupMode::ProvisionOnly,
refresh_only: false,
}
}
pub fn run_elevated_provisioning_setup(
codex_home: &Path,
real_user: &str,
settings: crate::WindowsSandboxProvisioningSettings,
) -> Result<()> {
let sbx_dir = sandbox_dir(codex_home);
std::fs::create_dir_all(&sbx_dir).map_err(|err| {
failure(
@@ -922,23 +950,7 @@ pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> Re
"sandbox provisioning setup must be run from an elevated process",
));
}
let payload = ElevationPayload {
version: SETUP_VERSION,
offline_username: OFFLINE_USERNAME.to_string(),
online_username: ONLINE_USERNAME.to_string(),
codex_home: codex_home.to_path_buf(),
command_cwd: codex_home.to_path_buf(),
read_roots: Vec::new(),
write_roots: Vec::new(),
deny_read_paths: Vec::new(),
deny_write_paths: Vec::new(),
proxy_ports: Vec::new(),
allow_local_binding: false,
otel: codex_otel::global_statsig_metrics_settings(),
real_user: real_user.to_string(),
mode: SetupMode::ProvisionOnly,
refresh_only: false,
};
let payload = build_provisioning_payload(codex_home, real_user, settings);
run_setup_exe(&payload, /*needs_elevation*/ false, codex_home)
}
@@ -1239,6 +1251,23 @@ mod tests {
);
}
#[test]
fn provisioning_payload_forwards_managed_proxy_settings() {
let codex_home = Path::new(r"C:\Users\test\.codex");
let payload = super::build_provisioning_payload(
codex_home,
"test-user",
crate::WindowsSandboxProvisioningSettings {
proxy_ports: vec![3128, 8081],
allow_local_binding: true,
},
);
assert_eq!(payload.proxy_ports, vec![3128, 8081]);
assert!(payload.allow_local_binding);
assert!(matches!(payload.mode, super::SetupMode::ProvisionOnly));
}
#[test]
fn report_helper_failure_uses_setup_error_report_when_clear_succeeded() {
let tmp = TempDir::new().expect("tempdir");