use anyhow::Result;use std::io::Write;use windows::Win32::Foundation::S_OK;use windows::Win32::Foundation::VARIANT_TRUE;use windows::Win32::NetworkManagement::WindowsFirewall::INetFwPolicy2;use windows::Win32::NetworkManagement::WindowsFirewall::INetFwRule3;use windows::Win32::NetworkManagement::WindowsFirewall::INetFwRules;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_ACTION_BLOCK;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_IP_PROTOCOL_ANY;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_IP_PROTOCOL_TCP;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_IP_PROTOCOL_UDP;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_MODIFY_STATE;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_MODIFY_STATE_OK;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_PROFILE2_ALL;use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_RULE_DIR_OUT;use windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2;use windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule;use windows::Win32::System::Com::CLSCTX_INPROC_SERVER;use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;use windows::Win32::System::Com::CoCreateInstance;use windows::Win32::System::Com::CoInitializeEx;use windows::Win32::System::Com::CoUninitialize;use windows::core::BSTR;use windows::core::Interface;use codex_windows_sandbox::SetupErrorCode;use codex_windows_sandbox::SetupFailure;// This is the stable identifier we use to find/update the rule idempotently.// It intentionally does not change between installs.const OFFLINE_BLOCK_RULE_NAME: &str = "codex_sandbox_offline_block_outbound";const OFFLINE_BLOCK_LOOPBACK_TCP_RULE_NAME: &str = "codex_sandbox_offline_block_loopback_tcp";const OFFLINE_BLOCK_LOOPBACK_UDP_RULE_NAME: &str = "codex_sandbox_offline_block_loopback_udp";// Friendly text shown in the firewall UI.const OFFLINE_BLOCK_RULE_FRIENDLY: &str = "Codex Sandbox Offline - Block Non-Loopback Outbound";const OFFLINE_BLOCK_LOOPBACK_TCP_RULE_FRIENDLY: &str = "Codex Sandbox Offline - Block Loopback TCP (Except Proxy)";const OFFLINE_BLOCK_LOOPBACK_UDP_RULE_FRIENDLY: &str = "Codex Sandbox Offline - Block Loopback UDP";const OFFLINE_PROXY_ALLOW_RULE_NAME: &str = "codex_sandbox_offline_allow_loopback_proxy";const LOOPBACK_REMOTE_ADDRESSES: &str = "127.0.0.0/8,::/127";const NON_LOOPBACK_REMOTE_ADDRESSES: &str = "0.0.0.0-126.255.255.255,128.0.0.0-255.255.255.255,::,::2-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";struct BlockRuleSpec<'a> { internal_name: &'a str, friendly_desc: &'a str, protocol: i32, local_user_spec: &'a str, offline_sid: &'a str, remote_addresses: Option<&'a str>, remote_ports: Option<&'a str>,}pub fn ensure_offline_proxy_allowlist( offline_sid: &str, proxy_ports: &[u16], allow_local_binding: bool, log: &mut dyn Write,) -> Result<()> { let local_user_spec = format!("O:LSD:(A;;CC;;;{offline_sid})"); let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; if hr.is_err() { return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallComInitFailed, format!("CoInitializeEx failed: {hr:?}"), ))); } let result = unsafe { (|| -> Result<()> { let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) .map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyAccessFailed, format!("CoCreateInstance NetFwPolicy2 failed: {err:?}"), )) })?; ensure_local_policy_rules_take_effect(&policy)?; let rules = policy.Rules().map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyAccessFailed, format!("INetFwPolicy2::Rules failed: {err:?}"), )) })?; if allow_local_binding { // Remove the legacy overlapping allow rule before returning to the local-binding // mode so stale proxy exceptions do not linger. remove_rule_if_present(&rules, OFFLINE_PROXY_ALLOW_RULE_NAME, log)?; remove_rule_if_present(&rules, OFFLINE_BLOCK_LOOPBACK_UDP_RULE_NAME, log)?; remove_rule_if_present(&rules, OFFLINE_BLOCK_LOOPBACK_TCP_RULE_NAME, log)?; return Ok(()); } ensure_block_rule( &rules, &BlockRuleSpec { internal_name: OFFLINE_BLOCK_LOOPBACK_UDP_RULE_NAME, friendly_desc: OFFLINE_BLOCK_LOOPBACK_UDP_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_UDP.0, local_user_spec: &local_user_spec, offline_sid, remote_addresses: Some(LOOPBACK_REMOTE_ADDRESSES), remote_ports: None, }, log, )?; // Install a broad TCP loopback block before narrowing it to the allowed proxy port // complement. If the narrowing update fails, the sandbox remains fail-closed. ensure_block_rule( &rules, &BlockRuleSpec { internal_name: OFFLINE_BLOCK_LOOPBACK_TCP_RULE_NAME, friendly_desc: OFFLINE_BLOCK_LOOPBACK_TCP_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_TCP.0, local_user_spec: &local_user_spec, offline_sid, remote_addresses: Some(LOOPBACK_REMOTE_ADDRESSES), remote_ports: None, }, log, )?; // Remove the legacy overlapping allow rule only after the explicit block rules are in // place so transitions back to proxy-only mode do not fail open. remove_rule_if_present(&rules, OFFLINE_PROXY_ALLOW_RULE_NAME, log)?; if let Some(blocked_remote_ports) = blocked_loopback_tcp_remote_ports(proxy_ports) { ensure_block_rule( &rules, &BlockRuleSpec { internal_name: OFFLINE_BLOCK_LOOPBACK_TCP_RULE_NAME, friendly_desc: OFFLINE_BLOCK_LOOPBACK_TCP_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_TCP.0, local_user_spec: &local_user_spec, offline_sid, remote_addresses: Some(LOOPBACK_REMOTE_ADDRESSES), remote_ports: Some(&blocked_remote_ports), }, log, )?; } Ok(()) })() }; unsafe { CoUninitialize(); } result}pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut dyn Write) -> Result<()> { let local_user_spec = format!("O:LSD:(A;;CC;;;{offline_sid})"); let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; if hr.is_err() { return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallComInitFailed, format!("CoInitializeEx failed: {hr:?}"), ))); } let result = unsafe { (|| -> Result<()> { let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) .map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyAccessFailed, format!("CoCreateInstance NetFwPolicy2 failed: {err:?}"), )) })?; ensure_local_policy_rules_take_effect(&policy)?; let rules = policy.Rules().map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyAccessFailed, format!("INetFwPolicy2::Rules failed: {err:?}"), )) })?; // Block all outbound IP protocols for this user. ensure_block_rule( &rules, &BlockRuleSpec { internal_name: OFFLINE_BLOCK_RULE_NAME, friendly_desc: OFFLINE_BLOCK_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_ANY.0, local_user_spec: &local_user_spec, offline_sid, remote_addresses: Some(NON_LOOPBACK_REMOTE_ADDRESSES), remote_ports: None, }, log, )?; Ok(()) })() }; unsafe { CoUninitialize(); } result}fn remove_rule_if_present( rules: &INetFwRules, internal_name: &str, log: &mut dyn Write,) -> Result<()> { let name = BSTR::from(internal_name); if unsafe { rules.Item(&name) }.is_ok() { unsafe { rules.Remove(&name) }.map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("Rules::Remove failed for {internal_name}: {err:?}"), )) })?; log_line(log, &format!("firewall rule removed name={internal_name}"))?; } Ok(())}fn ensure_local_policy_rules_take_effect(policy: &INetFwPolicy2) -> Result<()> { let mut modify_state = NET_FW_MODIFY_STATE::default(); let result = unsafe { (Interface::vtable(policy).LocalPolicyModifyState)( Interface::as_raw(policy), &mut modify_state, ) }; validate_local_policy_modify_result(result, modify_state)}fn validate_local_policy_modify_result( result: windows::core::HRESULT, modify_state: NET_FW_MODIFY_STATE,) -> Result<()> { if result.is_err() { // The COM query itself failed, so Windows never gave us a policy answer. return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyAccessFailed, format!("INetFwPolicy2::LocalPolicyModifyState failed: {result:?}"), ))); } if result != S_OK { // S_FALSE means the answer only holds for some active profiles, not all of them. return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyIneffective, format!( "local firewall policy modifications do not apply to every current profile: LocalPolicyModifyState result={result:?}" ), ))); } if modify_state == NET_FW_MODIFY_STATE_OK { return Ok(()); } // Windows answered uniformly, and that answer says local rule edits are ineffective. Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallPolicyIneffective, format!( "local firewall policy modifications will not take effect: LocalPolicyModifyState={modify_state:?}" ), )))}fn ensure_block_rule( rules: &INetFwRules, spec: &BlockRuleSpec<'_>, log: &mut dyn Write,) -> Result<()> { let name = BSTR::from(spec.internal_name); let rule: INetFwRule3 = match unsafe { rules.Item(&name) } { Ok(existing) => existing.cast().map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("cast existing firewall rule to INetFwRule3 failed: {err:?}"), )) })?, Err(_) => { let new_rule: INetFwRule3 = unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }.map_err( |err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("CoCreateInstance NetFwRule failed: {err:?}"), )) }, )?; unsafe { new_rule.SetName(&name) }.map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetName failed: {err:?}"), )) })?; // Set all properties before adding the rule so we don't leave half-configured rules. configure_rule(&new_rule, spec)?; unsafe { rules.Add(&new_rule) }.map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("Rules::Add failed: {err:?}"), )) })?; new_rule } }; // Always re-apply fields to keep the setup idempotent. configure_rule(&rule, spec)?; let remote_addresses_log = spec.remote_addresses.unwrap_or("*"); let remote_ports_log = spec.remote_ports.unwrap_or("*"); log_line( log, &format!( "firewall rule configured name={} protocol={} RemoteAddresses={remote_addresses_log} RemotePorts={remote_ports_log} LocalUserAuthorizedList={}", spec.internal_name, spec.protocol, spec.local_user_spec ), )?; Ok(())}fn configure_rule(rule: &INetFwRule3, spec: &BlockRuleSpec<'_>) -> Result<()> { unsafe { rule.SetDescription(&BSTR::from(spec.friendly_desc)) .map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetDescription failed: {err:?}"), )) })?; rule.SetDirection(NET_FW_RULE_DIR_OUT).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetDirection failed: {err:?}"), )) })?; rule.SetAction(NET_FW_ACTION_BLOCK).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetAction failed: {err:?}"), )) })?; rule.SetEnabled(VARIANT_TRUE).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetEnabled failed: {err:?}"), )) })?; rule.SetProfiles(NET_FW_PROFILE2_ALL.0).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetProfiles failed: {err:?}"), )) })?; configure_rule_network_scope(rule, spec)?; rule.SetLocalUserAuthorizedList(&BSTR::from(spec.local_user_spec)) .map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetLocalUserAuthorizedList failed: {err:?}"), )) })?; } // Read-back verification: ensure we actually wrote the expected SID scope. let actual = unsafe { rule.LocalUserAuthorizedList() }.map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleVerifyFailed, format!("LocalUserAuthorizedList (read-back) failed: {err:?}"), )) })?; let actual_str = actual.to_string(); if !actual_str.contains(spec.offline_sid) { return Err(anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleVerifyFailed, format!( "offline firewall rule user scope mismatch: expected SID {}, got {actual_str}", spec.offline_sid ), ))); } Ok(())}fn configure_rule_network_scope(rule: &INetFwRule3, spec: &BlockRuleSpec<'_>) -> Result<()> { unsafe { rule.SetProtocol(spec.protocol).map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetProtocol failed: {err:?}"), )) })?; let remote_addresses = spec.remote_addresses.unwrap_or("*"); rule.SetRemoteAddresses(&BSTR::from(remote_addresses)) .map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetRemoteAddresses failed: {err:?}"), )) })?; if let Some(remote_ports) = spec.remote_ports { rule.SetRemotePorts(&BSTR::from(remote_ports)) .map_err(|err| { anyhow::Error::new(SetupFailure::new( SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, format!("SetRemotePorts failed: {err:?}"), )) })?; } } Ok(())}fn blocked_loopback_tcp_remote_ports(proxy_ports: &[u16]) -> Option<String> { let mut allowed_ports = proxy_ports .iter() .copied() .filter(|port| *port != 0) .collect::<Vec<_>>(); allowed_ports.sort_unstable(); allowed_ports.dedup(); let mut blocked_ranges = Vec::new(); let mut start = 1_u32; for port in allowed_ports { let port = u32::from(port); if port < start { continue; } if port > start { blocked_ranges.push(port_range_string(start, port - 1)); } start = port + 1; } if start <= u32::from(u16::MAX) { blocked_ranges.push(port_range_string(start, u32::from(u16::MAX))); } if blocked_ranges.is_empty() { None } else { Some(blocked_ranges.join(",")) }}fn port_range_string(start: u32, end: u32) -> String { if start == end { start.to_string() } else { format!("{start}-{end}") }}fn log_line(log: &mut dyn Write, msg: &str) -> Result<()> { let ts = chrono::Utc::now().to_rfc3339(); writeln!(log, "[{ts}] {msg}")?; Ok(())}#[cfg(test)]mod tests { use pretty_assertions::assert_eq; use windows::Win32::Foundation::S_FALSE; use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_MODIFY_STATE_GP_OVERRIDE; use super::*; #[test] fn configured_remote_address_literals_are_accepted_by_firewall_com() { let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; assert!(hr.is_ok(), "CoInitializeEx failed: {hr:?}"); let candidates = [ LOOPBACK_REMOTE_ADDRESSES, NON_LOOPBACK_REMOTE_ADDRESSES, "*", ]; let results = candidates.map(|remote_addresses| unsafe { let rule: windows::core::Result<INetFwRule3> = CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER); rule.and_then(|rule| { rule.SetRemoteAddresses(&BSTR::from(remote_addresses))?; rule.RemoteAddresses() }) .map(|stored| stored.to_string()) }); unsafe { CoUninitialize(); } for (remote_addresses, result) in candidates.into_iter().zip(results) { assert!( result.is_ok(), "firewall rejected RemoteAddresses={remote_addresses:?}: {result:?}" ); } } #[test] fn production_firewall_rule_network_scopes_are_accepted_by_firewall_com() { let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; assert!(hr.is_ok(), "CoInitializeEx failed: {hr:?}"); let local_user_spec = "O:LSD:(A;;CC;;;S-1-5-18)"; let offline_sid = "S-1-5-18"; let blocked_remote_ports = blocked_loopback_tcp_remote_ports(&[8080]).expect("proxy-port complement should exist"); let specs = [ BlockRuleSpec { internal_name: OFFLINE_BLOCK_LOOPBACK_UDP_RULE_NAME, friendly_desc: OFFLINE_BLOCK_LOOPBACK_UDP_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_UDP.0, local_user_spec, offline_sid, remote_addresses: Some(LOOPBACK_REMOTE_ADDRESSES), remote_ports: None, }, BlockRuleSpec { internal_name: OFFLINE_BLOCK_LOOPBACK_TCP_RULE_NAME, friendly_desc: OFFLINE_BLOCK_LOOPBACK_TCP_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_TCP.0, local_user_spec, offline_sid, remote_addresses: Some(LOOPBACK_REMOTE_ADDRESSES), remote_ports: Some(&blocked_remote_ports), }, BlockRuleSpec { internal_name: OFFLINE_BLOCK_RULE_NAME, friendly_desc: OFFLINE_BLOCK_RULE_FRIENDLY, protocol: NET_FW_IP_PROTOCOL_ANY.0, local_user_spec, offline_sid, remote_addresses: Some(NON_LOOPBACK_REMOTE_ADDRESSES), remote_ports: None, }, ]; let results = specs.each_ref().map(|spec| unsafe { let rule: windows::core::Result<INetFwRule3> = CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER); match rule { Ok(rule) => configure_rule_network_scope(&rule, spec), Err(err) => Err(err.into()), } }); unsafe { CoUninitialize(); } for (spec, result) in specs.into_iter().zip(results) { assert!( result.is_ok(), "firewall rejected network scope for rule={} protocol={} remote_addresses={:?} remote_ports={:?}: {result:?}", spec.internal_name, spec.protocol, spec.remote_addresses, spec.remote_ports ); } } #[test] fn local_policy_modify_state_accepts_effective_policy() { assert!(validate_local_policy_modify_result(S_OK, NET_FW_MODIFY_STATE_OK).is_ok()); } #[test] fn local_policy_modify_state_rejects_ineffective_policy() { let err = validate_local_policy_modify_result(S_OK, NET_FW_MODIFY_STATE_GP_OVERRIDE) .expect_err("group-policy override should fail sandbox firewall setup"); let failure = err .downcast_ref::<SetupFailure>() .expect("expected setup failure"); assert_eq!( failure.code, SetupErrorCode::HelperFirewallPolicyIneffective ); } #[test] fn local_policy_modify_state_rejects_partial_profile_coverage() { let err = validate_local_policy_modify_result(S_FALSE, NET_FW_MODIFY_STATE_OK) .expect_err("partial profile coverage should fail sandbox firewall setup"); let failure = err .downcast_ref::<SetupFailure>() .expect("expected setup failure"); assert_eq!( failure.code, SetupErrorCode::HelperFirewallPolicyIneffective ); }}