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, + ); +}