From 8121710ffe20d586e32dd3d4e957e5a242c5831e Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Thu, 30 Apr 2026 12:39:01 -0700 Subject: [PATCH] install WFP filters for Windows sandbox setup (#20101) ## 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) --- codex-rs/Cargo.lock | 1 + codex-rs/otel/src/config.rs | 10 + codex-rs/otel/src/lib.rs | 7 + codex-rs/otel/src/metrics/mod.rs | 10 + codex-rs/otel/src/provider.rs | 6 + codex-rs/windows-sandbox-rs/Cargo.toml | 3 + codex-rs/windows-sandbox-rs/src/lib.rs | 6 + .../windows-sandbox-rs/src/setup_main_win.rs | 58 +++ .../src/setup_orchestrator.rs | 3 + codex-rs/windows-sandbox-rs/src/wfp.rs | 409 ++++++++++++++++++ .../src/wfp_filter_specs.rs | 114 +++++ codex-rs/windows-sandbox-rs/src/wfp_setup.rs | 178 ++++++++ 12 files changed, 805 insertions(+) create mode 100644 codex-rs/windows-sandbox-rs/src/wfp.rs create mode 100644 codex-rs/windows-sandbox-rs/src/wfp_filter_specs.rs create mode 100644 codex-rs/windows-sandbox-rs/src/wfp_setup.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2b67bda5c..404ada1ee 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3905,6 +3905,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "chrono", + "codex-otel", "codex-protocol", "codex-utils-absolute-path", "codex-utils-pty", diff --git a/codex-rs/otel/src/config.rs b/codex-rs/otel/src/config.rs index 4f2b82a22..fa088df7d 100644 --- a/codex-rs/otel/src/config.rs +++ b/codex-rs/otel/src/config.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::path::PathBuf; use codex_utils_absolute_path::AbsolutePathBuf; +use serde::Deserialize; +use serde::Serialize; pub(crate) const STATSIG_OTLP_HTTP_ENDPOINT: &str = "https://ab.chatgpt.com/otlp/v1/metrics"; pub(crate) const STATSIG_API_KEY_HEADER: &str = "statsig-api-key"; @@ -44,6 +46,14 @@ pub struct OtelSettings { pub runtime_metrics: bool, } +/// Resolved Statsig metrics settings that another process can use to recreate +/// the built-in metrics exporter configuration without receiving generic +/// exporter credentials in-process. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StatsigMetricsSettings { + pub environment: String, +} + #[derive(Clone, Debug)] pub enum OtelHttpProtocol { /// HTTP protocol with binary protobuf diff --git a/codex-rs/otel/src/lib.rs b/codex-rs/otel/src/lib.rs index 0ea401140..431ed331a 100644 --- a/codex-rs/otel/src/lib.rs +++ b/codex-rs/otel/src/lib.rs @@ -15,6 +15,7 @@ pub use crate::config::OtelExporter; pub use crate::config::OtelHttpProtocol; pub use crate::config::OtelSettings; pub use crate::config::OtelTlsConfig; +pub use crate::config::StatsigMetricsSettings; pub use crate::events::session_telemetry::AuthEnvTelemetryMetadata; pub use crate::events::session_telemetry::SessionTelemetry; pub use crate::events::session_telemetry::SessionTelemetryMetadata; @@ -65,3 +66,9 @@ pub fn start_global_timer(name: &str, tags: &[(&str, &str)]) -> MetricsResult Option { + crate::metrics::global_statsig_settings() +} diff --git a/codex-rs/otel/src/metrics/mod.rs b/codex-rs/otel/src/metrics/mod.rs index bcbb85d35..e75840bf3 100644 --- a/codex-rs/otel/src/metrics/mod.rs +++ b/codex-rs/otel/src/metrics/mod.rs @@ -7,6 +7,7 @@ pub(crate) mod tags; pub(crate) mod timer; pub(crate) mod validation; +use crate::config::StatsigMetricsSettings; pub use crate::metrics::client::MetricsClient; pub use crate::metrics::config::MetricsConfig; pub use crate::metrics::config::MetricsExporter; @@ -17,6 +18,7 @@ use std::sync::OnceLock; pub use tags::SessionMetricTagValues; static GLOBAL_METRICS: OnceLock = OnceLock::new(); +static GLOBAL_STATSIG_METRICS_SETTINGS: OnceLock = OnceLock::new(); pub(crate) fn install_global(metrics: MetricsClient) { let _ = GLOBAL_METRICS.set(metrics); @@ -25,3 +27,11 @@ pub(crate) fn install_global(metrics: MetricsClient) { pub fn global() -> Option { GLOBAL_METRICS.get().cloned() } + +pub(crate) fn install_global_statsig_settings(settings: StatsigMetricsSettings) { + let _ = GLOBAL_STATSIG_METRICS_SETTINGS.set(settings); +} + +pub(crate) fn global_statsig_settings() -> Option { + GLOBAL_STATSIG_METRICS_SETTINGS.get().cloned() +} diff --git a/codex-rs/otel/src/provider.rs b/codex-rs/otel/src/provider.rs index b6df9f5ad..72a1c7c9b 100644 --- a/codex-rs/otel/src/provider.rs +++ b/codex-rs/otel/src/provider.rs @@ -1,6 +1,7 @@ use crate::config::OtelExporter; use crate::config::OtelHttpProtocol; use crate::config::OtelSettings; +use crate::config::StatsigMetricsSettings; use crate::metrics::MetricsClient; use crate::metrics::MetricsConfig; use crate::targets::is_log_export_target; @@ -86,6 +87,11 @@ impl OtelProvider { if let Some(metrics) = metrics.as_ref() { crate::metrics::install_global(metrics.clone()); + if matches!(settings.metrics_exporter, OtelExporter::Statsig) { + crate::metrics::install_global_statsig_settings(StatsigMetricsSettings { + environment: settings.environment.clone(), + }); + } } if !log_enabled && !trace_enabled && metrics.is_none() { diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index e7ca02fed..e45509a96 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -30,6 +30,7 @@ chrono = { version = "0.4.42", default-features = false, features = [ codex-utils-pty = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-string = { workspace = true } +codex-otel = { workspace = true } dunce = "1.0" glob = { workspace = true } serde = { version = "1.0", features = ["derive"] } @@ -74,11 +75,13 @@ features = [ "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_NetworkManagement_NetManagement", + "Win32_NetworkManagement_WindowsFilteringPlatform", "Win32_Networking_WinSock", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_Security_Cryptography", "Win32_Security_Authentication_Identity", + "Win32_System_Rpc", "Win32_Graphics_Gdi", "Win32_System_StationsAndDesktops", "Win32_UI_WindowsAndMessaging", diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 01ac2e781..16b47f293 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -27,6 +27,8 @@ windows_modules!( policy, process, token, + wfp, + wfp_setup, winutil, workspace_acl ); @@ -218,6 +220,10 @@ pub use token::create_workspace_write_token_with_caps_from; #[cfg(target_os = "windows")] pub use token::get_current_token_for_restriction; #[cfg(target_os = "windows")] +pub use wfp::install_wfp_filters_for_account; +#[cfg(target_os = "windows")] +pub use wfp_setup::install_wfp_filters; +#[cfg(target_os = "windows")] pub use windows_impl::CaptureResult; #[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_capture; diff --git a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs index 78c0a8be8..ca3fc1e44 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs @@ -6,6 +6,7 @@ use anyhow::Context; use anyhow::Result; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; +use codex_otel::StatsigMetricsSettings; use codex_windows_sandbox::LOG_FILE_NAME; use codex_windows_sandbox::SETUP_VERSION; use codex_windows_sandbox::SetupErrorCode; @@ -18,6 +19,7 @@ use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance; use codex_windows_sandbox::ensure_allow_write_aces; use codex_windows_sandbox::extract_setup_failure; use codex_windows_sandbox::hide_newly_created_users; +use codex_windows_sandbox::install_wfp_filters; use codex_windows_sandbox::is_command_cwd_root; use codex_windows_sandbox::load_or_create_cap_sids; use codex_windows_sandbox::log_note; @@ -88,6 +90,8 @@ struct Payload { proxy_ports: Vec, #[serde(default)] allow_local_binding: bool, + #[serde(default)] + otel: Option, real_user: String, #[serde(default)] mode: SetupMode, @@ -601,6 +605,14 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( format!("ensure offline outbound block failed: {err}"), ))); } + install_wfp_filters( + &payload.codex_home, + &payload.offline_username, + payload.otel.as_ref(), + |message| { + let _ = log_line(log, message); + }, + ); } if payload.read_roots.is_empty() { @@ -897,3 +909,49 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( log_note("setup binary completed", Some(sbx_dir)); Ok(()) } + +#[cfg(test)] +mod tests { + use super::Payload; + use super::SETUP_VERSION; + use codex_otel::StatsigMetricsSettings; + use pretty_assertions::assert_eq; + use serde_json::json; + + fn payload_json() -> serde_json::Value { + json!({ + "version": SETUP_VERSION, + "offline_username": "CodexSandboxOffline", + "online_username": "CodexSandboxOnline", + "codex_home": "C:\\codex-home", + "command_cwd": "C:\\workspace", + "read_roots": [], + "write_roots": [], + "proxy_ports": [], + "real_user": "User", + }) + } + + #[test] + fn payload_defaults_otel_absent() { + let payload: Payload = serde_json::from_value(payload_json()).expect("payload"); + + assert_eq!(payload.otel, None); + } + + #[test] + fn payload_accepts_otel_settings() { + let mut payload = payload_json(); + payload["otel"] = json!({ + "environment": "prod", + }); + let payload: Payload = serde_json::from_value(payload).expect("payload"); + + assert_eq!( + payload.otel, + Some(StatsigMetricsSettings { + environment: "prod".to_string(), + }) + ); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 94dd3574b..ef952a4e0 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -183,6 +183,7 @@ fn run_setup_refresh_inner( deny_write_paths, proxy_ports: offline_proxy_settings.proxy_ports, allow_local_binding: offline_proxy_settings.allow_local_binding, + otel: None, real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), refresh_only: true, }; @@ -421,6 +422,7 @@ struct ElevationPayload { proxy_ports: Vec, #[serde(default)] allow_local_binding: bool, + otel: Option, real_user: String, #[serde(default)] refresh_only: bool, @@ -734,6 +736,7 @@ pub fn run_elevated_setup( proxy_ports: offline_proxy_settings.proxy_ports, allow_local_binding: offline_proxy_settings.allow_local_binding, real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), + otel: codex_otel::global_statsig_metrics_settings(), refresh_only: false, }; let needs_elevation = !is_elevated().map_err(|err| { diff --git a/codex-rs/windows-sandbox-rs/src/wfp.rs b/codex-rs/windows-sandbox-rs/src/wfp.rs new file mode 100644 index 000000000..36d72e637 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/wfp.rs @@ -0,0 +1,409 @@ +#![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 { + 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 { + 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> { + 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 { + 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 { + 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::>(); + assert_eq!(keys.len(), FILTER_SPECS.len()); + } + + #[test] + fn filter_names_are_unique() { + let names = FILTER_SPECS.iter().map(|spec| spec.name).collect::>(); + assert_eq!(names.len(), FILTER_SPECS.len()); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/wfp_filter_specs.rs b/codex-rs/windows-sandbox-rs/src/wfp_filter_specs.rs new file mode 100644 index 000000000..295320025 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/wfp_filter_specs.rs @@ -0,0 +1,114 @@ +#![cfg(target_os = "windows")] + +use windows_sys::core::GUID; +use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_AUTH_CONNECT_V4; +use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_AUTH_CONNECT_V6; +use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4; +use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V6; +use windows_sys::Win32::Networking::WinSock::IPPROTO_ICMP; +use windows_sys::Win32::Networking::WinSock::IPPROTO_ICMPV6; + +#[derive(Clone, Copy)] +pub(super) enum ConditionSpec { + User, + Protocol(u8), + RemotePort(u16), +} + +#[derive(Clone, Copy)] +pub(super) struct FilterSpec { + pub(super) key: GUID, + pub(super) name: &'static str, + pub(super) description: &'static str, + pub(super) layer_key: GUID, + pub(super) conditions: &'static [ConditionSpec], +} + +pub(super) const FILTER_SPECS: &[FilterSpec] = &[ + FilterSpec { + key: GUID::from_u128(0x9f5f3812_79f0_4fe9_9615_4c2c92d2f0ff), + name: "codex_wfp_icmp_connect_v4", + description: "Block sandbox-account ICMP connect v4", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V4, + conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMP as u8)], + }, + FilterSpec { + key: GUID::from_u128(0x87498484_45ab_4510_845e_ece8b791b3bc), + name: "codex_wfp_icmp_connect_v6", + description: "Block sandbox-account ICMP connect v6", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V6, + conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMPV6 as u8)], + }, + FilterSpec { + key: GUID::from_u128(0xaf4751de_f874_4a7b_a34d_f0d0f22d1d9b), + name: "codex_wfp_icmp_assign_v4", + description: "Block sandbox-account ICMP resource assignment v4", + layer_key: FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4, + conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMP as u8)], + }, + FilterSpec { + key: GUID::from_u128(0xea10db66_a928_4b2e_a82e_a376a54f93ba), + name: "codex_wfp_icmp_assign_v6", + description: "Block sandbox-account ICMP resource assignment v6", + layer_key: FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V6, + conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMPV6 as u8)], + }, + // NAME_RESOLUTION_CACHE filters are intentionally omitted because ordinary + // static filter shapes returned FWP_E_OUT_OF_BOUNDS during validation. + FilterSpec { + key: GUID::from_u128(0x83172805_f6be_4ae1_9dc6_6847aef04e7f), + name: "codex_wfp_dns_53_v4", + description: "Block sandbox-account DNS TCP or UDP port 53 v4", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V4, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(53)], + }, + FilterSpec { + key: GUID::from_u128(0xd23b2efb_1efb_46b2_96f3_b0ccda5690c8), + name: "codex_wfp_dns_53_v6", + description: "Block sandbox-account DNS TCP or UDP port 53 v6", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V6, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(53)], + }, + FilterSpec { + key: GUID::from_u128(0x420b026f_9dc9_4aea_88f4_0f2b9feab39a), + name: "codex_wfp_dns_853_v4", + description: "Block sandbox-account DNS-over-TLS port 853 v4", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V4, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(853)], + }, + FilterSpec { + key: GUID::from_u128(0x8d917c81_99cc_45e7_84d6_824df860cfb8), + name: "codex_wfp_dns_853_v6", + description: "Block sandbox-account DNS-over-TLS port 853 v6", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V6, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(853)], + }, + FilterSpec { + key: GUID::from_u128(0xe1d6e0af_ce5f_471b_b2d3_15ca00e966f3), + name: "codex_wfp_smb_445_v4", + description: "Block sandbox-account SMB port 445 v4", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V4, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(445)], + }, + FilterSpec { + key: GUID::from_u128(0xc2bceca4_66ef_4a0f_ba80_f4f761b8c6f0), + name: "codex_wfp_smb_445_v6", + description: "Block sandbox-account SMB port 445 v6", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V6, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(445)], + }, + FilterSpec { + key: GUID::from_u128(0xba10c618_84e7_4b83_8f74_36e22b2fa1ff), + name: "codex_wfp_smb_139_v4", + description: "Block sandbox-account SMB port 139 v4", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V4, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(139)], + }, + FilterSpec { + key: GUID::from_u128(0xfe7f22b8_5cf5_4adb_b2aa_71fc0a8f5d44), + name: "codex_wfp_smb_139_v6", + description: "Block sandbox-account SMB port 139 v6", + layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V6, + conditions: &[ConditionSpec::User, ConditionSpec::RemotePort(139)], + }, +]; diff --git a/codex-rs/windows-sandbox-rs/src/wfp_setup.rs b/codex-rs/windows-sandbox-rs/src/wfp_setup.rs new file mode 100644 index 000000000..bfcc2c069 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/wfp_setup.rs @@ -0,0 +1,178 @@ +use crate::install_wfp_filters_for_account; +use crate::setup_error::sanitize_setup_metric_tag_value; +use anyhow::Result; +use codex_otel::OtelExporter; +use codex_otel::OtelProvider; +use codex_otel::OtelSettings; +use codex_otel::StatsigMetricsSettings; +use std::path::Path; + +const WFP_SETUP_SERVICE_NAME: &str = "codex-windows-sandbox-setup"; +const WFP_SETUP_SUCCESS_METRIC: &str = "codex.windows_sandbox.wfp_setup_success"; +const WFP_SETUP_FAILURE_METRIC: &str = "codex.windows_sandbox.wfp_setup_failure"; + +#[derive(Debug, Clone, Copy)] +enum WfpSetupMetricOutcome { + Success, + Failure, +} + +struct WfpSetupMetric { + outcome: WfpSetupMetricOutcome, + target_account: String, + installed_filter_count: usize, + error: Option, +} + +fn panic_payload_to_string(panic_payload: Box) -> String { + match panic_payload.downcast::() { + Ok(message) => *message, + Err(panic_payload) => match panic_payload.downcast::<&'static str>() { + Ok(message) => (*message).to_string(), + Err(_) => "unknown panic payload".to_string(), + }, + } +} + +fn build_wfp_metrics_provider( + codex_home: &Path, + otel: Option<&StatsigMetricsSettings>, +) -> Result> { + let Some(otel) = otel else { + return Ok(None); + }; + // The setup helper cannot call codex-core's OTEL builder because core + // depends on this crate, so the parent process passes only the resolved + // Statsig environment in the elevation payload. Other exporters are + // intentionally omitted from this helper path. + OtelProvider::from(&OtelSettings { + environment: otel.environment.clone(), + service_name: WFP_SETUP_SERVICE_NAME.to_string(), + service_version: env!("CARGO_PKG_VERSION").to_string(), + codex_home: codex_home.to_path_buf(), + exporter: OtelExporter::None, + trace_exporter: OtelExporter::None, + metrics_exporter: OtelExporter::Statsig, + runtime_metrics: false, + }) + .map_err(|err| anyhow::anyhow!("failed to initialize WFP setup metrics provider: {err}")) +} + +fn emit_wfp_setup_metric( + codex_home: &Path, + otel: Option<&StatsigMetricsSettings>, + metric: &WfpSetupMetric, +) -> Result<()> { + let Some(provider) = build_wfp_metrics_provider(codex_home, otel)? else { + return Ok(()); + }; + if let Some(metrics) = provider.metrics() { + let target_account = sanitize_setup_metric_tag_value(&metric.target_account); + match metric.outcome { + WfpSetupMetricOutcome::Success => { + let installed_filter_count = metric.installed_filter_count.to_string(); + metrics.counter( + WFP_SETUP_SUCCESS_METRIC, + /*inc*/ 1, + &[ + ("target_account", target_account.as_str()), + ("installed_filter_count", installed_filter_count.as_str()), + ], + )?; + } + WfpSetupMetricOutcome::Failure => { + let mut tags = vec![("target_account", target_account.as_str())]; + let error_tag = metric.error.as_deref().map(sanitize_setup_metric_tag_value); + if let Some(error) = error_tag.as_deref() { + tags.push(("message", error)); + } + metrics.counter(WFP_SETUP_FAILURE_METRIC, /*inc*/ 1, &tags)?; + } + } + } + provider.shutdown(); + Ok(()) +} + +fn emit_wfp_setup_metric_safely( + codex_home: &Path, + otel: Option<&StatsigMetricsSettings>, + offline_username: &str, + metric: &WfpSetupMetric, + log: &mut F, +) where + F: FnMut(&str), +{ + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + emit_wfp_setup_metric(codex_home, otel, metric) + })); + match result { + Ok(Ok(())) => {} + Ok(Err(err)) => log(&format!( + "failed to emit WFP setup metric for {offline_username}: {err}" + )), + Err(panic_payload) => { + let error = panic_payload_to_string(panic_payload); + log(&format!( + "WFP setup metric emission panicked for {offline_username}: {error}" + )); + } + } +} + +pub fn install_wfp_filters( + codex_home: &Path, + offline_username: &str, + otel: Option<&StatsigMetricsSettings>, + mut log: F, +) where + F: FnMut(&str), +{ + let metric = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + install_wfp_filters_for_account(offline_username) + })) { + Ok(Ok(installed_filter_count)) => { + log(&format!( + "WFP setup succeeded for {offline_username} with {installed_filter_count} installed filters" + )); + WfpSetupMetric { + outcome: WfpSetupMetricOutcome::Success, + target_account: offline_username.to_string(), + installed_filter_count, + error: None, + } + } + Ok(Err(err)) => { + let error = err.to_string(); + log(&format!( + "WFP setup failed for {offline_username}: {error}; continuing elevated setup" + )); + WfpSetupMetric { + outcome: WfpSetupMetricOutcome::Failure, + target_account: offline_username.to_string(), + installed_filter_count: 0, + error: Some(error), + } + } + Err(panic_payload) => { + let error = panic_payload_to_string(panic_payload); + log(&format!( + "WFP setup panicked for {offline_username}: {error}; continuing elevated setup" + )); + WfpSetupMetric { + outcome: WfpSetupMetricOutcome::Failure, + target_account: offline_username.to_string(), + installed_filter_count: 0, + error: Some(format!("panic: {error}")), + } + } + }; + + emit_wfp_setup_metric_safely( + codex_home, + otel, + offline_username, + &metric, + &mut log, + ); +}