From dcfcb570b2cd0a2500b1d47a7b04a7cb1b0a0bd2 Mon Sep 17 00:00:00 2001 From: johnl-oai Date: Wed, 2 Sep 2026 18:40:46 +0000 Subject: [PATCH] Add an authenticated Windows sandbox provisioning client (#42337) ## What changed - Add a client for the packaged Windows sandbox provisioning service that verifies the named-pipe server belongs to the running `CodexSandboxService` process before exchanging a versioned provisioning request. - Treat an absent, busy, timed-out, or disconnected service as unavailable while surfacing provisioning and authentication failures. - Send the complete `WindowsSandboxProvisioningSettings` and classify inherited HTTP and SOCKS proxy listeners separately for managed-policy validation. - Share bounded frame-readiness handling between runner and provisioning IPC. ## Testing - Extend proxy environment tests to cover network-enabled profiles, mixed HTTP and SOCKS listeners, explicit proxy-port overrides, case-insensitive schemes, and unclassified protocols. GitOrigin-RevId: 6ff6741fc3ecd76314a7a045523a8d7a7e7af686 --- codex-rs/windows-sandbox-rs/Cargo.toml | 1 + .../src/elevated/runner_client.rs | 55 +--- codex-rs/windows-sandbox-rs/src/framed_io.rs | 52 ++++ codex-rs/windows-sandbox-rs/src/lib.rs | 14 +- .../src/provisioning_client.rs | 277 ++++++++++++++++++ .../src/provisioning_protocol.rs | 14 +- codex-rs/windows-sandbox-rs/src/setup.rs | 136 ++++++++- 7 files changed, 491 insertions(+), 58 deletions(-) create mode 100644 codex-rs/windows-sandbox-rs/src/provisioning_client.rs diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index aa41ed5f08..2e9aedefd4 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -89,6 +89,7 @@ features = [ "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_System_Registry", + "Win32_System_Services", ] version = "0.52" diff --git a/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs b/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs index 50c2843dbf..d04245c8ae 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs @@ -21,7 +21,6 @@ use anyhow::Context; use anyhow::Result; use std::ffi::c_void; use std::fs::File; -use std::os::windows::io::AsRawHandle; use std::os::windows::io::FromRawHandle; use std::path::Path; use std::ptr; @@ -39,7 +38,6 @@ use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode; use windows_sys::Win32::System::IO::CancelSynchronousIo; -use windows_sys::Win32::System::Pipes::PeekNamedPipe; use windows_sys::Win32::System::Threading::CreateProcessWithLogonW; use windows_sys::Win32::System::Threading::GetCurrentProcess; use windows_sys::Win32::System::Threading::GetCurrentThread; @@ -50,7 +48,6 @@ use windows_sys::Win32::System::Threading::WaitForSingleObject; const RUNNER_SPAWN_READY_TIMEOUT: Duration = Duration::from_secs(15); const RUNNER_PIPE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); -const RUNNER_SPAWN_READY_POLL_INTERVAL: Duration = Duration::from_millis(5); const RUNNER_ERROR_MODE_FLAGS: u32 = 0x0001 | 0x0002; const WAIT_OBJECT_0: u32 = 0; @@ -159,7 +156,11 @@ impl RunnerTransport { } pub(crate) fn read_spawn_ready(&mut self) -> Result<()> { - wait_for_complete_frame(&self.pipe_read, RUNNER_SPAWN_READY_TIMEOUT)?; + crate::framed_io::wait_for_complete_frame( + &self.pipe_read, + Instant::now() + RUNNER_SPAWN_READY_TIMEOUT, + ) + .context("wait for runner spawn_ready")?; let msg = read_frame(&mut self.pipe_read)? .ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?; match msg.message { @@ -446,52 +447,6 @@ pub(crate) fn spawn_runner_transport( Ok(transport) } -fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()> { - let handle = pipe_read.as_raw_handle() as HANDLE; - let deadline = Instant::now() + timeout; - let mut len_buf = [0u8; 4]; - - loop { - let mut bytes_read = 0u32; - let mut total_available = 0u32; - let ok = unsafe { - PeekNamedPipe( - handle, - len_buf.as_mut_ptr() as *mut c_void, - len_buf.len() as u32, - &mut bytes_read, - &mut total_available, - ptr::null_mut(), - ) - }; - if ok == 0 { - let err = unsafe { GetLastError() } as i32; - return Err(anyhow::anyhow!( - "PeekNamedPipe failed while waiting for spawn_ready: {err}" - )); - } - - if bytes_read == len_buf.len() as u32 { - let frame_len = u32::from_le_bytes(len_buf) as usize; - let total_len = frame_len - .checked_add(len_buf.len()) - .ok_or_else(|| anyhow::anyhow!("runner frame length overflow"))?; - if total_available as usize >= total_len { - return Ok(()); - } - } - - if Instant::now() >= deadline { - return Err(anyhow::anyhow!( - "timed out after {}ms waiting for runner spawn_ready", - timeout.as_millis() - )); - } - - std::thread::sleep(RUNNER_SPAWN_READY_POLL_INTERVAL); - } -} - #[cfg(test)] mod tests { use super::RunnerLogonError; diff --git a/codex-rs/windows-sandbox-rs/src/framed_io.rs b/codex-rs/windows-sandbox-rs/src/framed_io.rs index 7ed5662ed5..537257f522 100644 --- a/codex-rs/windows-sandbox-rs/src/framed_io.rs +++ b/codex-rs/windows-sandbox-rs/src/framed_io.rs @@ -3,11 +3,19 @@ use anyhow::Result; use serde::Serialize; use serde::de::DeserializeOwned; +use std::fs::File; +use std::io; use std::io::Read; use std::io::Write; +use std::os::windows::io::AsRawHandle; +use std::ptr; +use std::time::Duration; +use std::time::Instant; +use windows_sys::Win32::System::Pipes::PeekNamedPipe; /// Bound the memory used by an individual untrusted IPC frame. const MAX_FRAME_LEN: usize = 8 * 1024 * 1024; +const FRAME_POLL_INTERVAL: Duration = Duration::from_millis(5); pub(crate) fn write_frame(mut writer: W, message: &T) -> Result<()> { let payload = serde_json::to_vec(message)?; @@ -37,3 +45,47 @@ pub(crate) fn read_frame(mut reader: R) -> Result< let message = serde_json::from_slice(&payload)?; Ok(Some(message)) } + +pub(crate) fn wait_for_complete_frame(pipe: &File, deadline: Instant) -> io::Result<()> { + let mut len_buf = [0_u8; 4]; + + loop { + let mut bytes_read = 0_u32; + let mut total_available = 0_u32; + let ok = unsafe { + PeekNamedPipe( + pipe.as_raw_handle() as _, + len_buf.as_mut_ptr().cast(), + len_buf.len() as u32, + &mut bytes_read, + &mut total_available, + ptr::null_mut(), + ) + }; + if ok == 0 { + return Err(io::Error::last_os_error()); + } + + if bytes_read == len_buf.len() as u32 { + let frame_len = u32::from_le_bytes(len_buf) as usize; + if frame_len > MAX_FRAME_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("frame too large: {frame_len}"), + )); + } + if total_available as usize >= len_buf.len() + frame_len { + return Ok(()); + } + } + + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "timed out waiting for a complete IPC frame", + )); + } + std::thread::sleep(remaining.min(FRAME_POLL_INTERVAL)); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 7ccea63198..e48063aa79 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -8,6 +8,9 @@ mod ssh_config_dependencies; use std::fmt; use std::sync::Arc; +use serde::Deserialize; +use serde::Serialize; + /// Cancellation hook used by Windows sandbox capture backends. #[derive(Clone)] pub struct WindowsSandboxCancellationToken { @@ -38,7 +41,8 @@ impl fmt::Debug for WindowsSandboxCancellationToken { pub use codex_protocol::config_types::WindowsSandboxProxySettingsMode; /// Network settings installed by an administrator during managed Windows sandbox setup. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct WindowsSandboxProvisioningSettings { /// Loopback proxy ports permitted for the offline sandbox identity. pub proxy_ports: Vec, @@ -79,6 +83,8 @@ mod path_normalization; #[cfg(target_os = "windows")] mod process; #[cfg(target_os = "windows")] +mod provisioning_client; +#[cfg(target_os = "windows")] mod provisioning_protocol; #[cfg(target_os = "windows")] mod resolved_permissions; @@ -259,6 +265,10 @@ pub use process::read_handle_loop; #[cfg(target_os = "windows")] pub use process::spawn_process_with_pipes; #[cfg(target_os = "windows")] +pub use provisioning_client::WindowsSandboxProvisioningOutcome; +#[cfg(target_os = "windows")] +pub use provisioning_client::provision_windows_sandbox_via_service; +#[cfg(target_os = "windows")] pub use provisioning_protocol::FramedProvisioningMessage; #[cfg(target_os = "windows")] pub use provisioning_protocol::PROVISIONING_PROTOCOL_VERSION; @@ -271,6 +281,8 @@ pub use provisioning_protocol::SandboxProvisioningRequest; #[cfg(target_os = "windows")] pub use provisioning_protocol::SandboxProvisioningResponse; #[cfg(target_os = "windows")] +pub use provisioning_protocol::WindowsSandboxProxyListeners; +#[cfg(target_os = "windows")] pub use provisioning_protocol::read_provisioning_frame; #[cfg(target_os = "windows")] pub use provisioning_protocol::write_provisioning_frame; diff --git a/codex-rs/windows-sandbox-rs/src/provisioning_client.rs b/codex-rs/windows-sandbox-rs/src/provisioning_client.rs new file mode 100644 index 0000000000..edc58e045b --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/provisioning_client.rs @@ -0,0 +1,277 @@ +//! Authenticated client for the packaged Windows sandbox provisioning service. + +use crate::WindowsSandboxProvisioningSettings; +use crate::WindowsSandboxProxyListeners; +use std::collections::HashMap; +use std::fs::File; +use std::fs::OpenOptions; +use std::io; +use std::mem::size_of; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::AsRawHandle; +use std::path::Path; +use std::ptr; +use std::time::Duration; +use std::time::Instant; + +use anyhow::Context; +use anyhow::anyhow; +use anyhow::bail; +use codex_protocol::models::PermissionProfile; +use windows_sys::Win32::Foundation::ERROR_BROKEN_PIPE; +use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND; +use windows_sys::Win32::Foundation::ERROR_NO_DATA; +use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY; +use windows_sys::Win32::Foundation::ERROR_PIPE_NOT_CONNECTED; +use windows_sys::Win32::Foundation::ERROR_SEM_TIMEOUT; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Security::SC_HANDLE; +use windows_sys::Win32::Storage::FileSystem::SECURITY_IMPERSONATION; +use windows_sys::Win32::Storage::FileSystem::SECURITY_SQOS_PRESENT; +use windows_sys::Win32::System::Pipes::GetNamedPipeServerProcessId; +use windows_sys::Win32::System::Pipes::WaitNamedPipeW; +use windows_sys::Win32::System::Services; + +const SERVICE_NAME: &str = "CodexSandboxService"; +const PROVISIONING_TIMEOUT: Duration = Duration::from_secs(120); + +impl WindowsSandboxProvisioningSettings { + /// Derives the full firewall settings using the same environment handling as elevated setup. + pub fn from_environment( + permission_profile: &PermissionProfile, + env_map: &HashMap, + ) -> Self { + let network_identity = if permission_profile.network_sandbox_policy().is_enabled() { + crate::setup::SandboxNetworkIdentity::Online + } else { + crate::setup::SandboxNetworkIdentity::Offline + }; + let settings = crate::setup::offline_proxy_settings_from_env(env_map, network_identity); + Self { + proxy_ports: settings.proxy_ports, + allow_local_binding: settings.allow_local_binding, + } + } +} + +impl WindowsSandboxProxyListeners { + /// Identifies known listener protocols without restricting the ports allowed by setup. + pub fn from_environment( + permission_profile: &PermissionProfile, + env_map: &HashMap, + ) -> Self { + if permission_profile.network_sandbox_policy().is_enabled() { + return Self::default(); + } + let mut listeners = Self::default(); + for value in crate::setup::PROXY_ENV_KEYS + .iter() + .filter_map(|key| env_map.get(*key)) + { + let Some((scheme, _)) = value.trim().split_once("://") else { + continue; + }; + let Some(port) = crate::setup::loopback_proxy_port_from_url(value) else { + continue; + }; + match scheme.to_ascii_lowercase().as_str() { + "http" | "https" => listeners.http_ports.push(port), + "socks4" | "socks4a" | "socks5" | "socks5h" => { + listeners.socks_ports.push(port); + } + _ => {} + } + } + listeners.http_ports.sort_unstable(); + listeners.http_ports.dedup(); + listeners.socks_ports.sort_unstable(); + listeners.socks_ports.dedup(); + listeners + } +} + +/// Result of attempting setup through the packaged provisioning service. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum WindowsSandboxProvisioningOutcome { + /// The service completed elevated provisioning. + Provisioned, + /// The service was absent, did not support this caller, or timed out. + Unavailable, +} + +/// Provisions the elevated Windows sandbox through the authenticated packaged service. +pub fn provision_windows_sandbox_via_service( + codex_home: &Path, + settings: WindowsSandboxProvisioningSettings, + listeners: WindowsSandboxProxyListeners, +) -> anyhow::Result { + let request = crate::FramedProvisioningMessage { + version: crate::PROVISIONING_PROTOCOL_VERSION, + message: crate::ProvisioningMessage::ProvisionSandboxRequest { + payload: crate::SandboxProvisioningRequest { + codex_home: codex_home + .to_str() + .context("sandbox provisioning home is not valid UTF-8")? + .to_owned(), + settings, + listeners, + }, + }, + }; + + let deadline = Instant::now() + PROVISIONING_TIMEOUT; + let Some(mut pipe) = connect(deadline)? else { + return Ok(WindowsSandboxProvisioningOutcome::Unavailable); + }; + let response = (|| -> anyhow::Result { + verify_server(pipe.as_raw_handle() as HANDLE) + .context("authenticate provisioning pipe server")?; + crate::write_provisioning_frame(&mut pipe, &request) + .context("send sandbox provisioning request")?; + crate::framed_io::wait_for_complete_frame(&pipe, deadline) + .context("wait for sandbox provisioning response")?; + crate::read_provisioning_frame(&mut pipe) + .context("read sandbox provisioning response")? + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::UnexpectedEof, + "sandbox provisioning service closed the pipe without a response", + ) + }) + .context("read sandbox provisioning response") + })(); + let response = match response { + Ok(response) => response, + Err(error) + if error.downcast_ref::().is_some_and(|error| { + matches!( + error.kind(), + io::ErrorKind::UnexpectedEof | io::ErrorKind::TimedOut + ) || matches!( + error.raw_os_error(), + Some(code) + if code == ERROR_BROKEN_PIPE as i32 + || code == ERROR_NO_DATA as i32 + || code == ERROR_PIPE_NOT_CONNECTED as i32 + ) + }) => + { + return Ok(WindowsSandboxProvisioningOutcome::Unavailable); + } + Err(error) => return Err(error), + }; + + if response.version != crate::PROVISIONING_PROTOCOL_VERSION { + return Ok(WindowsSandboxProvisioningOutcome::Unavailable); + } + let crate::ProvisioningMessage::ProvisionSandboxResponse { payload } = response.message else { + bail!("unexpected sandbox provisioning response message"); + }; + match payload { + crate::SandboxProvisioningResponse::Ok => { + Ok(WindowsSandboxProvisioningOutcome::Provisioned) + } + crate::SandboxProvisioningResponse::Error { message } => Err(anyhow!(message)), + } +} + +fn connect(deadline: Instant) -> anyhow::Result> { + let open_pipe = || { + OpenOptions::new() + .read(true) + .write(true) + .custom_flags(SECURITY_SQOS_PRESENT | SECURITY_IMPERSONATION) + .open(crate::SANDBOX_PROVISIONING_PIPE_NAME) + }; + + let pipe_name = crate::to_wide(crate::SANDBOX_PROVISIONING_PIPE_NAME); + loop { + match open_pipe() { + Ok(pipe) => return Ok(Some(pipe)), + Err(error) if error.raw_os_error() == Some(ERROR_FILE_NOT_FOUND as i32) => { + return Ok(None); + } + Err(error) if error.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(None); + } + let wait_ms = u32::try_from(remaining.as_millis()) + .unwrap_or(u32::MAX) + .max(1); + if unsafe { WaitNamedPipeW(pipe_name.as_ptr(), wait_ms) } == 0 { + let error = io::Error::last_os_error(); + if matches!( + error.raw_os_error(), + Some(code) + if code == ERROR_FILE_NOT_FOUND as i32 + || code == ERROR_SEM_TIMEOUT as i32 + ) { + return Ok(None); + } + return Err(error).context("wait for sandbox provisioning pipe"); + } + } + Err(error) => return Err(error).context("open sandbox provisioning pipe"), + } + } +} + +fn verify_server(pipe: HANDLE) -> anyhow::Result<()> { + let mut pipe_process_id = 0; + if unsafe { GetNamedPipeServerProcessId(pipe, &mut pipe_process_id) } == 0 { + return Err(io::Error::last_os_error()).context("identify provisioning pipe server"); + } + + let manager = + unsafe { Services::OpenSCManagerW(ptr::null(), ptr::null(), Services::SC_MANAGER_CONNECT) }; + if manager == 0 { + return Err(io::Error::last_os_error()).context("open service control manager"); + } + let manager = ServiceHandle(manager); + + let service_name = crate::to_wide(SERVICE_NAME); + let service = unsafe { + Services::OpenServiceW( + manager.0, + service_name.as_ptr(), + Services::SERVICE_QUERY_STATUS, + ) + }; + if service == 0 { + return Err(io::Error::last_os_error()).context("open sandbox provisioning service"); + } + let service = ServiceHandle(service); + + let mut status: Services::SERVICE_STATUS_PROCESS = unsafe { std::mem::zeroed() }; + let mut bytes_needed = 0; + if unsafe { + Services::QueryServiceStatusEx( + service.0, + Services::SC_STATUS_PROCESS_INFO, + ptr::from_mut(&mut status).cast(), + size_of::() as u32, + &mut bytes_needed, + ) + } == 0 + { + return Err(io::Error::last_os_error()).context("query sandbox provisioning service"); + } + if status.dwCurrentState != Services::SERVICE_RUNNING + || status.dwProcessId == 0 + || status.dwProcessId != pipe_process_id + { + bail!("the provisioning pipe server does not match the running service"); + } + Ok(()) +} + +struct ServiceHandle(SC_HANDLE); + +impl Drop for ServiceHandle { + fn drop(&mut self) { + if self.0 != 0 { + unsafe { Services::CloseServiceHandle(self.0) }; + } + } +} diff --git a/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs b/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs index fb6dd5250a..ca05a73ef1 100644 --- a/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs +++ b/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs @@ -1,5 +1,6 @@ //! Dedicated protocol exchanged with the Windows sandbox provisioning service. +use crate::WindowsSandboxProvisioningSettings; use anyhow::Result; use serde::Deserialize; use serde::Serialize; @@ -37,9 +38,16 @@ pub enum ProvisioningMessage { #[serde(deny_unknown_fields)] pub struct SandboxProvisioningRequest { pub codex_home: String, - pub http_port: Option, - pub socks_port: Option, - pub allow_local_binding: bool, + pub settings: WindowsSandboxProvisioningSettings, + pub listeners: WindowsSandboxProxyListeners, +} + +/// Known proxy protocols used for managed-policy validation, separate from firewall settings. +#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct WindowsSandboxProxyListeners { + pub http_ports: Vec, + pub socks_ports: Vec, } /// Result returned by the sandbox provisioning service. diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index df3605aa15..86611a0164 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -726,7 +726,7 @@ impl SandboxNetworkIdentity { } } -const PROXY_ENV_KEYS: &[&str] = &[ +pub(crate) const PROXY_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", @@ -793,7 +793,7 @@ pub(crate) fn proxy_ports_from_env(env_map: &HashMap) -> Vec Option { +pub(crate) fn loopback_proxy_port_from_url(url: &str) -> Option { let authority = url.trim().split_once("://")?.1.split('/').next()?; let host_port = authority.rsplit_once('@').map_or(authority, |(_, hp)| hp); @@ -1360,6 +1360,8 @@ mod tests { use super::profile_read_roots; use super::proxy_ports_from_env; use super::verify_setup_completed; + use crate::WindowsSandboxProvisioningSettings; + use crate::WindowsSandboxProxyListeners; use crate::helper_materialization::BIN_DIRNAME; use crate::helper_materialization::RESOURCES_DIRNAME; use crate::helper_materialization::helper_bin_dir; @@ -1822,6 +1824,26 @@ mod tests { ); assert_eq!(proxy_ports_from_env(&env), vec![1081, 8080, 43128, 43129]); + assert_eq!( + WindowsSandboxProvisioningSettings::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProvisioningSettings { + proxy_ports: vec![1081, 8080, 43128, 43129], + allow_local_binding: false, + } + ); + assert_eq!( + WindowsSandboxProxyListeners::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProxyListeners { + http_ports: vec![8080], + socks_ports: vec![1081], + } + ); } #[test] @@ -1843,6 +1865,20 @@ mod tests { allow_local_binding: false, } ); + let permission_profile = PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Enabled, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); + assert_eq!( + WindowsSandboxProvisioningSettings::from_environment(&permission_profile, &env), + WindowsSandboxProvisioningSettings::default() + ); + assert_eq!( + WindowsSandboxProxyListeners::from_environment(&permission_profile, &env), + WindowsSandboxProxyListeners::default() + ); } #[test] @@ -1862,12 +1898,104 @@ mod tests { ); assert_eq!( - offline_proxy_settings_from_env(&env, super::SandboxNetworkIdentity::Offline), - super::OfflineProxySettings { + WindowsSandboxProvisioningSettings::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProvisioningSettings { proxy_ports: vec![1081, 8080], allow_local_binding: true, } ); + assert_eq!( + WindowsSandboxProxyListeners::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProxyListeners { + http_ports: vec![8080], + socks_ports: vec![1081], + } + ); + + env.remove("ALL_PROXY"); + for (all_proxy, socks_ports) in [ + ("HTTP://localhost:8080", vec![]), + ("socks5h://[::1]:8080", vec![8080]), + ] { + env.insert("all_proxy".to_string(), all_proxy.to_string()); + assert_eq!( + WindowsSandboxProxyListeners::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProxyListeners { + http_ports: vec![8080], + socks_ports, + } + ); + } + } + + #[test] + fn provisioning_settings_preserve_all_inherited_proxy_ports() { + for (proxy_env, proxy_ports, http_ports, socks_ports) in [ + ( + vec![("ALL_PROXY", "socks5h://127.0.0.1:1081")], + vec![1081], + vec![], + vec![1081], + ), + ( + vec![ + ("HTTP_PROXY", "http://127.0.0.1:8080"), + ("HTTPS_PROXY", "http://127.0.0.1:3128"), + ], + vec![3128, 8080], + vec![3128, 8080], + vec![], + ), + ( + vec![ + ("HTTP_PROXY", "http://127.0.0.1:8080"), + (WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY, "8080,1081"), + ], + vec![1081, 8080], + vec![8080], + vec![], + ), + ( + vec![("ALL_PROXY", "ftp://127.0.0.1:3128")], + vec![3128], + vec![], + vec![], + ), + ] { + let env = proxy_env + .into_iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect(); + assert_eq!( + WindowsSandboxProvisioningSettings::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProvisioningSettings { + proxy_ports, + allow_local_binding: false, + } + ); + assert_eq!( + WindowsSandboxProxyListeners::from_environment( + &PermissionProfile::workspace_write(), + &env, + ), + WindowsSandboxProxyListeners { + http_ports, + socks_ports, + } + ); + } } #[test]