mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
## Why Elevated Windows sandboxes need stable managed-proxy ports while preserving the network policy and environment attribution of each sandboxed process. ## What changed - Keep shared HTTP and SOCKS5 loopback ingress listeners alive across managed-proxy instances. - Add a per-route restricting SID to elevated sandbox tokens and dispatch incoming connections to the matching proxy policy after attributing the client process. - Reject connections without exactly one registered route, remove routes when their proxy handle is dropped, and keep unsandboxed Windows launches off the managed ingress. - Provision the elevated sandbox with the configured proxy ports and local-binding setting, honoring the selected profile and CLI overrides. ## Testing - Add Windows unit tests for TCP ownership attribution, route selection, restricting-token propagation, and setup settings. - Add an end-to-end Windows test covering stable ports, isolated environment policies, HTTP and SOCKS5 routing, and route teardown. GitOrigin-RevId: 783fac6e0f904dc9bb1955b75d4a5895e8bb9690
316 lines
10 KiB
Rust
316 lines
10 KiB
Rust
use std::ffi::c_void;
|
|
use std::io;
|
|
use std::mem::offset_of;
|
|
use std::mem::size_of;
|
|
use std::net::Ipv4Addr;
|
|
use std::net::SocketAddr;
|
|
use std::net::SocketAddrV4;
|
|
use std::os::windows::io::AsRawHandle;
|
|
use std::os::windows::io::FromRawHandle;
|
|
use std::os::windows::io::OwnedHandle;
|
|
use std::os::windows::io::RawHandle;
|
|
|
|
use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
|
|
use windows_sys::Win32::Foundation::GetLastError;
|
|
use windows_sys::Win32::Foundation::HANDLE;
|
|
use windows_sys::Win32::Foundation::HLOCAL;
|
|
use windows_sys::Win32::Foundation::LocalFree;
|
|
use windows_sys::Win32::Foundation::NO_ERROR;
|
|
use windows_sys::Win32::Foundation::PSID;
|
|
use windows_sys::Win32::NetworkManagement::IpHelper::GetExtendedTcpTable;
|
|
use windows_sys::Win32::NetworkManagement::IpHelper::MIB_TCPROW_OWNER_PID;
|
|
use windows_sys::Win32::NetworkManagement::IpHelper::MIB_TCPTABLE_OWNER_PID;
|
|
use windows_sys::Win32::NetworkManagement::IpHelper::TCP_TABLE_OWNER_PID_CONNECTIONS;
|
|
use windows_sys::Win32::Networking::WinSock::AF_INET;
|
|
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
|
|
use windows_sys::Win32::Security::GetTokenInformation;
|
|
use windows_sys::Win32::Security::SID_AND_ATTRIBUTES;
|
|
use windows_sys::Win32::Security::TOKEN_GROUPS;
|
|
use windows_sys::Win32::Security::TOKEN_QUERY;
|
|
use windows_sys::Win32::Security::TokenRestrictedSids;
|
|
use windows_sys::Win32::System::Threading::OpenProcess;
|
|
use windows_sys::Win32::System::Threading::OpenProcessToken;
|
|
use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
|
|
|
|
/// Returns the restricting SIDs on the process that opened an accepted loopback connection.
|
|
///
|
|
/// `accepted_local_addr` and `accepted_peer_addr` must come from the accepted server socket. The
|
|
/// owning-PID table describes the client side in the opposite direction, so the lookup matches the
|
|
/// exact reversed four-tuple.
|
|
pub(crate) fn restricting_sids_for_tcp_connection(
|
|
accepted_local_addr: SocketAddr,
|
|
accepted_peer_addr: SocketAddr,
|
|
) -> io::Result<Vec<String>> {
|
|
let (SocketAddr::V4(accepted_local_addr), SocketAddr::V4(accepted_peer_addr)) =
|
|
(accepted_local_addr, accepted_peer_addr)
|
|
else {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"Windows proxy connection attribution currently supports IPv4 only",
|
|
));
|
|
};
|
|
|
|
let process_id = owning_process_id(accepted_local_addr, accepted_peer_addr)?;
|
|
restricting_sids_for_process(process_id)
|
|
}
|
|
|
|
fn owning_process_id(
|
|
accepted_local_addr: SocketAddrV4,
|
|
accepted_peer_addr: SocketAddrV4,
|
|
) -> io::Result<u32> {
|
|
let mut byte_len = 0_u32;
|
|
let result = unsafe {
|
|
GetExtendedTcpTable(
|
|
std::ptr::null_mut(),
|
|
&mut byte_len,
|
|
0,
|
|
AF_INET as u32,
|
|
TCP_TABLE_OWNER_PID_CONNECTIONS,
|
|
0,
|
|
)
|
|
};
|
|
if result != ERROR_INSUFFICIENT_BUFFER {
|
|
return Err(win32_error("query IPv4 TCP owner table size", result));
|
|
}
|
|
|
|
let buffer = loop {
|
|
let mut buffer = aligned_buffer(byte_len as usize)?;
|
|
let result = unsafe {
|
|
GetExtendedTcpTable(
|
|
buffer.as_mut_ptr().cast::<c_void>(),
|
|
&mut byte_len,
|
|
0,
|
|
AF_INET as u32,
|
|
TCP_TABLE_OWNER_PID_CONNECTIONS,
|
|
0,
|
|
)
|
|
};
|
|
match result {
|
|
NO_ERROR => break buffer,
|
|
ERROR_INSUFFICIENT_BUFFER => continue,
|
|
_ => return Err(win32_error("read IPv4 TCP owner table", result)),
|
|
}
|
|
};
|
|
|
|
let rows = parse_tcp_owner_rows(&buffer, byte_len as usize)?;
|
|
unique_client_process_id(rows, accepted_local_addr, accepted_peer_addr)
|
|
}
|
|
|
|
fn parse_tcp_owner_rows(buffer: &[usize], byte_len: usize) -> io::Result<&[MIB_TCPROW_OWNER_PID]> {
|
|
let rows_offset = offset_of!(MIB_TCPTABLE_OWNER_PID, table);
|
|
if byte_len > size_of_val(buffer) || byte_len < rows_offset {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"invalid IPv4 TCP owner table length",
|
|
));
|
|
}
|
|
|
|
let row_count = unsafe { std::ptr::read_unaligned(buffer.as_ptr().cast::<u32>()) } as usize;
|
|
let rows_byte_len = row_count
|
|
.checked_mul(size_of::<MIB_TCPROW_OWNER_PID>())
|
|
.and_then(|len| rows_offset.checked_add(len))
|
|
.ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"IPv4 TCP owner table length overflow",
|
|
)
|
|
})?;
|
|
if rows_byte_len > byte_len {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"truncated IPv4 TCP owner table",
|
|
));
|
|
}
|
|
|
|
let rows = unsafe {
|
|
let rows_ptr = buffer
|
|
.as_ptr()
|
|
.cast::<u8>()
|
|
.add(rows_offset)
|
|
.cast::<MIB_TCPROW_OWNER_PID>();
|
|
std::slice::from_raw_parts(rows_ptr, row_count)
|
|
};
|
|
Ok(rows)
|
|
}
|
|
|
|
fn unique_client_process_id(
|
|
rows: &[MIB_TCPROW_OWNER_PID],
|
|
accepted_local_addr: SocketAddrV4,
|
|
accepted_peer_addr: SocketAddrV4,
|
|
) -> io::Result<u32> {
|
|
let mut matching_process_ids = rows
|
|
.iter()
|
|
.filter(|row| client_row_matches(row, accepted_local_addr, accepted_peer_addr))
|
|
.map(|row| row.dwOwningPid);
|
|
let process_id = matching_process_ids.next().ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"accepted connection is absent from the IPv4 TCP owner table",
|
|
)
|
|
})?;
|
|
if matching_process_ids.next().is_some() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"accepted connection has multiple IPv4 TCP owner rows",
|
|
));
|
|
}
|
|
Ok(process_id)
|
|
}
|
|
|
|
fn client_row_matches(
|
|
row: &MIB_TCPROW_OWNER_PID,
|
|
accepted_local_addr: SocketAddrV4,
|
|
accepted_peer_addr: SocketAddrV4,
|
|
) -> bool {
|
|
ipv4_addr_matches(row.dwLocalAddr, *accepted_peer_addr.ip())
|
|
&& tcp_port(row.dwLocalPort) == accepted_peer_addr.port()
|
|
&& ipv4_addr_matches(row.dwRemoteAddr, *accepted_local_addr.ip())
|
|
&& tcp_port(row.dwRemotePort) == accepted_local_addr.port()
|
|
}
|
|
|
|
fn ipv4_addr_matches(table_addr: u32, socket_addr: Ipv4Addr) -> bool {
|
|
table_addr.to_ne_bytes() == socket_addr.octets()
|
|
}
|
|
|
|
fn tcp_port(table_port: u32) -> u16 {
|
|
u16::from_be(table_port as u16)
|
|
}
|
|
|
|
fn restricting_sids_for_process(process_id: u32) -> io::Result<Vec<String>> {
|
|
let process_handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id) };
|
|
let process = owned_handle(process_handle, "open proxy client process")?;
|
|
|
|
let mut token_handle: HANDLE = 0;
|
|
let opened = unsafe {
|
|
OpenProcessToken(
|
|
process.as_raw_handle() as HANDLE,
|
|
TOKEN_QUERY,
|
|
&mut token_handle,
|
|
)
|
|
};
|
|
if opened == 0 {
|
|
return Err(last_error("open proxy client process token"));
|
|
}
|
|
let token = owned_handle(token_handle, "open proxy client process token")?;
|
|
|
|
let mut byte_len = 0_u32;
|
|
let queried = unsafe {
|
|
GetTokenInformation(
|
|
token.as_raw_handle() as HANDLE,
|
|
TokenRestrictedSids,
|
|
std::ptr::null_mut(),
|
|
0,
|
|
&mut byte_len,
|
|
)
|
|
};
|
|
if queried != 0 || unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER {
|
|
return Err(last_error("query proxy client restricting SID buffer size"));
|
|
}
|
|
|
|
let mut buffer = aligned_buffer(byte_len as usize)?;
|
|
let queried = unsafe {
|
|
GetTokenInformation(
|
|
token.as_raw_handle() as HANDLE,
|
|
TokenRestrictedSids,
|
|
buffer.as_mut_ptr().cast::<c_void>(),
|
|
byte_len,
|
|
&mut byte_len,
|
|
)
|
|
};
|
|
if queried == 0 {
|
|
return Err(last_error("read proxy client restricting SIDs"));
|
|
}
|
|
|
|
parse_token_groups(&buffer, byte_len as usize)?
|
|
.iter()
|
|
.map(|entry| sid_to_string(entry.Sid))
|
|
.collect()
|
|
}
|
|
|
|
fn parse_token_groups(buffer: &[usize], byte_len: usize) -> io::Result<&[SID_AND_ATTRIBUTES]> {
|
|
let groups_offset = offset_of!(TOKEN_GROUPS, Groups);
|
|
if byte_len > size_of_val(buffer) || byte_len < groups_offset {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"invalid restricting SID buffer length",
|
|
));
|
|
}
|
|
|
|
let group_count = unsafe { std::ptr::read_unaligned(buffer.as_ptr().cast::<u32>()) } as usize;
|
|
let groups_byte_len = group_count
|
|
.checked_mul(size_of::<SID_AND_ATTRIBUTES>())
|
|
.and_then(|len| groups_offset.checked_add(len))
|
|
.ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"restricting SID buffer length overflow",
|
|
)
|
|
})?;
|
|
if groups_byte_len > byte_len {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"truncated restricting SID buffer",
|
|
));
|
|
}
|
|
|
|
let groups = unsafe {
|
|
let groups_ptr = buffer
|
|
.as_ptr()
|
|
.cast::<u8>()
|
|
.add(groups_offset)
|
|
.cast::<SID_AND_ATTRIBUTES>();
|
|
std::slice::from_raw_parts(groups_ptr, group_count)
|
|
};
|
|
Ok(groups)
|
|
}
|
|
|
|
fn sid_to_string(sid: PSID) -> io::Result<String> {
|
|
let mut string_sid = std::ptr::null_mut();
|
|
if unsafe { ConvertSidToStringSidW(sid, &mut string_sid) } == 0 {
|
|
return Err(last_error("convert proxy client restricting SID to string"));
|
|
}
|
|
|
|
let value = unsafe {
|
|
let mut len = 0;
|
|
while *string_sid.add(len) != 0 {
|
|
len += 1;
|
|
}
|
|
String::from_utf16_lossy(std::slice::from_raw_parts(string_sid, len))
|
|
};
|
|
unsafe {
|
|
LocalFree(string_sid as HLOCAL);
|
|
}
|
|
Ok(value)
|
|
}
|
|
|
|
fn aligned_buffer(byte_len: usize) -> io::Result<Vec<usize>> {
|
|
if byte_len == 0 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"Windows API returned an empty buffer length",
|
|
));
|
|
}
|
|
Ok(vec![0; byte_len.div_ceil(size_of::<usize>())])
|
|
}
|
|
|
|
fn owned_handle(handle: HANDLE, operation: &str) -> io::Result<OwnedHandle> {
|
|
if handle == 0 {
|
|
return Err(last_error(operation));
|
|
}
|
|
Ok(unsafe { OwnedHandle::from_raw_handle(handle as RawHandle) })
|
|
}
|
|
|
|
fn win32_error(operation: &str, error_code: u32) -> io::Error {
|
|
let error = io::Error::from_raw_os_error(error_code as i32);
|
|
io::Error::new(error.kind(), format!("{operation}: {error}"))
|
|
}
|
|
|
|
fn last_error(operation: &str) -> io::Error {
|
|
let error = io::Error::last_os_error();
|
|
io::Error::new(error.kind(), format!("{operation}: {error}"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "windows_tcp_attribution_tests.rs"]
|
|
mod tests;
|