Files
codex/codex-rs/core/src/windows_sandbox.rs
T
viyatb-oai 81fa04783a feat(windows-sandbox): add network proxy support (#12220)
## Summary

This PR makes Windows sandbox proxying enforceable by routing proxy-only
runs through the existing `offline` sandbox user and reserving direct
network access for the existing `online` sandbox user.

In brief:

- if a Windows sandbox run should be proxy-enforced, we run it as the
`offline` user
- the `offline` user gets firewall rules that block direct outbound
traffic and only permit the configured localhost proxy path
- if a Windows sandbox run should have true direct network access, we
run it as the `online` user
- no new sandbox identity is introduced

This brings Windows in line with the intended model: proxy use is not
just env-based, it is backed by OS-level egress controls. Windows
already has two sandbox identities:

- `offline`: intended to have no direct network egress
- `online`: intended to have full network access

This PR makes proxy-enforced runs use that model directly.

### Proxy-enforced runs

When proxy enforcement is active:

- the run is assigned to the `offline` identity
- setup extracts the loopback proxy ports from the sandbox env
- Windows setup programs firewall rules for the `offline` user that:
  - block all non-loopback outbound traffic
  - block loopback UDP
  - block loopback TCP except for the configured proxy ports
- optionally allow broader localhost access when `allow_local_binding=1`

So the sandboxed process can only talk to the local proxy. It cannot
open direct outbound sockets or do local UDP-based DNS on its own.The
proxy then performs the real outbound network access outside that
restricted sandbox identity.

### Direct-network runs

When proxy enforcement is not active and full network access is allowed:

- the run is assigned to the `online` identity
- no proxy-only firewall restrictions are applied
- the process gets normal direct network access

### Unelevated vs elevated

The restricted-token / unelevated path cannot enforce per-identity
firewall policy by itself.

So for Windows proxy-enforced runs, we transparently use the logon-user
sandbox path under the hood, even if the caller started from the
unelevated mode. That keeps enforcement real instead of best-effort.

---------

Co-authored-by: Codex <noreply@openai.com>
2026-03-26 17:27:38 -07:00

452 lines
14 KiB
Rust

use crate::config::Config;
use crate::config::ConfigToml;
use crate::config::edit::ConfigEditsBuilder;
use crate::config::profile::ConfigProfile;
use crate::config::types::WindowsSandboxModeToml;
use crate::default_client::originator;
use crate::protocol::SandboxPolicy;
use codex_features::Feature;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_otel::sanitize_metric_tag_value;
use codex_protocol::config_types::WindowsSandboxLevel;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;
/// Kill switch for the elevated sandbox NUX on Windows.
///
/// When false, revert to the previous sandbox NUX, which only
/// prompts users to enable the legacy sandbox feature.
pub const ELEVATED_SANDBOX_NUX_ENABLED: bool = true;
pub trait WindowsSandboxLevelExt {
fn from_config(config: &Config) -> WindowsSandboxLevel;
fn from_features(features: &Features) -> WindowsSandboxLevel;
}
impl WindowsSandboxLevelExt for WindowsSandboxLevel {
fn from_config(config: &Config) -> WindowsSandboxLevel {
match config.permissions.windows_sandbox_mode {
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
None => Self::from_features(&config.features),
}
}
fn from_features(features: &Features) -> WindowsSandboxLevel {
if features.enabled(Feature::WindowsSandboxElevated) {
return WindowsSandboxLevel::Elevated;
}
if features.enabled(Feature::WindowsSandbox) {
WindowsSandboxLevel::RestrictedToken
} else {
WindowsSandboxLevel::Disabled
}
}
}
pub fn windows_sandbox_level_from_config(config: &Config) -> WindowsSandboxLevel {
WindowsSandboxLevel::from_config(config)
}
pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandboxLevel {
WindowsSandboxLevel::from_features(features)
}
pub fn resolve_windows_sandbox_mode(
cfg: &ConfigToml,
profile: &ConfigProfile,
) -> Option<WindowsSandboxModeToml> {
if let Some(mode) = legacy_windows_sandbox_mode(profile.features.as_ref()) {
return Some(mode);
}
if legacy_windows_sandbox_keys_present(profile.features.as_ref()) {
return None;
}
profile
.windows
.as_ref()
.and_then(|windows| windows.sandbox)
.or_else(|| cfg.windows.as_ref().and_then(|windows| windows.sandbox))
.or_else(|| legacy_windows_sandbox_mode(cfg.features.as_ref()))
}
pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml, profile: &ConfigProfile) -> bool {
profile
.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
.or_else(|| {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
})
.unwrap_or(true)
}
fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool {
let Some(entries) = features.map(|features| &features.entries) else {
return false;
};
entries.contains_key(Feature::WindowsSandboxElevated.key())
|| entries.contains_key(Feature::WindowsSandbox.key())
|| entries.contains_key("enable_experimental_windows_sandbox")
}
pub fn legacy_windows_sandbox_mode(
features: Option<&FeaturesToml>,
) -> Option<WindowsSandboxModeToml> {
let entries = features.map(|features| &features.entries)?;
legacy_windows_sandbox_mode_from_entries(entries)
}
pub fn legacy_windows_sandbox_mode_from_entries(
entries: &BTreeMap<String, bool>,
) -> Option<WindowsSandboxModeToml> {
if entries
.get(Feature::WindowsSandboxElevated.key())
.copied()
.unwrap_or(false)
{
return Some(WindowsSandboxModeToml::Elevated);
}
if entries
.get(Feature::WindowsSandbox.key())
.copied()
.unwrap_or(false)
|| entries
.get("enable_experimental_windows_sandbox")
.copied()
.unwrap_or(false)
{
Some(WindowsSandboxModeToml::Unelevated)
} else {
None
}
}
#[cfg(target_os = "windows")]
pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool {
codex_windows_sandbox::sandbox_setup_is_complete(codex_home)
}
#[cfg(not(target_os = "windows"))]
pub fn sandbox_setup_is_complete(_codex_home: &Path) -> bool {
false
}
#[cfg(target_os = "windows")]
pub fn elevated_setup_failure_details(err: &anyhow::Error) -> Option<(String, String)> {
let failure = codex_windows_sandbox::extract_setup_failure(err)?;
let code = failure.code.as_str().to_string();
let message = codex_windows_sandbox::sanitize_setup_metric_tag_value(&failure.message);
Some((code, message))
}
#[cfg(not(target_os = "windows"))]
pub fn elevated_setup_failure_details(_err: &anyhow::Error) -> Option<(String, String)> {
None
}
#[cfg(target_os = "windows")]
pub fn elevated_setup_failure_metric_name(err: &anyhow::Error) -> &'static str {
if codex_windows_sandbox::extract_setup_failure(err).is_some_and(|failure| {
matches!(
failure.code,
codex_windows_sandbox::SetupErrorCode::OrchestratorHelperLaunchCanceled
)
}) {
"codex.windows_sandbox.elevated_setup_canceled"
} else {
"codex.windows_sandbox.elevated_setup_failure"
}
}
#[cfg(not(target_os = "windows"))]
pub fn elevated_setup_failure_metric_name(_err: &anyhow::Error) -> &'static str {
panic!("elevated_setup_failure_metric_name is only supported on Windows")
}
#[cfg(target_os = "windows")]
pub fn run_elevated_setup(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_elevated_setup(
codex_windows_sandbox::SandboxSetupRequest {
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
proxy_enforced: false,
},
codex_windows_sandbox::SetupRootOverrides::default(),
)
}
#[cfg(not(target_os = "windows"))]
pub fn run_elevated_setup(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
) -> anyhow::Result<()> {
anyhow::bail!("elevated Windows sandbox setup is only supported on Windows")
}
#[cfg(target_os = "windows")]
pub fn run_legacy_setup_preflight(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_windows_sandbox_legacy_preflight(
policy,
policy_cwd,
codex_home,
command_cwd,
env_map,
)
}
#[cfg(target_os = "windows")]
pub fn run_setup_refresh_with_extra_read_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_setup_refresh_with_extra_read_roots(
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
extra_read_roots,
/*proxy_enforced*/ false,
)
}
#[cfg(not(target_os = "windows"))]
pub fn run_legacy_setup_preflight(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
) -> anyhow::Result<()> {
anyhow::bail!("legacy Windows sandbox setup is only supported on Windows")
}
#[cfg(not(target_os = "windows"))]
pub fn run_setup_refresh_with_extra_read_roots(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
_extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
anyhow::bail!("Windows sandbox read-root refresh is only supported on Windows")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowsSandboxSetupMode {
Elevated,
Unelevated,
}
#[derive(Debug, Clone)]
pub struct WindowsSandboxSetupRequest {
pub mode: WindowsSandboxSetupMode,
pub policy: SandboxPolicy,
pub policy_cwd: PathBuf,
pub command_cwd: PathBuf,
pub env_map: HashMap<String, String>,
pub codex_home: PathBuf,
pub active_profile: Option<String>,
}
pub async fn run_windows_sandbox_setup(request: WindowsSandboxSetupRequest) -> anyhow::Result<()> {
let start = Instant::now();
let mode = request.mode;
let originator_tag = sanitize_metric_tag_value(originator().value.as_str());
let result = run_windows_sandbox_setup_and_persist(request).await;
match result {
Ok(()) => {
emit_windows_sandbox_setup_success_metrics(
mode,
originator_tag.as_str(),
start.elapsed(),
);
Ok(())
}
Err(err) => {
emit_windows_sandbox_setup_failure_metrics(
mode,
originator_tag.as_str(),
start.elapsed(),
&err,
);
Err(err)
}
}
}
async fn run_windows_sandbox_setup_and_persist(
request: WindowsSandboxSetupRequest,
) -> anyhow::Result<()> {
let mode = request.mode;
let policy = request.policy;
let policy_cwd = request.policy_cwd;
let command_cwd = request.command_cwd;
let env_map = request.env_map;
let codex_home = request.codex_home;
let active_profile = request.active_profile;
let setup_codex_home = codex_home.clone();
let setup_result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
match mode {
WindowsSandboxSetupMode::Elevated => {
if !sandbox_setup_is_complete(setup_codex_home.as_path()) {
run_elevated_setup(
&policy,
policy_cwd.as_path(),
command_cwd.as_path(),
&env_map,
setup_codex_home.as_path(),
)?;
}
}
WindowsSandboxSetupMode::Unelevated => {
run_legacy_setup_preflight(
&policy,
policy_cwd.as_path(),
command_cwd.as_path(),
&env_map,
setup_codex_home.as_path(),
)?;
}
}
Ok(())
})
.await
.map_err(|join_err| anyhow::anyhow!("windows sandbox setup task failed: {join_err}"))?;
setup_result?;
ConfigEditsBuilder::new(codex_home.as_path())
.with_profile(active_profile.as_deref())
.set_windows_sandbox_mode(windows_sandbox_setup_mode_tag(mode))
.clear_legacy_windows_sandbox_keys()
.apply()
.await
.map_err(|err| anyhow::anyhow!("failed to persist windows sandbox mode: {err}"))
}
fn emit_windows_sandbox_setup_success_metrics(
mode: WindowsSandboxSetupMode,
originator_tag: &str,
duration: std::time::Duration,
) {
let Some(metrics) = codex_otel::metrics::global() else {
return;
};
let mode_tag = windows_sandbox_setup_mode_tag(mode);
let _ = metrics.record_duration(
"codex.windows_sandbox.setup_duration_ms",
duration,
&[
("result", "success"),
("originator", originator_tag),
("mode", mode_tag),
],
);
let _ = metrics.counter(
"codex.windows_sandbox.setup_success",
/*inc*/ 1,
&[("originator", originator_tag), ("mode", mode_tag)],
);
}
fn emit_windows_sandbox_setup_failure_metrics(
mode: WindowsSandboxSetupMode,
originator_tag: &str,
duration: std::time::Duration,
_err: &anyhow::Error,
) {
let Some(metrics) = codex_otel::metrics::global() else {
return;
};
let mode_tag = windows_sandbox_setup_mode_tag(mode);
let _ = metrics.record_duration(
"codex.windows_sandbox.setup_duration_ms",
duration,
&[
("result", "failure"),
("originator", originator_tag),
("mode", mode_tag),
],
);
let _ = metrics.counter(
"codex.windows_sandbox.setup_failure",
/*inc*/ 1,
&[("originator", originator_tag), ("mode", mode_tag)],
);
if matches!(mode, WindowsSandboxSetupMode::Elevated) {
#[cfg(target_os = "windows")]
{
let mut failure_tags: Vec<(&str, &str)> = vec![("originator", originator_tag)];
let mut code_tag: Option<String> = None;
let mut message_tag: Option<String> = None;
if let Some((code, message)) = elevated_setup_failure_details(_err) {
code_tag = Some(code);
message_tag = Some(message);
}
if let Some(code) = code_tag.as_deref() {
failure_tags.push(("code", code));
}
if let Some(message) = message_tag.as_deref() {
failure_tags.push(("message", message));
}
let _ = metrics.counter(
elevated_setup_failure_metric_name(_err),
/*inc*/ 1,
&failure_tags,
);
}
} else {
let _ = metrics.counter(
"codex.windows_sandbox.legacy_setup_preflight_failed",
/*inc*/ 1,
&[("originator", originator_tag)],
);
}
}
fn windows_sandbox_setup_mode_tag(mode: WindowsSandboxSetupMode) -> &'static str {
match mode {
WindowsSandboxSetupMode::Elevated => "elevated",
WindowsSandboxSetupMode::Unelevated => "unelevated",
}
}
#[cfg(test)]
#[path = "windows_sandbox_tests.rs"]
mod tests;