mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary This PR installs a first wave of WFP (Windows Filtering Platform) filters that reduce the surface area of network egress vulnerabilities for the Windows Sandbox. - Add persistent Windows Filtering Platform provider, sublayer, and filters for the Windows sandbox offline account. - Install WFP filters during elevated full setup, log failures non-fatally, and emit setup metrics when analytics are enabled. - Bump the Windows sandbox setup version so existing users rerun full setup and receive the new filters. ## What WFP is Windows Filtering Platform (WFP) is the low-level Windows networking policy engine underneath things like Windows Firewall. It lets privileged code install persistent filtering rules at specific network stack layers, with conditions like "only traffic from this Windows account" or "only this remote port," and an action like block. In this change, we create a Codex-owned persistent WFP provider and sublayer, then install block filters scoped to the Windows sandbox's offline user account via `ALE_USER_ID`. That means the filters are targeted at sandboxed processes running as that account, rather than globally affecting the host. ## Initial filter set We are starting with 12 concrete WFP filters across a few high-value bypass surfaces. The table below describes the filter families rather than one filter per row: | Area | Concrete filters | Purpose | | --- | --- | --- | | ICMP | 4 filters: ICMP v4/v6 on `ALE_AUTH_CONNECT` and `ALE_RESOURCE_ASSIGNMENT` | Block direct ping-style network reachability checks from the offline account. | | DNS | 2 filters: remote port `53` on `ALE_AUTH_CONNECT_V4/V6` | Block direct DNS queries that bypass our intended proxy/offline path. | | DNS-over-TLS | 2 filters: remote port `853` on `ALE_AUTH_CONNECT_V4/V6` | Block encrypted DNS attempts that could bypass ordinary DNS interception. | | SMB / NetBIOS | 4 filters: remote ports `445` and `139` on `ALE_AUTH_CONNECT_V4/V6` | Block Windows file-sharing/network share traffic from sandboxed processes. | For IPv4/IPv6 coverage, the port-based filters are installed on both `ALE_AUTH_CONNECT_V4` and `ALE_AUTH_CONNECT_V6`. ICMP also gets both connect-layer and resource-assignment-layer coverage because ICMP traffic is shaped differently from ordinary TCP/UDP port traffic. ## Validation - `cargo fmt -p codex-windows-sandbox` (completed with existing stable-rustfmt warnings about `imports_granularity = Item`) - `cargo test -p codex-windows-sandbox wfp::tests` - `cargo test -p codex-windows-sandbox` (fails in existing legacy PowerShell sandbox tests because `Microsoft.PowerShell.Utility` could not be loaded; WFP tests passed before that failure)
410 lines
15 KiB
Rust
410 lines
15 KiB
Rust
#![cfg(target_os = "windows")]
|
|
|
|
#[path = "wfp_filter_specs.rs"]
|
|
mod filter_specs;
|
|
|
|
use crate::to_wide;
|
|
use anyhow::Result;
|
|
use std::ffi::OsStr;
|
|
use std::mem::zeroed;
|
|
use std::ptr::null;
|
|
use std::ptr::null_mut;
|
|
use windows_sys::core::GUID;
|
|
use windows_sys::Win32::Foundation::FWP_E_ALREADY_EXISTS;
|
|
use windows_sys::Win32::Foundation::FWP_E_FILTER_NOT_FOUND;
|
|
use windows_sys::Win32::Foundation::FWP_E_NOT_FOUND;
|
|
use windows_sys::Win32::Foundation::HANDLE;
|
|
use windows_sys::Win32::Foundation::HLOCAL;
|
|
use windows_sys::Win32::Foundation::LocalFree;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_ACTRL_MATCH_FILTER;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_ACTION_BLOCK;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_BYTE_BLOB;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_EMPTY;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_MATCH_EQUAL;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_SECURITY_DESCRIPTOR_TYPE;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_UINT16;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_UINT8;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_VALUE0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_ACTION0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_ACTION0_0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_CONDITION_ALE_USER_ID;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_CONDITION_IP_PROTOCOL;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_CONDITION_IP_REMOTE_PORT;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_DISPLAY_DATA0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER0_0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER_CONDITION0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER_FLAG_PERSISTENT;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_PROVIDER0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_PROVIDER_FLAG_PERSISTENT;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SESSION0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SUBLAYER0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SUBLAYER_FLAG_PERSISTENT;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmEngineClose0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmEngineOpen0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmFilterAdd0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmFilterDeleteByKey0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmProviderAdd0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmSubLayerAdd0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmTransactionAbort0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmTransactionBegin0;
|
|
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmTransactionCommit0;
|
|
use windows_sys::Win32::Security::Authorization::BuildExplicitAccessWithNameW;
|
|
use windows_sys::Win32::Security::Authorization::BuildSecurityDescriptorW;
|
|
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
|
use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
|
|
use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR;
|
|
use windows_sys::Win32::System::Rpc::RPC_C_AUTHN_DEFAULT;
|
|
use windows_sys::Win32::System::Threading::INFINITE;
|
|
|
|
use filter_specs::ConditionSpec;
|
|
use filter_specs::FILTER_SPECS;
|
|
use filter_specs::FilterSpec;
|
|
|
|
const SESSION_NAME: &str = "Codex Windows Sandbox WFP";
|
|
const PROVIDER_NAME: &str = "Codex Windows Sandbox WFP";
|
|
const PROVIDER_DESCRIPTION: &str = "Persistent WFP provider for Codex Windows sandbox filters";
|
|
const SUBLAYER_NAME: &str = "Codex Windows Sandbox WFP";
|
|
const SUBLAYER_DESCRIPTION: &str = "Persistent WFP sublayer for Codex Windows sandbox filters";
|
|
|
|
// WFP identifies persistent providers, sublayers, and filters by stable GUIDs.
|
|
// These values are Codex-owned identities; do not regenerate them unless we
|
|
// intentionally want to orphan old objects and create a new WFP namespace.
|
|
const PROVIDER_KEY: GUID = GUID::from_u128(0x2e31d31c_3948_4753_9117_e5d1a6496f41);
|
|
const SUBLAYER_KEY: GUID = GUID::from_u128(0xe65054fd_4d32_4c7c_95ef_621f0cf6431a);
|
|
|
|
/// Installs the persistent Codex WFP filters for `account`.
|
|
///
|
|
/// This is intended to run from the already-elevated setup helper. Callers
|
|
/// should treat any returned error as non-fatal to the rest of setup.
|
|
pub fn install_wfp_filters_for_account(account: &str) -> Result<usize> {
|
|
let engine = Engine::open()?;
|
|
let mut transaction = engine.begin_transaction()?;
|
|
ensure_provider(engine.handle)?;
|
|
ensure_sublayer(engine.handle)?;
|
|
|
|
let user_condition = UserMatchCondition::for_account(account)?;
|
|
let mut installed_filter_count = 0;
|
|
for spec in FILTER_SPECS {
|
|
delete_filter_if_present(engine.handle, &spec.key)?;
|
|
add_filter(engine.handle, spec, &user_condition)?;
|
|
installed_filter_count += 1;
|
|
}
|
|
|
|
transaction.commit()?;
|
|
Ok(installed_filter_count)
|
|
}
|
|
|
|
/// Owns an open WFP engine handle and closes it on drop.
|
|
struct Engine {
|
|
handle: HANDLE,
|
|
}
|
|
|
|
impl Engine {
|
|
fn open() -> Result<Self> {
|
|
let session_name = to_wide(OsStr::new(SESSION_NAME));
|
|
let mut session: FWPM_SESSION0 = unsafe { zeroed() };
|
|
session.displayData = FWPM_DISPLAY_DATA0 {
|
|
name: session_name.as_ptr() as *mut _,
|
|
description: null_mut(),
|
|
};
|
|
session.txnWaitTimeoutInMSec = INFINITE;
|
|
|
|
let mut handle = HANDLE::default();
|
|
let result = unsafe {
|
|
FwpmEngineOpen0(
|
|
null(),
|
|
RPC_C_AUTHN_DEFAULT as u32,
|
|
null(),
|
|
&session,
|
|
&mut handle,
|
|
)
|
|
};
|
|
ensure_success(result, "FwpmEngineOpen0")?;
|
|
Ok(Self { handle })
|
|
}
|
|
|
|
fn begin_transaction(&self) -> Result<Transaction<'_>> {
|
|
let result = unsafe { FwpmTransactionBegin0(self.handle, 0) };
|
|
ensure_success(result, "FwpmTransactionBegin0")?;
|
|
Ok(Transaction {
|
|
engine: self,
|
|
committed: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for Engine {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
FwpmEngineClose0(self.handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Aborts an open WFP transaction unless it was explicitly committed.
|
|
struct Transaction<'a> {
|
|
engine: &'a Engine,
|
|
committed: bool,
|
|
}
|
|
|
|
impl Transaction<'_> {
|
|
fn commit(&mut self) -> Result<()> {
|
|
let result = unsafe { FwpmTransactionCommit0(self.engine.handle) };
|
|
ensure_success(result, "FwpmTransactionCommit0")?;
|
|
self.committed = true;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for Transaction<'_> {
|
|
fn drop(&mut self) {
|
|
if !self.committed {
|
|
unsafe {
|
|
FwpmTransactionAbort0(self.engine.handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Builds the ALE_USER_ID condition blob that scopes filters to one account.
|
|
struct UserMatchCondition {
|
|
security_descriptor: PSECURITY_DESCRIPTOR,
|
|
blob: FWP_BYTE_BLOB,
|
|
}
|
|
|
|
impl UserMatchCondition {
|
|
fn for_account(account: &str) -> Result<Self> {
|
|
let account_w = to_wide(OsStr::new(account));
|
|
let mut access: EXPLICIT_ACCESS_W = unsafe { zeroed() };
|
|
unsafe {
|
|
BuildExplicitAccessWithNameW(
|
|
&mut access,
|
|
account_w.as_ptr(),
|
|
FWP_ACTRL_MATCH_FILTER,
|
|
GRANT_ACCESS,
|
|
0,
|
|
);
|
|
}
|
|
|
|
let mut security_descriptor: PSECURITY_DESCRIPTOR = null_mut();
|
|
let mut security_descriptor_len = 0;
|
|
let result = unsafe {
|
|
BuildSecurityDescriptorW(
|
|
null(),
|
|
null(),
|
|
1,
|
|
&access,
|
|
0,
|
|
null(),
|
|
null_mut(),
|
|
&mut security_descriptor_len,
|
|
&mut security_descriptor,
|
|
)
|
|
};
|
|
ensure_success(result, "BuildSecurityDescriptorW")?;
|
|
|
|
Ok(Self {
|
|
security_descriptor,
|
|
blob: FWP_BYTE_BLOB {
|
|
size: security_descriptor_len,
|
|
data: security_descriptor as *mut u8,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for UserMatchCondition {
|
|
fn drop(&mut self) {
|
|
if !self.security_descriptor.is_null() {
|
|
unsafe {
|
|
LocalFree(self.security_descriptor as HLOCAL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ensures the persistent Codex WFP provider exists.
|
|
fn ensure_provider(engine: HANDLE) -> Result<()> {
|
|
let provider_name = to_wide(OsStr::new(PROVIDER_NAME));
|
|
let provider_description = to_wide(OsStr::new(PROVIDER_DESCRIPTION));
|
|
let provider = FWPM_PROVIDER0 {
|
|
providerKey: PROVIDER_KEY,
|
|
displayData: FWPM_DISPLAY_DATA0 {
|
|
name: provider_name.as_ptr() as *mut _,
|
|
description: provider_description.as_ptr() as *mut _,
|
|
},
|
|
flags: FWPM_PROVIDER_FLAG_PERSISTENT,
|
|
providerData: empty_blob(),
|
|
serviceName: null_mut(),
|
|
};
|
|
|
|
let result = unsafe { FwpmProviderAdd0(engine, &provider, null_mut()) };
|
|
ensure_success_or(result, "FwpmProviderAdd0", &[FWP_E_ALREADY_EXISTS as u32])
|
|
}
|
|
|
|
/// Ensures the persistent Codex sublayer exists under the Codex provider.
|
|
fn ensure_sublayer(engine: HANDLE) -> Result<()> {
|
|
let sublayer_name = to_wide(OsStr::new(SUBLAYER_NAME));
|
|
let sublayer_description = to_wide(OsStr::new(SUBLAYER_DESCRIPTION));
|
|
let provider_key = PROVIDER_KEY;
|
|
let sublayer = FWPM_SUBLAYER0 {
|
|
subLayerKey: SUBLAYER_KEY,
|
|
displayData: FWPM_DISPLAY_DATA0 {
|
|
name: sublayer_name.as_ptr() as *mut _,
|
|
description: sublayer_description.as_ptr() as *mut _,
|
|
},
|
|
flags: FWPM_SUBLAYER_FLAG_PERSISTENT,
|
|
providerKey: &provider_key as *const _ as *mut _,
|
|
providerData: empty_blob(),
|
|
weight: 0x8000,
|
|
};
|
|
|
|
let result = unsafe { FwpmSubLayerAdd0(engine, &sublayer, null_mut()) };
|
|
ensure_success_or(result, "FwpmSubLayerAdd0", &[FWP_E_ALREADY_EXISTS as u32])
|
|
}
|
|
|
|
/// Adds one blocking WFP filter from the static filter spec list.
|
|
fn add_filter(engine: HANDLE, spec: &FilterSpec, user_condition: &UserMatchCondition) -> Result<()> {
|
|
let filter_name = to_wide(OsStr::new(spec.name));
|
|
let filter_description = to_wide(OsStr::new(spec.description));
|
|
let mut filter_conditions = build_conditions(spec.conditions, user_condition);
|
|
let provider_key = PROVIDER_KEY;
|
|
let filter = FWPM_FILTER0 {
|
|
filterKey: spec.key,
|
|
displayData: FWPM_DISPLAY_DATA0 {
|
|
name: filter_name.as_ptr() as *mut _,
|
|
description: filter_description.as_ptr() as *mut _,
|
|
},
|
|
flags: FWPM_FILTER_FLAG_PERSISTENT,
|
|
providerKey: &provider_key as *const _ as *mut _,
|
|
providerData: empty_blob(),
|
|
layerKey: spec.layer_key,
|
|
subLayerKey: SUBLAYER_KEY,
|
|
weight: empty_value(),
|
|
numFilterConditions: filter_conditions.len() as u32,
|
|
filterCondition: filter_conditions.as_mut_ptr(),
|
|
action: FWPM_ACTION0 {
|
|
r#type: FWP_ACTION_BLOCK,
|
|
Anonymous: FWPM_ACTION0_0 { filterType: zero_guid() },
|
|
},
|
|
Anonymous: FWPM_FILTER0_0 { rawContext: 0 },
|
|
reserved: null_mut(),
|
|
filterId: 0,
|
|
effectiveWeight: empty_value(),
|
|
};
|
|
|
|
let mut filter_id = 0_u64;
|
|
let result = unsafe { FwpmFilterAdd0(engine, &filter, null_mut(), &mut filter_id) };
|
|
ensure_success(result, &format!("FwpmFilterAdd0({})", spec.name))
|
|
}
|
|
|
|
/// Converts our compact condition specs into WFP filter conditions.
|
|
fn build_conditions(
|
|
specs: &[ConditionSpec],
|
|
user_condition: &UserMatchCondition,
|
|
) -> Vec<FWPM_FILTER_CONDITION0> {
|
|
specs
|
|
.iter()
|
|
.map(|spec| match spec {
|
|
ConditionSpec::User => FWPM_FILTER_CONDITION0 {
|
|
fieldKey: FWPM_CONDITION_ALE_USER_ID,
|
|
matchType: FWP_MATCH_EQUAL,
|
|
conditionValue: FWP_CONDITION_VALUE0 {
|
|
r#type: FWP_SECURITY_DESCRIPTOR_TYPE,
|
|
Anonymous: FWP_CONDITION_VALUE0_0 {
|
|
sd: &user_condition.blob as *const _ as *mut _,
|
|
},
|
|
},
|
|
},
|
|
ConditionSpec::Protocol(protocol) => FWPM_FILTER_CONDITION0 {
|
|
fieldKey: FWPM_CONDITION_IP_PROTOCOL,
|
|
matchType: FWP_MATCH_EQUAL,
|
|
conditionValue: FWP_CONDITION_VALUE0 {
|
|
r#type: FWP_UINT8,
|
|
Anonymous: FWP_CONDITION_VALUE0_0 { uint8: *protocol },
|
|
},
|
|
},
|
|
ConditionSpec::RemotePort(port) => FWPM_FILTER_CONDITION0 {
|
|
fieldKey: FWPM_CONDITION_IP_REMOTE_PORT,
|
|
matchType: FWP_MATCH_EQUAL,
|
|
conditionValue: FWP_CONDITION_VALUE0 {
|
|
r#type: FWP_UINT16,
|
|
Anonymous: FWP_CONDITION_VALUE0_0 { uint16: *port },
|
|
},
|
|
},
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Deletes an old copy of a filter before re-adding it.
|
|
fn delete_filter_if_present(engine: HANDLE, key: &GUID) -> Result<()> {
|
|
let result = unsafe { FwpmFilterDeleteByKey0(engine, key) };
|
|
ensure_success_or(
|
|
result,
|
|
"FwpmFilterDeleteByKey0",
|
|
&[FWP_E_FILTER_NOT_FOUND as u32, FWP_E_NOT_FOUND as u32],
|
|
)
|
|
}
|
|
|
|
fn ensure_success(result: u32, operation: &str) -> Result<()> {
|
|
ensure_success_or(result, operation, &[])
|
|
}
|
|
|
|
fn ensure_success_or(result: u32, operation: &str, allowed: &[u32]) -> Result<()> {
|
|
if result == 0 || allowed.contains(&result) {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"{operation} failed: {}",
|
|
format_error_code(result)
|
|
))
|
|
}
|
|
}
|
|
|
|
fn format_error_code(result: u32) -> String {
|
|
format!("0x{result:08X}")
|
|
}
|
|
|
|
fn empty_blob() -> FWP_BYTE_BLOB {
|
|
FWP_BYTE_BLOB {
|
|
size: 0,
|
|
data: null_mut(),
|
|
}
|
|
}
|
|
|
|
fn empty_value() -> FWP_VALUE0 {
|
|
FWP_VALUE0 {
|
|
r#type: FWP_EMPTY,
|
|
Anonymous: unsafe { zeroed() },
|
|
}
|
|
}
|
|
|
|
fn zero_guid() -> GUID {
|
|
GUID::from_u128(0)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::FILTER_SPECS;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeSet;
|
|
|
|
#[test]
|
|
fn filter_keys_are_unique() {
|
|
let keys = FILTER_SPECS
|
|
.iter()
|
|
.map(|spec| (spec.key.data1, spec.key.data2, spec.key.data3, spec.key.data4))
|
|
.collect::<BTreeSet<_>>();
|
|
assert_eq!(keys.len(), FILTER_SPECS.len());
|
|
}
|
|
|
|
#[test]
|
|
fn filter_names_are_unique() {
|
|
let names = FILTER_SPECS.iter().map(|spec| spec.name).collect::<BTreeSet<_>>();
|
|
assert_eq!(names.len(), FILTER_SPECS.len());
|
|
}
|
|
}
|