mirror of
https://github.com/openai/codex.git
synced 2026-09-06 15:29:32 +00:00
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
This commit is contained in:
@@ -89,6 +89,7 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_Registry",
|
||||
"Win32_System_Services",
|
||||
]
|
||||
version = "0.52"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<W: Write, T: Serialize>(mut writer: W, message: &T) -> Result<()> {
|
||||
let payload = serde_json::to_vec(message)?;
|
||||
@@ -37,3 +45,47 @@ pub(crate) fn read_frame<R: Read, T: DeserializeOwned>(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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<u16>,
|
||||
@@ -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;
|
||||
|
||||
277
codex-rs/windows-sandbox-rs/src/provisioning_client.rs
Normal file
277
codex-rs/windows-sandbox-rs/src/provisioning_client.rs
Normal file
@@ -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<String, String>,
|
||||
) -> 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<String, String>,
|
||||
) -> 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<WindowsSandboxProvisioningOutcome> {
|
||||
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<crate::FramedProvisioningMessage> {
|
||||
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::<io::Error>().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<Option<File>> {
|
||||
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::<Services::SERVICE_STATUS_PROCESS>() 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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u16>,
|
||||
pub socks_port: Option<u16>,
|
||||
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<u16>,
|
||||
pub socks_ports: Vec<u16>,
|
||||
}
|
||||
|
||||
/// Result returned by the sandbox provisioning service.
|
||||
|
||||
@@ -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<String, String>) -> Vec<u16
|
||||
ports.into_iter().collect()
|
||||
}
|
||||
|
||||
fn loopback_proxy_port_from_url(url: &str) -> Option<u16> {
|
||||
pub(crate) fn loopback_proxy_port_from_url(url: &str) -> Option<u16> {
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user