mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
fix(windows-sandbox): stabilize managed proxy ports
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
co-authored by
Codex noreply@openai.com
parent
7094fa467e
commit
b4da9f3690
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
¤t_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();
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user