mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Route Windows sandbox proxy traffic by restricting SID (#34613)
## Why Elevated Windows sandboxes need stable managed-proxy ports while preserving the network policy and environment attribution of each sandboxed process. ## What changed - Keep shared HTTP and SOCKS5 loopback ingress listeners alive across managed-proxy instances. - Add a per-route restricting SID to elevated sandbox tokens and dispatch incoming connections to the matching proxy policy after attributing the client process. - Reject connections without exactly one registered route, remove routes when their proxy handle is dropped, and keep unsandboxed Windows launches off the managed ingress. - Provision the elevated sandbox with the configured proxy ports and local-binding setting, honoring the selected profile and CLI overrides. ## Testing - Add Windows unit tests for TCP ownership attribution, route selection, restricting-token propagation, and setup settings. - Add an end-to-end Windows test covering stable ports, isolated environment policies, HTTP and SOCKS5 routing, and route teardown. GitOrigin-RevId: 783fac6e0f904dc9bb1955b75d4a5895e8bb9690
This commit is contained in:
Generated
+2
@@ -3606,6 +3606,7 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-home-dir",
|
||||
"codex-utils-rustls-provider",
|
||||
"codex-windows-sandbox",
|
||||
"globset",
|
||||
"pretty_assertions",
|
||||
"rama-core",
|
||||
@@ -3629,6 +3630,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -488,6 +488,7 @@ async fn run_command_under_windows_session(
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(config),
|
||||
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
|
||||
proxy_enforced: false,
|
||||
network_proxy_restricting_sid: None,
|
||||
timeout_ms: None,
|
||||
read_roots_override: None,
|
||||
read_roots_include_platform_defaults: false,
|
||||
|
||||
+47
-14
@@ -1445,6 +1445,14 @@ async fn cli_main(
|
||||
.await?;
|
||||
}
|
||||
Some(Subcommand::Sandbox(mut sandbox_cli)) => {
|
||||
let config_profile = sandbox_cli
|
||||
.config_profile
|
||||
.as_ref()
|
||||
.or(interactive.config_profile_v2.as_ref());
|
||||
prepend_config_flags(
|
||||
&mut sandbox_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(setup_cli) = sandbox_setup::parse_setup_command(&sandbox_cli.command)? {
|
||||
reject_remote_mode_for_subcommand(
|
||||
@@ -1452,7 +1460,11 @@ async fn cli_main(
|
||||
root_remote_auth_token_env.as_deref(),
|
||||
"sandbox setup",
|
||||
)?;
|
||||
sandbox_setup::run(setup_cli).await?;
|
||||
let cli_overrides = sandbox_cli
|
||||
.config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
sandbox_setup::run(setup_cli, config_profile.cloned(), cli_overrides).await?;
|
||||
return Ok(());
|
||||
}
|
||||
reject_remote_mode_for_subcommand(
|
||||
@@ -1460,15 +1472,7 @@ async fn cli_main(
|
||||
root_remote_auth_token_env.as_deref(),
|
||||
"sandbox",
|
||||
)?;
|
||||
let config_profile = sandbox_cli
|
||||
.config_profile
|
||||
.as_ref()
|
||||
.or(interactive.config_profile_v2.as_ref());
|
||||
let loader_overrides = loader_overrides_for_profile(config_profile)?;
|
||||
prepend_config_flags(
|
||||
&mut sandbox_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
codex_cli::run_command_under_seatbelt(
|
||||
sandbox_cli,
|
||||
@@ -1879,16 +1883,29 @@ fn loader_overrides_for_profile(
|
||||
match profile_v2 {
|
||||
Some(profile_v2) => {
|
||||
let codex_home = find_codex_home()?;
|
||||
Ok(LoaderOverrides {
|
||||
user_config_path: Some(resolve_profile_v2_config_path(&codex_home, profile_v2)),
|
||||
user_config_profile: Some(profile_v2.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
Ok(loader_overrides_for_profile_at_codex_home(
|
||||
Some(profile_v2),
|
||||
&codex_home,
|
||||
))
|
||||
}
|
||||
None => Ok(LoaderOverrides::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn loader_overrides_for_profile_at_codex_home(
|
||||
profile_v2: Option<&ProfileV2Name>,
|
||||
codex_home: &std::path::Path,
|
||||
) -> LoaderOverrides {
|
||||
match profile_v2 {
|
||||
Some(profile_v2) => LoaderOverrides {
|
||||
user_config_path: Some(resolve_profile_v2_config_path(codex_home, profile_v2)),
|
||||
user_config_profile: Some(profile_v2.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
None => LoaderOverrides::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_print_under_development_feature_warning(codex_home: &std::path::Path, feature: &str) {
|
||||
let Some(spec) = FEATURES.iter().find(|spec| spec.key == feature) else {
|
||||
return;
|
||||
@@ -2706,6 +2723,22 @@ mod tests {
|
||||
Ok(profile_v2_for_subcommand(&cli.interactive, subcommand)?.map(ToString::to_string))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_loader_overrides_use_explicit_codex_home() -> anyhow::Result<()> {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let profile: ProfileV2Name = "work".parse()?;
|
||||
|
||||
let overrides =
|
||||
loader_overrides_for_profile_at_codex_home(Some(&profile), codex_home.path());
|
||||
|
||||
assert_eq!(
|
||||
overrides.user_config_path,
|
||||
Some(resolve_profile_v2_config_path(codex_home.path(), &profile))
|
||||
);
|
||||
assert_eq!(overrides.user_config_profile, Some(profile));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_is_rejected_for_config_management_subcommands() {
|
||||
assert!(profile_v2_for_args(&["codex", "--profile", "work", "features", "list"]).is_err());
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::ArgAction;
|
||||
use clap::ArgGroup;
|
||||
use clap::Parser;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_utils_cli::ProfileV2Name;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(group(
|
||||
@@ -54,9 +58,13 @@ impl SandboxSetupCommand {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(cmd: SandboxSetupCommand) -> anyhow::Result<()> {
|
||||
pub(crate) async fn run(
|
||||
cmd: SandboxSetupCommand,
|
||||
config_profile: Option<ProfileV2Name>,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
) -> anyhow::Result<()> {
|
||||
match cmd.setup_level()? {
|
||||
SandboxSetupLevel::Elevated => run_elevated(cmd).await,
|
||||
SandboxSetupLevel::Elevated => run_elevated(cmd, config_profile, cli_overrides).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +83,28 @@ pub(crate) fn parse_setup_command(
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
async fn run_elevated(cmd: SandboxSetupCommand) -> anyhow::Result<()> {
|
||||
async fn run_elevated(
|
||||
cmd: SandboxSetupCommand,
|
||||
config_profile: Option<ProfileV2Name>,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
) -> anyhow::Result<()> {
|
||||
let identity = resolve_sandbox_setup_identity(&cmd)?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(identity.codex_home.clone())
|
||||
.fallback_cwd(Some(identity.codex_home.clone()))
|
||||
.loader_overrides(super::loader_overrides_for_profile_at_codex_home(
|
||||
config_profile.as_ref(),
|
||||
&identity.codex_home,
|
||||
))
|
||||
.cli_overrides(cli_overrides)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to load target user's Codex config for sandbox provisioning")?;
|
||||
|
||||
codex_core::windows_sandbox::run_elevated_provisioning_setup(
|
||||
identity.codex_home.as_path(),
|
||||
identity.real_user.as_str(),
|
||||
config.permissions.network.as_ref(),
|
||||
)?;
|
||||
ConfigEditsBuilder::new(identity.codex_home.as_path())
|
||||
.set_windows_sandbox_mode("elevated")
|
||||
|
||||
@@ -14,6 +14,8 @@ use codex_network_proxy::NetworkProxyHandle;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use codex_network_proxy::host_and_port_from_network_addr;
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
use codex_network_proxy::managed_proxy_ports;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -85,6 +87,16 @@ impl NetworkProxySpec {
|
||||
self.config.enable_socks5
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
pub(crate) fn configured_proxy_ports(&self) -> std::io::Result<Vec<u16>> {
|
||||
managed_proxy_ports(&self.config).map_err(std::io::Error::other)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
pub(crate) fn allow_local_binding(&self) -> bool {
|
||||
self.config.allow_local_binding
|
||||
}
|
||||
|
||||
pub(crate) fn from_config_and_constraints(
|
||||
config: NetworkProxyConfig,
|
||||
requirements: Option<NetworkConstraints>,
|
||||
|
||||
@@ -632,6 +632,18 @@ async fn exec_windows_sandbox(
|
||||
network_proxy_environment_error(network_environment_id.as_deref(), err)
|
||||
})?;
|
||||
}
|
||||
let network_proxy_restricting_sid = network
|
||||
.as_ref()
|
||||
.map(|network| {
|
||||
network
|
||||
.network_proxy_restricting_sid(network_environment_id.as_deref())
|
||||
.ok_or_else(|| {
|
||||
CodexErr::Io(io::Error::other(
|
||||
"managed Windows proxy route is missing its restricting SID",
|
||||
))
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Windows sandbox capture still receives timeout and cancellation separately.
|
||||
let (cancellation, timeout_ms) = if capture_policy.uses_expiration() {
|
||||
@@ -686,6 +698,7 @@ async fn exec_windows_sandbox(
|
||||
cancellation,
|
||||
use_private_desktop: windows_sandbox_private_desktop,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid,
|
||||
read_roots_override: elevated_read_roots_override.as_deref(),
|
||||
read_roots_include_platform_defaults:
|
||||
elevated_read_roots_include_platform_defaults,
|
||||
|
||||
@@ -903,7 +903,14 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
|
||||
let mut stream = tokio::net::TcpStream::connect(started_proxy.proxy().http_addr()).await?;
|
||||
let prepared = started_proxy
|
||||
.proxy()
|
||||
.prepare_for_remote_environment(std::collections::HashMap::new(), "test-bridge")?;
|
||||
let proxy_addr = prepared.env["HTTP_PROXY"]
|
||||
.strip_prefix("http://")
|
||||
.expect("HTTP proxy URL")
|
||||
.parse::<std::net::SocketAddr>()?;
|
||||
let mut stream = tokio::net::TcpStream::connect(proxy_addr).await?;
|
||||
stream
|
||||
.write_all(
|
||||
b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n",
|
||||
|
||||
@@ -19,7 +19,8 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
|
||||
use codex_network_proxy::PROXY_ENV_KEYS;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
||||
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
|
||||
pub(crate) use codex_network_proxy::is_managed_proxy_env_var;
|
||||
pub(crate) use codex_network_proxy::strip_managed_proxy_env;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
@@ -69,28 +70,6 @@ pub(crate) fn exec_env_for_sandbox_permissions(
|
||||
env
|
||||
}
|
||||
|
||||
pub(crate) fn is_managed_proxy_env_var(key: &str, value: &str) -> bool {
|
||||
if PROXY_ENV_KEYS.contains(&key) {
|
||||
return true;
|
||||
}
|
||||
if CUSTOM_CA_ENV_KEYS.contains(&key) {
|
||||
return is_managed_mitm_ca_trust_bundle_path(value);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
key == PROXY_GIT_SSH_COMMAND_ENV_KEY
|
||||
&& value.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER)
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
|
||||
env.retain(|key, value| !is_managed_proxy_env_var(key, value));
|
||||
}
|
||||
|
||||
/// Prepends `path_entry` to `PATH`, removing duplicate and empty existing
|
||||
/// entries.
|
||||
///
|
||||
|
||||
@@ -1031,6 +1031,35 @@ impl UnifiedExecProcessManager {
|
||||
if request.command.is_empty() {
|
||||
return Err(UnifiedExecError::MissingCommandLine);
|
||||
}
|
||||
let network_proxy_restricting_sid = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
|
||||
request
|
||||
.network
|
||||
.as_ref()
|
||||
.map(|network| {
|
||||
network
|
||||
.network_proxy_restricting_sid(
|
||||
request.network_environment_id.as_deref(),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
UnifiedExecError::create_process(
|
||||
"managed Windows proxy route is missing its restricting SID"
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
None::<String>
|
||||
}
|
||||
};
|
||||
let windows_sandbox = if request.sandbox
|
||||
== codex_sandboxing::SandboxType::WindowsRestrictedToken
|
||||
{
|
||||
@@ -1039,6 +1068,7 @@ impl UnifiedExecProcessManager {
|
||||
workspace_roots: &request.windows_sandbox_workspace_roots,
|
||||
windows_sandbox_level: request.windows_sandbox_level,
|
||||
proxy_enforced: request.network.is_some(),
|
||||
network_proxy_restricting_sid: network_proxy_restricting_sid.as_deref(),
|
||||
proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||
filesystem_overrides: request.windows_sandbox_filesystem_overrides.as_ref(),
|
||||
use_private_desktop: request.windows_sandbox_private_desktop,
|
||||
|
||||
@@ -123,9 +123,30 @@ pub fn run_elevated_setup(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn provisioning_settings(
|
||||
network: Option<&crate::config::NetworkProxySpec>,
|
||||
) -> std::io::Result<codex_windows_sandbox::WindowsSandboxProvisioningSettings> {
|
||||
let Some(network) = network.filter(|network| network.enabled()) else {
|
||||
return Ok(codex_windows_sandbox::WindowsSandboxProvisioningSettings::default());
|
||||
};
|
||||
Ok(codex_windows_sandbox::WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: network.configured_proxy_ports()?,
|
||||
allow_local_binding: network.allow_local_binding(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> anyhow::Result<()> {
|
||||
codex_windows_sandbox::run_elevated_provisioning_setup(codex_home, real_user)
|
||||
pub fn run_elevated_provisioning_setup(
|
||||
codex_home: &Path,
|
||||
real_user: &str,
|
||||
network: Option<&crate::config::NetworkProxySpec>,
|
||||
) -> anyhow::Result<()> {
|
||||
codex_windows_sandbox::run_elevated_provisioning_setup(
|
||||
codex_home,
|
||||
real_user,
|
||||
provisioning_settings(network)?,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -140,7 +161,11 @@ pub fn run_elevated_setup(
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn run_elevated_provisioning_setup(_codex_home: &Path, _real_user: &str) -> anyhow::Result<()> {
|
||||
pub fn run_elevated_provisioning_setup(
|
||||
_codex_home: &Path,
|
||||
_real_user: &str,
|
||||
_network: Option<&crate::config::NetworkProxySpec>,
|
||||
) -> anyhow::Result<()> {
|
||||
anyhow::bail!("elevated Windows sandbox setup is only supported on Windows")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::*;
|
||||
use codex_config::types::WindowsToml;
|
||||
use codex_features::Features;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -115,3 +117,44 @@ fn resolve_windows_sandbox_private_desktop_respects_explicit_cfg_value() {
|
||||
|
||||
assert!(!resolve_windows_sandbox_private_desktop(&cfg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provisioning_settings_omit_the_disabled_socks_proxy() {
|
||||
let config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
proxy_url: "http://127.0.0.1:43128".to_string(),
|
||||
enable_socks5: false,
|
||||
socks_url: "socks5h://127.0.0.1:48081".to_string(),
|
||||
allow_local_binding: true,
|
||||
..Default::default()
|
||||
};
|
||||
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
/*requirements*/ None,
|
||||
&PermissionProfile::workspace_write(),
|
||||
)
|
||||
.expect("managed proxy config should resolve");
|
||||
|
||||
assert_eq!(
|
||||
provisioning_settings(Some(&spec)).expect("provisioning settings should resolve"),
|
||||
codex_windows_sandbox::WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![43128],
|
||||
allow_local_binding: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provisioning_settings_are_empty_when_managed_network_is_disabled() {
|
||||
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
|
||||
NetworkProxyConfig::default(),
|
||||
/*requirements*/ None,
|
||||
&PermissionProfile::workspace_write(),
|
||||
)
|
||||
.expect("managed proxy config should resolve");
|
||||
|
||||
assert_eq!(
|
||||
provisioning_settings(Some(&spec)).expect("provisioning settings should resolve"),
|
||||
codex_windows_sandbox::WindowsSandboxProvisioningSettings::default()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::managed_network_requirements_loader;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
@@ -22,6 +23,7 @@ use futures::StreamExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
use test_case::test_case;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -46,6 +48,12 @@ enum PushedExecScenario {
|
||||
ReplayGap,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PushedExecServerResult {
|
||||
process_read_requests: usize,
|
||||
process_start: Value,
|
||||
}
|
||||
|
||||
async fn read_exec_server_json(websocket: &mut WebSocketStream<TcpStream>) -> Value {
|
||||
loop {
|
||||
match timeout(Duration::from_secs(5), websocket.next())
|
||||
@@ -96,11 +104,18 @@ async fn accept_initialized_exec_server(listener: TcpListener) -> WebSocketStrea
|
||||
async fn send_environment_info(websocket: &mut WebSocketStream<TcpStream>) {
|
||||
let info = read_exec_server_json(websocket).await;
|
||||
assert_eq!(info["method"], "environment/info");
|
||||
respond_environment_info(websocket, &info["id"]).await;
|
||||
}
|
||||
|
||||
async fn respond_environment_info(websocket: &mut WebSocketStream<TcpStream>, id: &Value) {
|
||||
send_exec_server_json(
|
||||
websocket,
|
||||
json!({
|
||||
"id": info["id"],
|
||||
"result": { "shell": { "name": "zsh", "path": "/bin/zsh" } }
|
||||
"id": id,
|
||||
"result": {
|
||||
"shell": { "name": "zsh", "path": "/bin/zsh" },
|
||||
"capabilities": { "networkProxyLaunch": true }
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -109,7 +124,7 @@ async fn send_environment_info(websocket: &mut WebSocketStream<TcpStream>) {
|
||||
async fn serve_exec_with_pushed_events(
|
||||
listener: TcpListener,
|
||||
scenario: PushedExecScenario,
|
||||
) -> usize {
|
||||
) -> PushedExecServerResult {
|
||||
let mut websocket = accept_initialized_exec_server(listener).await;
|
||||
send_environment_info(&mut websocket).await;
|
||||
|
||||
@@ -117,6 +132,9 @@ async fn serve_exec_with_pushed_events(
|
||||
let request = read_exec_server_json(&mut websocket).await;
|
||||
match request["method"].as_str() {
|
||||
Some("process/start") => break request,
|
||||
Some("environment/info") => {
|
||||
respond_environment_info(&mut websocket, &request["id"]).await;
|
||||
}
|
||||
Some("fs/getMetadata") => {
|
||||
send_exec_server_json(
|
||||
&mut websocket,
|
||||
@@ -360,20 +378,25 @@ async fn serve_exec_with_pushed_events(
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
return process_read_requests;
|
||||
return PushedExecServerResult {
|
||||
process_read_requests,
|
||||
process_start,
|
||||
};
|
||||
}
|
||||
method => panic!("unexpected exec-server request: {method:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(PushedExecScenario::Complete ; "complete_event_stream")]
|
||||
#[test_case(PushedExecScenario::DirectDenied ; "direct_sandbox_denial")]
|
||||
#[test_case(PushedExecScenario::LegacyExit ; "legacy_exit_metadata")]
|
||||
#[test_case(PushedExecScenario::ReplayGap ; "truncated_event_replay")]
|
||||
#[test_case(PushedExecScenario::Complete, false ; "complete_event_stream")]
|
||||
#[test_case(PushedExecScenario::DirectDenied, false ; "direct_sandbox_denial")]
|
||||
#[test_case(PushedExecScenario::LegacyExit, false ; "legacy_exit_metadata")]
|
||||
#[test_case(PushedExecScenario::ReplayGap, false ; "truncated_event_replay")]
|
||||
#[test_case(PushedExecScenario::Complete, true ; "managed_network_uses_executor_proxy_launch")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_command_consumes_pushed_remote_process_events(
|
||||
scenario: PushedExecScenario,
|
||||
managed_network: bool,
|
||||
) -> Result<()> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let server = start_mock_server().await;
|
||||
@@ -386,7 +409,7 @@ async fn exec_command_consumes_pushed_remote_process_events(
|
||||
CALL_ID,
|
||||
"exec_command",
|
||||
&json!({
|
||||
"cmd": "ignored by fake exec-server",
|
||||
"cmd": "pwd",
|
||||
"yield_time_ms": 1_000,
|
||||
})
|
||||
.to_string(),
|
||||
@@ -403,22 +426,46 @@ async fn exec_command_consumes_pushed_remote_process_events(
|
||||
.await;
|
||||
let exec_server_url = format!("ws://{}", listener.local_addr()?);
|
||||
let exec_server = tokio::spawn(serve_exec_with_pushed_events(listener, scenario));
|
||||
let mut builder = test_codex()
|
||||
.with_exec_server_url(exec_server_url)
|
||||
.with_config(|config| {
|
||||
config.project_doc_max_bytes = 0;
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let mut builder = test_codex().with_exec_server_url(exec_server_url);
|
||||
if managed_network {
|
||||
builder = builder
|
||||
.with_cloud_config_bundle(managed_network_requirements_loader())
|
||||
.with_pre_build_hook(|home| {
|
||||
fs::write(
|
||||
home.join("config.toml"),
|
||||
r#"default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.filesystem]
|
||||
":minimal" = "read"
|
||||
|
||||
[permissions.workspace.network]
|
||||
enabled = true
|
||||
mode = "full"
|
||||
allow_local_binding = true
|
||||
"#,
|
||||
)
|
||||
.expect("write managed-network test config");
|
||||
});
|
||||
}
|
||||
let mut builder = builder.with_config(|config| {
|
||||
config.project_doc_max_bytes = 0;
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = timeout(Duration::from_secs(5), builder.build(&server))
|
||||
.await
|
||||
.context("thread startup should connect to the fake exec-server")??;
|
||||
|
||||
let turn_permission_profile = if managed_network {
|
||||
test.session_configured.permission_profile.clone()
|
||||
} else {
|
||||
PermissionProfile::Disabled
|
||||
};
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
|
||||
turn_permission_fields(turn_permission_profile, test.config.cwd.as_path());
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
@@ -445,22 +492,47 @@ async fn exec_command_consumes_pushed_remote_process_events(
|
||||
})
|
||||
.await?;
|
||||
let mut saw_exec_command_begin = false;
|
||||
loop {
|
||||
let event = timeout(Duration::from_secs(5), test.codex.next_event())
|
||||
.await
|
||||
.context("turn should complete")??
|
||||
.msg;
|
||||
match event {
|
||||
EventMsg::ExecCommandBegin(event) if event.call_id == CALL_ID => {
|
||||
saw_exec_command_begin = true;
|
||||
if !managed_network {
|
||||
loop {
|
||||
let event = timeout(Duration::from_secs(5), test.codex.next_event())
|
||||
.await
|
||||
.context("turn should complete")??
|
||||
.msg;
|
||||
match event {
|
||||
EventMsg::ExecCommandBegin(event) if event.call_id == CALL_ID => {
|
||||
saw_exec_command_begin = true;
|
||||
}
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let process_read_requests = timeout(Duration::from_secs(5), exec_server)
|
||||
let cleanup_timeout = if managed_network {
|
||||
Duration::from_secs(15)
|
||||
} else {
|
||||
Duration::from_secs(5)
|
||||
};
|
||||
let exec_server_result = timeout(cleanup_timeout, exec_server)
|
||||
.await
|
||||
.context("fake exec-server should observe process cleanup")??;
|
||||
if managed_network {
|
||||
let params = &exec_server_result.process_start["params"];
|
||||
assert_eq!(params["enforceManagedNetwork"], true);
|
||||
assert_eq!(params["managedNetwork"], Value::Null);
|
||||
assert_eq!(params["env"]["HTTP_PROXY"], Value::Null);
|
||||
assert_eq!(params["networkProxy"]["proxy"]["enabled"], true);
|
||||
assert_eq!(params["networkProxy"]["proxy"]["mode"], "full");
|
||||
assert_eq!(params["networkProxy"]["environmentId"], "remote");
|
||||
assert!(params["networkProxy"]["executionId"].as_str().is_some());
|
||||
timeout(Duration::from_secs(5), async {
|
||||
while response_mock.requests().len() < 2 {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("model should receive the remote exec output")?;
|
||||
return Ok(());
|
||||
}
|
||||
let request = response_mock
|
||||
.last_request()
|
||||
.context("model should receive the exec_command output")?;
|
||||
@@ -468,6 +540,7 @@ async fn exec_command_consumes_pushed_remote_process_events(
|
||||
.function_call_output_content_and_success(CALL_ID)
|
||||
.context("exec_command output should be model visible")?;
|
||||
let output = output.context("exec_command output should contain text")?;
|
||||
let process_read_requests = exec_server_result.process_read_requests;
|
||||
match scenario {
|
||||
PushedExecScenario::Complete => {
|
||||
assert_ne!(success, Some(false));
|
||||
|
||||
@@ -1372,6 +1372,8 @@ mod tests {
|
||||
tokio::net::TcpStream::connect(proxy_addr)
|
||||
.await
|
||||
.expect("proxy should remain available to a child holding inherited output streams");
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(proxy.network_proxy_restricting_sid(None).is_some());
|
||||
|
||||
drop(process.stdout_tx);
|
||||
drop(process.stderr_tx);
|
||||
@@ -1382,6 +1384,9 @@ mod tests {
|
||||
.await
|
||||
.expect("process should close");
|
||||
assert!(closed_response.closed);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(proxy.network_proxy_restricting_sid(None), None);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(tokio::net::TcpStream::connect(proxy_addr).await.is_err());
|
||||
backend.shutdown().await;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use codex_network_proxy::NetworkProxyHandle;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
|
||||
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_network_proxy::strip_managed_proxy_env;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
|
||||
@@ -48,6 +50,7 @@ struct PreparedWindowsSandboxRequest {
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
proxy_enforced: bool,
|
||||
network_proxy_restricting_sid: Option<String>,
|
||||
filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
|
||||
use_private_desktop: bool,
|
||||
}
|
||||
@@ -61,6 +64,7 @@ impl PreparedExecRequest {
|
||||
workspace_roots: &request.workspace_roots,
|
||||
windows_sandbox_level: request.windows_sandbox_level,
|
||||
proxy_enforced: request.proxy_enforced,
|
||||
network_proxy_restricting_sid: request.network_proxy_restricting_sid.as_deref(),
|
||||
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
|
||||
filesystem_overrides: request.filesystem_overrides.as_ref(),
|
||||
use_private_desktop: request.use_private_desktop,
|
||||
@@ -73,12 +77,24 @@ pub(crate) async fn prepare_exec_request(
|
||||
env: HashMap<String, String>,
|
||||
runtime_paths: Option<&ExecServerRuntimePaths>,
|
||||
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
|
||||
let (env, managed_network, network_proxy_handle) = prepare_managed_network(
|
||||
params.managed_network.as_ref(),
|
||||
params.network_proxy.as_ref(),
|
||||
env,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut env = env;
|
||||
#[cfg(target_os = "windows")]
|
||||
let network_proxy = if params.sandbox.is_none() {
|
||||
// Shared Windows ingress selects a route from the sandbox token's SID. Native launches
|
||||
// have no route SID, so leave them direct.
|
||||
if params.network_proxy.is_some() {
|
||||
strip_managed_proxy_env(&mut env);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
params.network_proxy.as_ref()
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let network_proxy = params.network_proxy.as_ref();
|
||||
|
||||
let (env, managed_network, network_proxy_handle, network_proxy_restricting_sid) =
|
||||
prepare_managed_network(params.managed_network.as_ref(), network_proxy, env).await?;
|
||||
let Some(sandbox_context) = params.sandbox.as_ref() else {
|
||||
return Ok(PreparedExecRequest {
|
||||
command: params.argv.clone(),
|
||||
@@ -246,6 +262,7 @@ pub(crate) async fn prepare_exec_request(
|
||||
workspace_roots: native_workspace_roots,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid,
|
||||
filesystem_overrides,
|
||||
use_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
||||
})
|
||||
@@ -272,11 +289,12 @@ async fn prepare_managed_network(
|
||||
HashMap<String, String>,
|
||||
Option<ManagedNetworkSandboxContext>,
|
||||
Option<NetworkProxyHandle>,
|
||||
Option<String>,
|
||||
),
|
||||
JSONRPCErrorError,
|
||||
> {
|
||||
let Some(network_proxy) = network_proxy.cloned() else {
|
||||
return Ok((env, managed_network.cloned(), None));
|
||||
return Ok((env, managed_network.cloned(), None, None));
|
||||
};
|
||||
let state = NetworkProxyState::from_remote_launch_config(network_proxy)
|
||||
.map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?;
|
||||
@@ -289,12 +307,29 @@ async fn prepare_managed_network(
|
||||
.run()
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to start executor network proxy: {err}")))?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let network_proxy_restricting_sid = Some(
|
||||
proxy
|
||||
.network_proxy_restricting_sid(/*environment_id*/ None)
|
||||
.ok_or_else(|| {
|
||||
internal_error(
|
||||
"managed Windows proxy route is missing its restricting SID".to_string(),
|
||||
)
|
||||
})?,
|
||||
);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let network_proxy_restricting_sid = None;
|
||||
let prepared = proxy
|
||||
.prepare_for_optional_environment(env, /*environment_id*/ None)
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to prepare executor network proxy: {err}"))
|
||||
})?;
|
||||
Ok((prepared.env, Some(prepared.sandbox_context), Some(handle)))
|
||||
Ok((
|
||||
prepared.env,
|
||||
Some(prepared.sandbox_context),
|
||||
Some(handle),
|
||||
network_proxy_restricting_sid,
|
||||
))
|
||||
}
|
||||
|
||||
fn native_path(path: &PathUri, label: &str) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
|
||||
|
||||
@@ -231,7 +231,7 @@ async fn native_request_preserves_native_launch_fields() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_proxy_config_starts_executor_local_proxy() {
|
||||
async fn native_request_handles_remote_proxy_config_for_platform() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
@@ -261,12 +261,22 @@ async fn remote_proxy_config_starts_executor_local_proxy() {
|
||||
),
|
||||
};
|
||||
let stale_proxy = "http://127.0.0.1:9".to_string();
|
||||
let env = HashMap::from([("HTTP_PROXY".to_string(), stale_proxy.clone())]);
|
||||
let env = HashMap::from([
|
||||
("HTTP_PROXY".to_string(), stale_proxy.clone()),
|
||||
("TEST_ENV".to_string(), "value".to_string()),
|
||||
]);
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, env, /*runtime_paths*/ None)
|
||||
.await
|
||||
.expect("prepare request with executor-local proxy");
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(prepared.env.get("HTTP_PROXY"), None);
|
||||
assert_eq!(prepared.env.get("TEST_ENV"), Some(&"value".to_string()));
|
||||
assert!(prepared.network_proxy_handle.is_none());
|
||||
return;
|
||||
}
|
||||
|
||||
let http_proxy = prepared.env.get("HTTP_PROXY").expect("HTTP proxy env");
|
||||
assert_ne!(http_proxy, &stale_proxy);
|
||||
assert!(http_proxy.starts_with("http://127.0.0.1:"));
|
||||
@@ -297,6 +307,7 @@ async fn remote_proxy_config_starts_executor_local_proxy() {
|
||||
.expect("shut down executor proxy");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[tokio::test]
|
||||
async fn disabled_remote_proxy_config_is_rejected_before_exporting_ports() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
@@ -350,6 +361,12 @@ async fn managed_network_selects_elevated_windows_spawn() {
|
||||
cwd_uri.clone(),
|
||||
);
|
||||
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
|
||||
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig {
|
||||
enabled: true,
|
||||
enable_socks5: false,
|
||||
..NetworkProxyConfig::default()
|
||||
})
|
||||
.expect("supported remote proxy config");
|
||||
let params = ExecParams {
|
||||
process_id: ProcessId::from("process-managed-network"),
|
||||
argv: vec!["cmd.exe".to_string(), "/c".to_string(), "exit".to_string()],
|
||||
@@ -362,21 +379,31 @@ async fn managed_network_selects_elevated_windows_spawn() {
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: true,
|
||||
managed_network: None,
|
||||
network_proxy: None,
|
||||
network_proxy: Some(RemoteNetworkProxyLaunchConfig::new(proxy_config)),
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
let mut prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
.await
|
||||
.expect("prepare sandboxed request");
|
||||
let spawn = prepared
|
||||
.windows_sandbox_spawn_request()
|
||||
.expect("Windows sandbox spawn request");
|
||||
{
|
||||
let spawn = prepared
|
||||
.windows_sandbox_spawn_request()
|
||||
.expect("Windows sandbox spawn request");
|
||||
|
||||
assert_eq!(
|
||||
spawn.windows_sandbox_level,
|
||||
WindowsSandboxLevel::RestrictedToken
|
||||
);
|
||||
assert!(spawn.proxy_enforced);
|
||||
assert_eq!(spawn.permission_profile, &permissions);
|
||||
assert_eq!(spawn.workspace_roots, std::slice::from_ref(&cwd));
|
||||
assert_eq!(
|
||||
spawn.windows_sandbox_level,
|
||||
WindowsSandboxLevel::RestrictedToken
|
||||
);
|
||||
assert!(spawn.proxy_enforced);
|
||||
assert!(spawn.network_proxy_restricting_sid.is_some());
|
||||
assert_eq!(spawn.permission_profile, &permissions);
|
||||
assert_eq!(spawn.workspace_roots, std::slice::from_ref(&cwd));
|
||||
}
|
||||
prepared
|
||||
.network_proxy_handle
|
||||
.take()
|
||||
.expect("running executor proxy")
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("shut down executor proxy");
|
||||
}
|
||||
|
||||
@@ -51,3 +51,14 @@ security-framework = "3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
schannel = "0.1"
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
codex-windows-sandbox = { path = "../windows-sandbox-rs" }
|
||||
|
||||
@@ -430,6 +430,26 @@ pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the sorted loopback ports used by the configured managed proxy listeners.
|
||||
pub fn managed_proxy_ports(cfg: &NetworkProxyConfig) -> Result<Vec<u16>> {
|
||||
let runtime = resolve_runtime(cfg)?;
|
||||
if runtime.http_addr.port() == 0 {
|
||||
bail!("network.proxy_url must use a fixed non-zero port for managed proxy provisioning");
|
||||
}
|
||||
let mut ports = vec![runtime.http_addr.port()];
|
||||
if cfg.enable_socks5 {
|
||||
if runtime.socks_addr.port() == 0 {
|
||||
bail!(
|
||||
"network.socks_url must use a fixed non-zero port for managed proxy provisioning"
|
||||
);
|
||||
}
|
||||
ports.push(runtime.socks_addr.port());
|
||||
}
|
||||
ports.sort_unstable();
|
||||
ports.dedup();
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
fn resolve_addr(url: &str, default_port: u16) -> Result<SocketAddr> {
|
||||
let addr_parts = parse_host_port(url, default_port)?;
|
||||
let host = if addr_parts.host.eq_ignore_ascii_case("localhost") {
|
||||
@@ -605,6 +625,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_ports_reject_ephemeral_ports() {
|
||||
let mut config = NetworkProxyConfig {
|
||||
proxy_url: "http://127.0.0.1:0".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
managed_proxy_ports(&config).unwrap_err().to_string(),
|
||||
"network.proxy_url must use a fixed non-zero port for managed proxy provisioning"
|
||||
);
|
||||
|
||||
config.proxy_url = "http://127.0.0.1:3128".to_string();
|
||||
config.socks_url = "socks5h://127.0.0.1:48081".to_string();
|
||||
assert_eq!(managed_proxy_ports(&config).unwrap(), vec![3128, 48081]);
|
||||
|
||||
config.socks_url = "socks5h://127.0.0.1:0".to_string();
|
||||
assert_eq!(
|
||||
managed_proxy_ports(&config).unwrap_err().to_string(),
|
||||
"network.socks_url must use a fixed non-zero port for managed proxy provisioning"
|
||||
);
|
||||
|
||||
config.enable_socks5 = false;
|
||||
assert_eq!(managed_proxy_ports(&config).unwrap(), vec![3128]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_proxy_config_uses_struct_defaults_for_missing_fields() {
|
||||
let config: NetworkProxyConfig = serde_json::from_str(r#"{ "enabled": true }"#).unwrap();
|
||||
|
||||
@@ -40,6 +40,7 @@ use rama_core::error::ErrorExt as _;
|
||||
use rama_core::error::OpaqueError;
|
||||
use rama_core::extensions::ExtensionsMut;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::service::BoxService;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_core::stream::Stream;
|
||||
use rama_http::Body;
|
||||
@@ -66,6 +67,7 @@ use rama_net::proxy::ProxyRequest;
|
||||
use rama_net::proxy::ProxyTarget;
|
||||
use rama_net::proxy::StreamForwardService;
|
||||
use rama_net::stream::SocketInfo;
|
||||
use rama_tcp::TcpStream;
|
||||
use rama_tcp::client::Request as TcpRequest;
|
||||
use rama_tcp::server::TcpListener;
|
||||
use rama_tls_rustls::client::TlsConnectorDataBuilder;
|
||||
@@ -124,12 +126,25 @@ async fn run_http_proxy_with_listener(
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
environment_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
let addr = listener
|
||||
.local_addr()
|
||||
.context("read HTTP proxy listener local addr")?;
|
||||
|
||||
info!("HTTP proxy listening on {addr}");
|
||||
|
||||
listener
|
||||
.serve(http_proxy_service(state, policy_decider, environment_id))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn http_proxy_service(
|
||||
state: Arc<NetworkProxyState>,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
environment_id: Option<String>,
|
||||
) -> BoxService<TcpStream, (), rama_core::error::BoxError> {
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
// This proxy listener only needs HTTP/1 proxy semantics. Using Rama's auto builder
|
||||
// forces every accepted socket through the HTTP version sniffing pre-read path before proxy
|
||||
// request parsing, which can stall some local clients on macOS before CONNECT/absolute-form
|
||||
@@ -156,16 +171,7 @@ async fn run_http_proxy_with_listener(
|
||||
})),
|
||||
);
|
||||
|
||||
info!("HTTP proxy listening on {addr}");
|
||||
|
||||
listener
|
||||
.serve(BindConnectionAttribution::new(
|
||||
http_service,
|
||||
state,
|
||||
environment_id,
|
||||
))
|
||||
.await;
|
||||
Ok(())
|
||||
BindConnectionAttribution::new(http_service, state, environment_id).boxed()
|
||||
}
|
||||
|
||||
async fn http_connect_accept(
|
||||
|
||||
@@ -19,6 +19,10 @@ mod runtime;
|
||||
mod socks5;
|
||||
mod state;
|
||||
mod upstream;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_proxy_ingress;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_tcp_attribution;
|
||||
|
||||
pub use attribution::PROXY_ATTRIBUTION_TOKEN_ENV_KEY;
|
||||
pub use attribution::write_attribution_frame;
|
||||
@@ -32,6 +36,7 @@ pub use config::NetworkProxyConfig;
|
||||
pub use config::NetworkUnixSocketPermission;
|
||||
pub use config::NetworkUnixSocketPermissions;
|
||||
pub use config::host_and_port_from_network_addr;
|
||||
pub use config::managed_proxy_ports;
|
||||
pub use credential_broker::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
|
||||
pub use credential_broker::brokered_credential_dummy_env_keys;
|
||||
pub use credential_broker::brokered_credential_env_keys;
|
||||
@@ -67,7 +72,9 @@ pub use proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
||||
pub use proxy::PROXY_URL_ENV_KEYS;
|
||||
pub use proxy::PreparedManagedNetwork;
|
||||
pub use proxy::has_proxy_url_env_vars;
|
||||
pub use proxy::is_managed_proxy_env_var;
|
||||
pub use proxy::proxy_url_env_value;
|
||||
pub use proxy::strip_managed_proxy_env;
|
||||
pub use remote_config::RemoteNetworkProxyConfig;
|
||||
pub use remote_config::RemoteNetworkProxyLaunchConfig;
|
||||
pub use runtime::BlockedRequest;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ use rama_core::error::BoxError;
|
||||
use rama_core::extensions::Extensions;
|
||||
use rama_core::extensions::ExtensionsMut;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::service::BoxService;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_net::address::HostWithPort;
|
||||
use rama_net::client::EstablishedClientConnection;
|
||||
@@ -129,6 +130,23 @@ async fn run_socks5_with_listener(
|
||||
}
|
||||
}
|
||||
|
||||
listener
|
||||
.serve(socks5_proxy_service(
|
||||
state,
|
||||
policy_decider,
|
||||
environment_id,
|
||||
enable_socks5_udp,
|
||||
))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn socks5_proxy_service(
|
||||
state: Arc<NetworkProxyState>,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
environment_id: Option<String>,
|
||||
enable_socks5_udp: bool,
|
||||
) -> BoxService<TcpStream, (), BoxError> {
|
||||
let tcp_connector = TargetCheckedTcpConnector::new(state.clone());
|
||||
let policy_tcp_connector = service_fn({
|
||||
let policy_decider = policy_decider.clone();
|
||||
@@ -163,19 +181,10 @@ async fn run_socks5_with_listener(
|
||||
}
|
||||
}));
|
||||
let socks_acceptor = base.with_udp_associator(udp_relay);
|
||||
listener
|
||||
.serve(BindConnectionAttribution::new(
|
||||
socks_acceptor,
|
||||
state,
|
||||
environment_id,
|
||||
))
|
||||
.await;
|
||||
BindConnectionAttribution::new(socks_acceptor, state, environment_id).boxed()
|
||||
} else {
|
||||
listener
|
||||
.serve(BindConnectionAttribution::new(base, state, environment_id))
|
||||
.await;
|
||||
BindConnectionAttribution::new(base, state, environment_id).boxed()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_socks5_tcp(
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
use crate::proxy::reserve_windows_managed_listeners;
|
||||
use crate::proxy::reserve_windows_managed_socks_listener;
|
||||
use crate::proxy::windows_managed_loopback_addr;
|
||||
use crate::windows_tcp_attribution::restricting_sids_for_tcp_connection;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use rama_core::Service;
|
||||
use rama_core::error::BoxError;
|
||||
use rama_core::service::BoxService;
|
||||
use rama_net::stream::Socket;
|
||||
use rama_tcp::TcpStream;
|
||||
use rama_tcp::server::TcpListener;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
#[cfg(test)]
|
||||
use std::sync::Weak;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::info;
|
||||
|
||||
pub(crate) type WindowsRouteService = BoxService<TcpStream, (), BoxError>;
|
||||
|
||||
// Production keeps the listeners alive for the process lifetime so their ports remain stable even
|
||||
// when no routes are registered. Crate tests use independent Tokio runtimes and requested ports, so
|
||||
// they retain only a weak reference and can tear each ingress down between tests.
|
||||
#[cfg(not(test))]
|
||||
static SHARED_INGRESS: LazyLock<Mutex<Option<Arc<WindowsProxyIngress>>>> =
|
||||
LazyLock::new(|| Mutex::new(None));
|
||||
#[cfg(test)]
|
||||
static SHARED_INGRESS: LazyLock<Mutex<Weak<WindowsProxyIngress>>> =
|
||||
LazyLock::new(|| Mutex::new(Weak::new()));
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RouteServices {
|
||||
http: WindowsRouteService,
|
||||
socks: Option<WindowsRouteService>,
|
||||
}
|
||||
|
||||
type RouteRegistry = Arc<Mutex<HashMap<String, Arc<RouteServices>>>>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ProxyProtocol {
|
||||
Http,
|
||||
Socks,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IngressDispatcher {
|
||||
routes: RouteRegistry,
|
||||
protocol: ProxyProtocol,
|
||||
}
|
||||
|
||||
impl Service<TcpStream> for IngressDispatcher {
|
||||
type Output = ();
|
||||
type Error = BoxError;
|
||||
|
||||
async fn serve(&self, stream: TcpStream) -> Result<(), BoxError> {
|
||||
let local_addr = stream.local_addr()?;
|
||||
let peer_addr = stream.peer_addr()?;
|
||||
let restricting_sids = restricting_sids_for_tcp_connection(local_addr, peer_addr)?;
|
||||
let route = {
|
||||
let routes = self
|
||||
.routes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
registered_route_for_sids(&routes, &restricting_sids)?
|
||||
};
|
||||
let service = match self.protocol {
|
||||
ProxyProtocol::Http => route.http.clone(),
|
||||
ProxyProtocol::Socks => route.socks.clone().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"network proxy route does not enable SOCKS5",
|
||||
)
|
||||
})?,
|
||||
};
|
||||
service.serve(stream).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsProxyIngress {
|
||||
http_addr: SocketAddr,
|
||||
routes: RouteRegistry,
|
||||
runtime: Handle,
|
||||
http_task: JoinHandle<()>,
|
||||
socks: Mutex<SocksListenerState>,
|
||||
}
|
||||
|
||||
struct SocksListenerState {
|
||||
addr: SocketAddr,
|
||||
task: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl WindowsProxyIngress {
|
||||
pub(crate) fn shared(
|
||||
requested_http_addr: SocketAddr,
|
||||
requested_socks_addr: SocketAddr,
|
||||
reserve_socks_listener: bool,
|
||||
) -> Result<Arc<Self>> {
|
||||
let requested_http_addr = windows_managed_loopback_addr(requested_http_addr);
|
||||
let requested_socks_addr = windows_managed_loopback_addr(requested_socks_addr);
|
||||
let mut shared = SHARED_INGRESS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
#[cfg(not(test))]
|
||||
if let Some(ingress) = shared.as_ref()
|
||||
&& ingress.is_running()
|
||||
{
|
||||
if reserve_socks_listener {
|
||||
ingress.ensure_socks_listener(requested_socks_addr)?;
|
||||
}
|
||||
return Ok(Arc::clone(ingress));
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
shared.take();
|
||||
#[cfg(test)]
|
||||
if let Some(ingress) = shared.upgrade()
|
||||
&& ingress.is_running()
|
||||
{
|
||||
if reserve_socks_listener {
|
||||
ingress.ensure_socks_listener(requested_socks_addr)?;
|
||||
}
|
||||
return Ok(ingress);
|
||||
}
|
||||
|
||||
let listeners = reserve_windows_managed_listeners(
|
||||
requested_http_addr,
|
||||
requested_socks_addr,
|
||||
reserve_socks_listener,
|
||||
)
|
||||
.context("reserve shared managed Windows proxy ingress")?;
|
||||
let http_addr = listeners.http_addr()?;
|
||||
let socks_addr = listeners.socks_addr(requested_socks_addr)?;
|
||||
let (http_listener, socks_listener) = listeners.into_listeners();
|
||||
let http_listener =
|
||||
TcpListener::try_from(http_listener).context("convert shared HTTP ingress listener")?;
|
||||
let socks_listener = socks_listener
|
||||
.map(TcpListener::try_from)
|
||||
.transpose()
|
||||
.context("convert shared SOCKS5 ingress listener")?;
|
||||
let runtime =
|
||||
Handle::try_current().context("start shared managed Windows proxy ingress")?;
|
||||
let routes = Arc::new(Mutex::new(HashMap::new()));
|
||||
let http_task = runtime.spawn(run_listener(
|
||||
http_listener,
|
||||
IngressDispatcher {
|
||||
routes: Arc::clone(&routes),
|
||||
protocol: ProxyProtocol::Http,
|
||||
},
|
||||
"HTTP",
|
||||
http_addr,
|
||||
));
|
||||
let socks_task = socks_listener
|
||||
.map(|listener| spawn_socks_listener(&runtime, &routes, listener, socks_addr));
|
||||
let ingress = Arc::new(Self {
|
||||
http_addr,
|
||||
routes,
|
||||
runtime,
|
||||
http_task,
|
||||
socks: Mutex::new(SocksListenerState {
|
||||
addr: socks_addr,
|
||||
task: socks_task,
|
||||
}),
|
||||
});
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
*shared = Some(Arc::clone(&ingress));
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
*shared = Arc::downgrade(&ingress);
|
||||
}
|
||||
Ok(ingress)
|
||||
}
|
||||
|
||||
pub(crate) fn http_addr(&self) -> SocketAddr {
|
||||
self.http_addr
|
||||
}
|
||||
|
||||
pub(crate) fn socks_addr(&self) -> SocketAddr {
|
||||
self.socks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.addr
|
||||
}
|
||||
|
||||
pub(crate) fn active_socks_addr(&self) -> Option<SocketAddr> {
|
||||
let socks = self
|
||||
.socks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
socks
|
||||
.task
|
||||
.as_ref()
|
||||
.filter(|task| !task.is_finished())
|
||||
.map(|_| socks.addr)
|
||||
}
|
||||
|
||||
pub(crate) fn register_route(
|
||||
self: &Arc<Self>,
|
||||
http: WindowsRouteService,
|
||||
socks: Option<WindowsRouteService>,
|
||||
) -> WindowsProxyRoute {
|
||||
let services = Arc::new(RouteServices { http, socks });
|
||||
let mut routes = self
|
||||
.routes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let sid = loop {
|
||||
let sid = random_restricting_sid();
|
||||
if !routes.contains_key(&sid) {
|
||||
break sid;
|
||||
}
|
||||
};
|
||||
routes.insert(sid.clone(), Arc::clone(&services));
|
||||
WindowsProxyRoute {
|
||||
sid,
|
||||
services,
|
||||
ingress: Arc::clone(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
!self.http_task.is_finished()
|
||||
&& self
|
||||
.socks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.task
|
||||
.as_ref()
|
||||
.is_none_or(|task| !task.is_finished())
|
||||
}
|
||||
|
||||
fn ensure_socks_listener(&self, requested_addr: SocketAddr) -> Result<()> {
|
||||
let mut socks = self
|
||||
.socks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(task) = socks.task.as_ref() {
|
||||
anyhow::ensure!(
|
||||
!task.is_finished(),
|
||||
"shared managed Windows SOCKS5 ingress stopped"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let listener = reserve_windows_managed_socks_listener(requested_addr)
|
||||
.context("reserve shared managed Windows SOCKS5 ingress")?;
|
||||
let addr = listener
|
||||
.local_addr()
|
||||
.context("read shared managed Windows SOCKS5 ingress address")?;
|
||||
let listener = {
|
||||
let _runtime = self.runtime.enter();
|
||||
TcpListener::try_from(listener)
|
||||
}
|
||||
.context("convert shared SOCKS5 ingress listener")?;
|
||||
let task = spawn_socks_listener(&self.runtime, &self.routes, listener, addr);
|
||||
socks.addr = addr;
|
||||
socks.task = Some(task);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WindowsProxyIngress {
|
||||
fn drop(&mut self) {
|
||||
self.http_task.abort();
|
||||
let socks = self
|
||||
.socks
|
||||
.get_mut()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(socks_task) = socks.task.as_ref() {
|
||||
socks_task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_socks_listener(
|
||||
runtime: &Handle,
|
||||
routes: &RouteRegistry,
|
||||
listener: TcpListener,
|
||||
addr: SocketAddr,
|
||||
) -> JoinHandle<()> {
|
||||
runtime.spawn(run_listener(
|
||||
listener,
|
||||
IngressDispatcher {
|
||||
routes: Arc::clone(routes),
|
||||
protocol: ProxyProtocol::Socks,
|
||||
},
|
||||
"SOCKS5",
|
||||
addr,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsProxyRoute {
|
||||
sid: String,
|
||||
services: Arc<RouteServices>,
|
||||
ingress: Arc<WindowsProxyIngress>,
|
||||
}
|
||||
|
||||
impl WindowsProxyRoute {
|
||||
pub(crate) fn sid(&self) -> &str {
|
||||
&self.sid
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WindowsProxyRoute {
|
||||
fn drop(&mut self) {
|
||||
let mut routes = self
|
||||
.ingress
|
||||
.routes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if routes
|
||||
.get(&self.sid)
|
||||
.is_some_and(|services| Arc::ptr_eq(services, &self.services))
|
||||
{
|
||||
routes.remove(&self.sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_listener(
|
||||
listener: TcpListener,
|
||||
dispatcher: IngressDispatcher,
|
||||
protocol: &'static str,
|
||||
addr: SocketAddr,
|
||||
) {
|
||||
info!("shared managed Windows {protocol} proxy ingress listening on {addr}");
|
||||
listener.serve(dispatcher).await;
|
||||
}
|
||||
|
||||
fn registered_route_for_sids(
|
||||
routes: &HashMap<String, Arc<RouteServices>>,
|
||||
restricting_sids: &[String],
|
||||
) -> io::Result<Arc<RouteServices>> {
|
||||
let mut matching_routes = restricting_sids
|
||||
.iter()
|
||||
.filter_map(|sid| routes.get(sid).cloned());
|
||||
let route = matching_routes.next().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"proxy client token has no registered network proxy route SID",
|
||||
)
|
||||
})?;
|
||||
if matching_routes.next().is_some() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"proxy client token has multiple registered network proxy route SIDs",
|
||||
));
|
||||
}
|
||||
Ok(route)
|
||||
}
|
||||
|
||||
fn random_restricting_sid() -> String {
|
||||
let a = rand::random::<u32>();
|
||||
let b = rand::random::<u32>();
|
||||
let c = rand::random::<u32>();
|
||||
let d = rand::random::<u32>();
|
||||
format!("S-1-5-21-{a}-{b}-{c}-{d}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "windows_proxy_ingress_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,43 @@
|
||||
use super::*;
|
||||
use rama_core::service::service_fn;
|
||||
|
||||
#[test]
|
||||
fn selects_exactly_one_registered_route() {
|
||||
let route = route_services();
|
||||
let routes = HashMap::from([("registered".to_string(), Arc::clone(&route))]);
|
||||
|
||||
let selected = registered_route_for_sids(
|
||||
&routes,
|
||||
&["unrelated".to_string(), "registered".to_string()],
|
||||
)
|
||||
.expect("one registered route should be selected");
|
||||
|
||||
assert!(Arc::ptr_eq(&selected, &route));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_or_ambiguous_registered_routes() {
|
||||
let first = route_services();
|
||||
let second = route_services();
|
||||
let routes = HashMap::from([("first".to_string(), first), ("second".to_string(), second)]);
|
||||
|
||||
let Err(missing) = registered_route_for_sids(&routes, &["missing".to_string()]) else {
|
||||
panic!("an unknown SID should fail closed");
|
||||
};
|
||||
let Err(ambiguous) =
|
||||
registered_route_for_sids(&routes, &["first".to_string(), "second".to_string()])
|
||||
else {
|
||||
panic!("multiple registered SIDs should fail closed");
|
||||
};
|
||||
|
||||
assert_eq!(missing.kind(), io::ErrorKind::PermissionDenied);
|
||||
assert_eq!(ambiguous.kind(), io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
|
||||
fn route_services() -> Arc<RouteServices> {
|
||||
let service = service_fn(|_stream: TcpStream| async { Ok::<(), BoxError>(()) }).boxed();
|
||||
Arc::new(RouteServices {
|
||||
http: service,
|
||||
socks: None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
use std::ffi::c_void;
|
||||
use std::io;
|
||||
use std::mem::offset_of;
|
||||
use std::mem::size_of;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::SocketAddrV4;
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::os::windows::io::OwnedHandle;
|
||||
use std::os::windows::io::RawHandle;
|
||||
|
||||
use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::NO_ERROR;
|
||||
use windows_sys::Win32::Foundation::PSID;
|
||||
use windows_sys::Win32::NetworkManagement::IpHelper::GetExtendedTcpTable;
|
||||
use windows_sys::Win32::NetworkManagement::IpHelper::MIB_TCPROW_OWNER_PID;
|
||||
use windows_sys::Win32::NetworkManagement::IpHelper::MIB_TCPTABLE_OWNER_PID;
|
||||
use windows_sys::Win32::NetworkManagement::IpHelper::TCP_TABLE_OWNER_PID_CONNECTIONS;
|
||||
use windows_sys::Win32::Networking::WinSock::AF_INET;
|
||||
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
|
||||
use windows_sys::Win32::Security::GetTokenInformation;
|
||||
use windows_sys::Win32::Security::SID_AND_ATTRIBUTES;
|
||||
use windows_sys::Win32::Security::TOKEN_GROUPS;
|
||||
use windows_sys::Win32::Security::TOKEN_QUERY;
|
||||
use windows_sys::Win32::Security::TokenRestrictedSids;
|
||||
use windows_sys::Win32::System::Threading::OpenProcess;
|
||||
use windows_sys::Win32::System::Threading::OpenProcessToken;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
|
||||
|
||||
/// Returns the restricting SIDs on the process that opened an accepted loopback connection.
|
||||
///
|
||||
/// `accepted_local_addr` and `accepted_peer_addr` must come from the accepted server socket. The
|
||||
/// owning-PID table describes the client side in the opposite direction, so the lookup matches the
|
||||
/// exact reversed four-tuple.
|
||||
pub(crate) fn restricting_sids_for_tcp_connection(
|
||||
accepted_local_addr: SocketAddr,
|
||||
accepted_peer_addr: SocketAddr,
|
||||
) -> io::Result<Vec<String>> {
|
||||
let (SocketAddr::V4(accepted_local_addr), SocketAddr::V4(accepted_peer_addr)) =
|
||||
(accepted_local_addr, accepted_peer_addr)
|
||||
else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Windows proxy connection attribution currently supports IPv4 only",
|
||||
));
|
||||
};
|
||||
|
||||
let process_id = owning_process_id(accepted_local_addr, accepted_peer_addr)?;
|
||||
restricting_sids_for_process(process_id)
|
||||
}
|
||||
|
||||
fn owning_process_id(
|
||||
accepted_local_addr: SocketAddrV4,
|
||||
accepted_peer_addr: SocketAddrV4,
|
||||
) -> io::Result<u32> {
|
||||
let mut byte_len = 0_u32;
|
||||
let result = unsafe {
|
||||
GetExtendedTcpTable(
|
||||
std::ptr::null_mut(),
|
||||
&mut byte_len,
|
||||
0,
|
||||
AF_INET as u32,
|
||||
TCP_TABLE_OWNER_PID_CONNECTIONS,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if result != ERROR_INSUFFICIENT_BUFFER {
|
||||
return Err(win32_error("query IPv4 TCP owner table size", result));
|
||||
}
|
||||
|
||||
let buffer = loop {
|
||||
let mut buffer = aligned_buffer(byte_len as usize)?;
|
||||
let result = unsafe {
|
||||
GetExtendedTcpTable(
|
||||
buffer.as_mut_ptr().cast::<c_void>(),
|
||||
&mut byte_len,
|
||||
0,
|
||||
AF_INET as u32,
|
||||
TCP_TABLE_OWNER_PID_CONNECTIONS,
|
||||
0,
|
||||
)
|
||||
};
|
||||
match result {
|
||||
NO_ERROR => break buffer,
|
||||
ERROR_INSUFFICIENT_BUFFER => continue,
|
||||
_ => return Err(win32_error("read IPv4 TCP owner table", result)),
|
||||
}
|
||||
};
|
||||
|
||||
let rows = parse_tcp_owner_rows(&buffer, byte_len as usize)?;
|
||||
unique_client_process_id(rows, accepted_local_addr, accepted_peer_addr)
|
||||
}
|
||||
|
||||
fn parse_tcp_owner_rows(buffer: &[usize], byte_len: usize) -> io::Result<&[MIB_TCPROW_OWNER_PID]> {
|
||||
let rows_offset = offset_of!(MIB_TCPTABLE_OWNER_PID, table);
|
||||
if byte_len > size_of_val(buffer) || byte_len < rows_offset {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"invalid IPv4 TCP owner table length",
|
||||
));
|
||||
}
|
||||
|
||||
let row_count = unsafe { std::ptr::read_unaligned(buffer.as_ptr().cast::<u32>()) } as usize;
|
||||
let rows_byte_len = row_count
|
||||
.checked_mul(size_of::<MIB_TCPROW_OWNER_PID>())
|
||||
.and_then(|len| rows_offset.checked_add(len))
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"IPv4 TCP owner table length overflow",
|
||||
)
|
||||
})?;
|
||||
if rows_byte_len > byte_len {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"truncated IPv4 TCP owner table",
|
||||
));
|
||||
}
|
||||
|
||||
let rows = unsafe {
|
||||
let rows_ptr = buffer
|
||||
.as_ptr()
|
||||
.cast::<u8>()
|
||||
.add(rows_offset)
|
||||
.cast::<MIB_TCPROW_OWNER_PID>();
|
||||
std::slice::from_raw_parts(rows_ptr, row_count)
|
||||
};
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn unique_client_process_id(
|
||||
rows: &[MIB_TCPROW_OWNER_PID],
|
||||
accepted_local_addr: SocketAddrV4,
|
||||
accepted_peer_addr: SocketAddrV4,
|
||||
) -> io::Result<u32> {
|
||||
let mut matching_process_ids = rows
|
||||
.iter()
|
||||
.filter(|row| client_row_matches(row, accepted_local_addr, accepted_peer_addr))
|
||||
.map(|row| row.dwOwningPid);
|
||||
let process_id = matching_process_ids.next().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"accepted connection is absent from the IPv4 TCP owner table",
|
||||
)
|
||||
})?;
|
||||
if matching_process_ids.next().is_some() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"accepted connection has multiple IPv4 TCP owner rows",
|
||||
));
|
||||
}
|
||||
Ok(process_id)
|
||||
}
|
||||
|
||||
fn client_row_matches(
|
||||
row: &MIB_TCPROW_OWNER_PID,
|
||||
accepted_local_addr: SocketAddrV4,
|
||||
accepted_peer_addr: SocketAddrV4,
|
||||
) -> bool {
|
||||
ipv4_addr_matches(row.dwLocalAddr, *accepted_peer_addr.ip())
|
||||
&& tcp_port(row.dwLocalPort) == accepted_peer_addr.port()
|
||||
&& ipv4_addr_matches(row.dwRemoteAddr, *accepted_local_addr.ip())
|
||||
&& tcp_port(row.dwRemotePort) == accepted_local_addr.port()
|
||||
}
|
||||
|
||||
fn ipv4_addr_matches(table_addr: u32, socket_addr: Ipv4Addr) -> bool {
|
||||
table_addr.to_ne_bytes() == socket_addr.octets()
|
||||
}
|
||||
|
||||
fn tcp_port(table_port: u32) -> u16 {
|
||||
u16::from_be(table_port as u16)
|
||||
}
|
||||
|
||||
fn restricting_sids_for_process(process_id: u32) -> io::Result<Vec<String>> {
|
||||
let process_handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id) };
|
||||
let process = owned_handle(process_handle, "open proxy client process")?;
|
||||
|
||||
let mut token_handle: HANDLE = 0;
|
||||
let opened = unsafe {
|
||||
OpenProcessToken(
|
||||
process.as_raw_handle() as HANDLE,
|
||||
TOKEN_QUERY,
|
||||
&mut token_handle,
|
||||
)
|
||||
};
|
||||
if opened == 0 {
|
||||
return Err(last_error("open proxy client process token"));
|
||||
}
|
||||
let token = owned_handle(token_handle, "open proxy client process token")?;
|
||||
|
||||
let mut byte_len = 0_u32;
|
||||
let queried = unsafe {
|
||||
GetTokenInformation(
|
||||
token.as_raw_handle() as HANDLE,
|
||||
TokenRestrictedSids,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut byte_len,
|
||||
)
|
||||
};
|
||||
if queried != 0 || unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER {
|
||||
return Err(last_error("query proxy client restricting SID buffer size"));
|
||||
}
|
||||
|
||||
let mut buffer = aligned_buffer(byte_len as usize)?;
|
||||
let queried = unsafe {
|
||||
GetTokenInformation(
|
||||
token.as_raw_handle() as HANDLE,
|
||||
TokenRestrictedSids,
|
||||
buffer.as_mut_ptr().cast::<c_void>(),
|
||||
byte_len,
|
||||
&mut byte_len,
|
||||
)
|
||||
};
|
||||
if queried == 0 {
|
||||
return Err(last_error("read proxy client restricting SIDs"));
|
||||
}
|
||||
|
||||
parse_token_groups(&buffer, byte_len as usize)?
|
||||
.iter()
|
||||
.map(|entry| sid_to_string(entry.Sid))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_token_groups(buffer: &[usize], byte_len: usize) -> io::Result<&[SID_AND_ATTRIBUTES]> {
|
||||
let groups_offset = offset_of!(TOKEN_GROUPS, Groups);
|
||||
if byte_len > size_of_val(buffer) || byte_len < groups_offset {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"invalid restricting SID buffer length",
|
||||
));
|
||||
}
|
||||
|
||||
let group_count = unsafe { std::ptr::read_unaligned(buffer.as_ptr().cast::<u32>()) } as usize;
|
||||
let groups_byte_len = group_count
|
||||
.checked_mul(size_of::<SID_AND_ATTRIBUTES>())
|
||||
.and_then(|len| groups_offset.checked_add(len))
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"restricting SID buffer length overflow",
|
||||
)
|
||||
})?;
|
||||
if groups_byte_len > byte_len {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"truncated restricting SID buffer",
|
||||
));
|
||||
}
|
||||
|
||||
let groups = unsafe {
|
||||
let groups_ptr = buffer
|
||||
.as_ptr()
|
||||
.cast::<u8>()
|
||||
.add(groups_offset)
|
||||
.cast::<SID_AND_ATTRIBUTES>();
|
||||
std::slice::from_raw_parts(groups_ptr, group_count)
|
||||
};
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
fn sid_to_string(sid: PSID) -> io::Result<String> {
|
||||
let mut string_sid = std::ptr::null_mut();
|
||||
if unsafe { ConvertSidToStringSidW(sid, &mut string_sid) } == 0 {
|
||||
return Err(last_error("convert proxy client restricting SID to string"));
|
||||
}
|
||||
|
||||
let value = unsafe {
|
||||
let mut len = 0;
|
||||
while *string_sid.add(len) != 0 {
|
||||
len += 1;
|
||||
}
|
||||
String::from_utf16_lossy(std::slice::from_raw_parts(string_sid, len))
|
||||
};
|
||||
unsafe {
|
||||
LocalFree(string_sid as HLOCAL);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn aligned_buffer(byte_len: usize) -> io::Result<Vec<usize>> {
|
||||
if byte_len == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Windows API returned an empty buffer length",
|
||||
));
|
||||
}
|
||||
Ok(vec![0; byte_len.div_ceil(size_of::<usize>())])
|
||||
}
|
||||
|
||||
fn owned_handle(handle: HANDLE, operation: &str) -> io::Result<OwnedHandle> {
|
||||
if handle == 0 {
|
||||
return Err(last_error(operation));
|
||||
}
|
||||
Ok(unsafe { OwnedHandle::from_raw_handle(handle as RawHandle) })
|
||||
}
|
||||
|
||||
fn win32_error(operation: &str, error_code: u32) -> io::Error {
|
||||
let error = io::Error::from_raw_os_error(error_code as i32);
|
||||
io::Error::new(error.kind(), format!("{operation}: {error}"))
|
||||
}
|
||||
|
||||
fn last_error(operation: &str) -> io::Error {
|
||||
let error = io::Error::last_os_error();
|
||||
io::Error::new(error.kind(), format!("{operation}: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "windows_tcp_attribution_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,112 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
|
||||
#[test]
|
||||
fn parses_owner_table_and_matches_reversed_client_tuple() -> io::Result<()> {
|
||||
let proxy_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3128);
|
||||
let client_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49152);
|
||||
let rows = [
|
||||
tcp_row(proxy_addr, client_addr, 100),
|
||||
tcp_row(client_addr, proxy_addr, 200),
|
||||
];
|
||||
let (buffer, byte_len) = owner_table_buffer(&rows);
|
||||
|
||||
let parsed = parse_tcp_owner_rows(&buffer, byte_len)?;
|
||||
|
||||
assert_eq!(
|
||||
unique_client_process_id(parsed, proxy_addr, client_addr)?,
|
||||
200
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_matching_owner_rows() {
|
||||
let proxy_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3128);
|
||||
let client_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49152);
|
||||
let rows = [
|
||||
tcp_row(client_addr, proxy_addr, 200),
|
||||
tcp_row(client_addr, proxy_addr, 201),
|
||||
];
|
||||
|
||||
let error = unique_client_process_id(&rows, proxy_addr, client_addr)
|
||||
.expect_err("duplicate connection rows should fail closed");
|
||||
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_truncated_owner_table() {
|
||||
let byte_len = offset_of!(MIB_TCPTABLE_OWNER_PID, table);
|
||||
let mut buffer = aligned_buffer(byte_len).expect("aligned table buffer");
|
||||
unsafe {
|
||||
std::ptr::write_unaligned(buffer.as_mut_ptr().cast::<u32>(), 1);
|
||||
}
|
||||
|
||||
let Err(error) = parse_tcp_owner_rows(&buffer, byte_len) else {
|
||||
panic!("truncated connection row should fail closed");
|
||||
};
|
||||
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_loopback_connection_to_current_process() -> io::Result<()> {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
let client = TcpStream::connect(listener.local_addr()?)?;
|
||||
let (accepted, _) = listener.accept()?;
|
||||
let local_addr = accepted.local_addr()?;
|
||||
let peer_addr = accepted.peer_addr()?;
|
||||
|
||||
let process_id = owning_process_id(socket_addr_v4(local_addr)?, socket_addr_v4(peer_addr)?)?;
|
||||
let restricting_sids = restricting_sids_for_tcp_connection(local_addr, peer_addr)?;
|
||||
|
||||
assert_eq!(process_id, std::process::id());
|
||||
assert!(restricting_sids.iter().all(|sid| sid.starts_with("S-")));
|
||||
drop(client);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn socket_addr_v4(addr: SocketAddr) -> io::Result<SocketAddrV4> {
|
||||
match addr {
|
||||
SocketAddr::V4(addr) => Ok(addr),
|
||||
SocketAddr::V6(_) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"test listener unexpectedly used IPv6",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn tcp_row(
|
||||
local_addr: SocketAddrV4,
|
||||
remote_addr: SocketAddrV4,
|
||||
process_id: u32,
|
||||
) -> MIB_TCPROW_OWNER_PID {
|
||||
MIB_TCPROW_OWNER_PID {
|
||||
dwState: 0,
|
||||
dwLocalAddr: u32::from_ne_bytes(local_addr.ip().octets()),
|
||||
dwLocalPort: local_addr.port().to_be() as u32,
|
||||
dwRemoteAddr: u32::from_ne_bytes(remote_addr.ip().octets()),
|
||||
dwRemotePort: remote_addr.port().to_be() as u32,
|
||||
dwOwningPid: process_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn owner_table_buffer(rows: &[MIB_TCPROW_OWNER_PID]) -> (Vec<usize>, usize) {
|
||||
let rows_offset = offset_of!(MIB_TCPTABLE_OWNER_PID, table);
|
||||
let rows_byte_len = size_of_val(rows);
|
||||
let byte_len = rows_offset + rows_byte_len;
|
||||
let mut buffer = aligned_buffer(byte_len).expect("aligned table buffer");
|
||||
unsafe {
|
||||
let buffer_ptr = buffer.as_mut_ptr().cast::<u8>();
|
||||
std::ptr::write_unaligned(buffer_ptr.cast::<u32>(), rows.len() as u32);
|
||||
std::ptr::copy_nonoverlapping(
|
||||
rows.as_ptr().cast::<u8>(),
|
||||
buffer_ptr.add(rows_offset),
|
||||
rows_byte_len,
|
||||
);
|
||||
}
|
||||
(buffer, byte_len)
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigReloaderFuture;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::NetworkDecision;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkPolicyDecider;
|
||||
use codex_network_proxy::NetworkPolicyRequest;
|
||||
use codex_network_proxy::NetworkProtocol;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use codex_windows_sandbox::ConsoleMode;
|
||||
use codex_windows_sandbox::LocalSid;
|
||||
use codex_windows_sandbox::create_process_as_user;
|
||||
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
|
||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::os::windows::io::OwnedHandle;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::TerminateProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
|
||||
const CHILD_MODE_ENV: &str = "CODEX_WINDOWS_PROXY_TEST_CHILD";
|
||||
const HTTP_ADDR_ENV: &str = "CODEX_WINDOWS_PROXY_TEST_HTTP_ADDR";
|
||||
const SOCKS_ADDR_ENV: &str = "CODEX_WINDOWS_PROXY_TEST_SOCKS_ADDR";
|
||||
const ORIGIN_PORT_ENV: &str = "CODEX_WINDOWS_PROXY_TEST_ORIGIN_PORT";
|
||||
const ALLOWED_HOST_ENV: &str = "CODEX_WINDOWS_PROXY_TEST_ALLOWED_HOST";
|
||||
const DENIED_HOST_ENV: &str = "CODEX_WINDOWS_PROXY_TEST_DENIED_HOST";
|
||||
const FIRST_ENVIRONMENT_ID: &str = "first-environment";
|
||||
const SECOND_ENVIRONMENT_ID: &str = "second-environment";
|
||||
const DECIDER_DENIED_HOST: &str = "not-allowed.invalid";
|
||||
const CHILD_TIMEOUT_MS: u32 = 30_000;
|
||||
const WAIT_OBJECT_0: u32 = 0;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StaticReloader(ConfigState);
|
||||
|
||||
impl ConfigReloader for StaticReloader {
|
||||
fn source_label(&self) -> String {
|
||||
"test config".to_string()
|
||||
}
|
||||
|
||||
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
|
||||
Box::pin(async { Ok(None) })
|
||||
}
|
||||
|
||||
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
|
||||
let state = self.0.clone();
|
||||
Box::pin(async move { Ok(state) })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn restricted_tokens_select_stable_routes_and_cleanup() -> anyhow::Result<()> {
|
||||
let (origin_port, origin_task) = start_http_origin().await?;
|
||||
let (first_decider, first_requests) = recording_decider();
|
||||
let (second_decider, second_requests) = recording_decider();
|
||||
let first_requested = requested_addrs()?;
|
||||
let first = build_proxy(
|
||||
first_requested,
|
||||
"localhost",
|
||||
/*enable_socks5*/ false,
|
||||
Some(first_decider),
|
||||
)
|
||||
.await?;
|
||||
let initial_addrs = (first.http_addr(), first.socks_addr());
|
||||
let first_handle = first.run().await?;
|
||||
let first_sid = first
|
||||
.network_proxy_restricting_sid(None)
|
||||
.expect("running proxy should have a route SID");
|
||||
first.prepare_for_optional_environment(HashMap::new(), Some(FIRST_ENVIRONMENT_ID))?;
|
||||
let first_environment_sid = first
|
||||
.network_proxy_restricting_sid(Some(FIRST_ENVIRONMENT_ID))
|
||||
.expect("first environment should have a route SID");
|
||||
|
||||
let second_requested = requested_addrs()?;
|
||||
assert_ne!(second_requested.0, initial_addrs.0);
|
||||
let second = build_proxy(
|
||||
second_requested,
|
||||
"127.0.0.1",
|
||||
/*enable_socks5*/ true,
|
||||
Some(second_decider),
|
||||
)
|
||||
.await?;
|
||||
let stable_addrs = (second.http_addr(), second.socks_addr());
|
||||
assert_eq!(stable_addrs.0, initial_addrs.0);
|
||||
assert_eq!(stable_addrs.1, second_requested.1);
|
||||
assert_eq!((first.http_addr(), first.socks_addr()), stable_addrs);
|
||||
let second_handle = second.run().await?;
|
||||
let second_sid = second
|
||||
.network_proxy_restricting_sid(None)
|
||||
.expect("running proxy should have a route SID");
|
||||
assert_ne!(second_sid, first_sid);
|
||||
|
||||
second.prepare_for_optional_environment(HashMap::new(), Some(SECOND_ENVIRONMENT_ID))?;
|
||||
let second_environment_sid = second
|
||||
.network_proxy_restricting_sid(Some(SECOND_ENVIRONMENT_ID))
|
||||
.expect("second environment should have a route SID");
|
||||
|
||||
run_restricted_child(
|
||||
&first_environment_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
Some(("localhost", DECIDER_DENIED_HOST)),
|
||||
/*expect_socks*/ false,
|
||||
)
|
||||
.await?;
|
||||
assert_recorded_requests(
|
||||
&first_requests,
|
||||
FIRST_ENVIRONMENT_ID,
|
||||
DECIDER_DENIED_HOST,
|
||||
origin_port,
|
||||
&[NetworkProtocol::Http],
|
||||
);
|
||||
assert!(
|
||||
second_requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
run_restricted_child(
|
||||
&second_environment_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
Some(("127.0.0.1", DECIDER_DENIED_HOST)),
|
||||
/*expect_socks*/ true,
|
||||
)
|
||||
.await?;
|
||||
assert_recorded_requests(
|
||||
&first_requests,
|
||||
FIRST_ENVIRONMENT_ID,
|
||||
DECIDER_DENIED_HOST,
|
||||
origin_port,
|
||||
&[NetworkProtocol::Http],
|
||||
);
|
||||
assert_recorded_requests(
|
||||
&second_requests,
|
||||
SECOND_ENVIRONMENT_ID,
|
||||
DECIDER_DENIED_HOST,
|
||||
origin_port,
|
||||
&[NetworkProtocol::Http, NetworkProtocol::Socks5Tcp],
|
||||
);
|
||||
|
||||
run_restricted_child(
|
||||
&first_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
Some(("localhost", "127.0.0.1")),
|
||||
/*expect_socks*/ false,
|
||||
)
|
||||
.await?;
|
||||
run_restricted_child(
|
||||
&second_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
Some(("127.0.0.1", "localhost")),
|
||||
/*expect_socks*/ true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
first_handle.shutdown().await?;
|
||||
run_restricted_child(
|
||||
&first_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
None,
|
||||
/*expect_socks*/ false,
|
||||
)
|
||||
.await?;
|
||||
run_restricted_child(
|
||||
&first_environment_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
None,
|
||||
/*expect_socks*/ false,
|
||||
)
|
||||
.await?;
|
||||
run_restricted_child(
|
||||
&second_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
Some(("127.0.0.1", "localhost")),
|
||||
/*expect_socks*/ true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
second_handle.shutdown().await?;
|
||||
drop((first, second));
|
||||
|
||||
let third_requested = requested_addrs()?;
|
||||
assert_ne!(third_requested, stable_addrs);
|
||||
let third = build_proxy(
|
||||
third_requested,
|
||||
"localhost",
|
||||
/*enable_socks5*/ false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!((third.http_addr(), third.socks_addr()), stable_addrs);
|
||||
let third_handle = third.run().await?;
|
||||
let third_sid = third
|
||||
.network_proxy_restricting_sid(None)
|
||||
.expect("running proxy should have a route SID");
|
||||
assert_ne!(third_sid, first_sid);
|
||||
assert_ne!(third_sid, second_sid);
|
||||
|
||||
run_restricted_child(
|
||||
&third_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
Some(("localhost", "127.0.0.1")),
|
||||
/*expect_socks*/ false,
|
||||
)
|
||||
.await?;
|
||||
drop(third_handle);
|
||||
assert_eq!(third.network_proxy_restricting_sid(None), None);
|
||||
run_restricted_child(
|
||||
&third_sid,
|
||||
stable_addrs,
|
||||
origin_port,
|
||||
None,
|
||||
/*expect_socks*/ false,
|
||||
)
|
||||
.await?;
|
||||
origin_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_child_exercises_http_and_socks() -> anyhow::Result<()> {
|
||||
let Ok(mode) = std::env::var(CHILD_MODE_ENV) else {
|
||||
return Ok(());
|
||||
};
|
||||
let http_addr = required_env(HTTP_ADDR_ENV)?.parse::<SocketAddr>()?;
|
||||
let socks_addr = required_env(SOCKS_ADDR_ENV)?.parse::<SocketAddr>()?;
|
||||
let origin_port = required_env(ORIGIN_PORT_ENV)?.parse::<u16>()?;
|
||||
|
||||
if mode == "missing-route" {
|
||||
let authority = format!("localhost:{origin_port}");
|
||||
assert!(http_status(http_addr, &authority).is_err());
|
||||
assert!(socks_status(socks_addr, "localhost", origin_port).is_err());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let allowed_host = required_env(ALLOWED_HOST_ENV)?;
|
||||
let denied_host = required_env(DENIED_HOST_ENV)?;
|
||||
let allowed_authority = format!("{allowed_host}:{origin_port}");
|
||||
let denied_authority = format!("{denied_host}:{origin_port}");
|
||||
assert_eq!(http_status(http_addr, &allowed_authority)?, 200);
|
||||
assert_eq!(http_status(http_addr, &denied_authority)?, 403);
|
||||
if mode == "http-only" {
|
||||
assert!(socks_status(socks_addr, &allowed_host, origin_port).is_err());
|
||||
return Ok(());
|
||||
}
|
||||
assert_eq!(
|
||||
socks_status(socks_addr, &allowed_host, origin_port)?,
|
||||
SocksOutcome::Connected
|
||||
);
|
||||
assert!(matches!(
|
||||
socks_status(socks_addr, &denied_host, origin_port)?,
|
||||
SocksOutcome::Denied(_)
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_proxy(
|
||||
requested_addrs: (SocketAddr, SocketAddr),
|
||||
allowed_domain: &str,
|
||||
enable_socks5: bool,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
) -> anyhow::Result<NetworkProxy> {
|
||||
let (http_addr, socks_addr) = requested_addrs;
|
||||
let mut config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
proxy_url: format!("http://{http_addr}"),
|
||||
socks_url: format!("socks5://{socks_addr}"),
|
||||
enable_socks5,
|
||||
enable_socks5_udp: false,
|
||||
allow_local_binding: true,
|
||||
mode: NetworkMode::Full,
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
config.set_allowed_domains(vec![allowed_domain.to_string()]);
|
||||
let config_state = build_config_state(config, Default::default())?;
|
||||
let reloader = Arc::new(StaticReloader(config_state.clone()));
|
||||
let state = Arc::new(NetworkProxyState::with_reloader(config_state, reloader));
|
||||
let mut builder = NetworkProxy::builder().state(state);
|
||||
if let Some(policy_decider) = policy_decider {
|
||||
builder = builder.policy_decider_arc(policy_decider);
|
||||
}
|
||||
builder.build().await
|
||||
}
|
||||
|
||||
fn recording_decider() -> (
|
||||
Arc<dyn NetworkPolicyDecider>,
|
||||
Arc<Mutex<Vec<NetworkPolicyRequest>>>,
|
||||
) {
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let recorded_requests = Arc::clone(&requests);
|
||||
let decider: Arc<dyn NetworkPolicyDecider> = Arc::new(move |request: NetworkPolicyRequest| {
|
||||
recorded_requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(request);
|
||||
async { NetworkDecision::deny("integration test denial") }
|
||||
});
|
||||
(decider, requests)
|
||||
}
|
||||
|
||||
fn assert_recorded_requests(
|
||||
requests: &Arc<Mutex<Vec<NetworkPolicyRequest>>>,
|
||||
environment_id: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
expected_protocols: &[NetworkProtocol],
|
||||
) {
|
||||
let requests = requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(requests.len(), expected_protocols.len());
|
||||
let actual_protocols = requests
|
||||
.iter()
|
||||
.map(|request| request.protocol)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual_protocols, expected_protocols);
|
||||
assert!(requests.iter().all(|request| {
|
||||
request.environment_id.as_deref() == Some(environment_id)
|
||||
&& request.host == host
|
||||
&& request.port == port
|
||||
}));
|
||||
}
|
||||
|
||||
fn requested_addrs() -> std::io::Result<(SocketAddr, SocketAddr)> {
|
||||
let http = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
let socks = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
Ok((http.local_addr()?, socks.local_addr()?))
|
||||
}
|
||||
|
||||
async fn start_http_origin() -> std::io::Result<(u16, tokio::task::JoinHandle<()>)> {
|
||||
let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let task = tokio::spawn(async move {
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
tokio::spawn(async move {
|
||||
let mut request = [0_u8; 1024];
|
||||
let _ = stream.read(&mut request).await;
|
||||
let _ = stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK",
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
Ok((port, task))
|
||||
}
|
||||
|
||||
async fn run_restricted_child(
|
||||
route_sid: &str,
|
||||
proxy_addrs: (SocketAddr, SocketAddr),
|
||||
origin_port: u16,
|
||||
policy: Option<(&str, &str)>,
|
||||
expect_socks: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let route_sid = route_sid.to_string();
|
||||
let policy = policy.map(|(allowed, denied)| (allowed.to_string(), denied.to_string()));
|
||||
tokio::task::spawn_blocking(move || {
|
||||
run_restricted_child_blocking(&route_sid, proxy_addrs, origin_port, policy, expect_socks)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_restricted_child_blocking(
|
||||
route_sid: &str,
|
||||
(http_addr, socks_addr): (SocketAddr, SocketAddr),
|
||||
origin_port: u16,
|
||||
policy: Option<(String, String)>,
|
||||
expect_socks: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let route_sid = LocalSid::from_string(route_sid)?;
|
||||
let capability_sid = LocalSid::from_string("S-1-5-21-10-20-30-40")?;
|
||||
let base_token = unsafe {
|
||||
OwnedHandle::from_raw_handle(get_current_token_for_restriction()? as *mut std::ffi::c_void)
|
||||
};
|
||||
let restricted_token = unsafe {
|
||||
create_readonly_token_with_caps_and_user_from(
|
||||
base_token.as_raw_handle() as isize,
|
||||
&[capability_sid.as_ptr()],
|
||||
&[route_sid.as_ptr()],
|
||||
)?
|
||||
};
|
||||
let restricted_token =
|
||||
unsafe { OwnedHandle::from_raw_handle(restricted_token as *mut std::ffi::c_void) };
|
||||
|
||||
let mut env = std::env::vars().collect::<HashMap<_, _>>();
|
||||
env.insert(HTTP_ADDR_ENV.to_string(), http_addr.to_string());
|
||||
env.insert(SOCKS_ADDR_ENV.to_string(), socks_addr.to_string());
|
||||
env.insert(ORIGIN_PORT_ENV.to_string(), origin_port.to_string());
|
||||
match policy {
|
||||
Some((allowed, denied)) => {
|
||||
let mode = if expect_socks { "policy" } else { "http-only" };
|
||||
env.insert(CHILD_MODE_ENV.to_string(), mode.to_string());
|
||||
env.insert(ALLOWED_HOST_ENV.to_string(), allowed);
|
||||
env.insert(DENIED_HOST_ENV.to_string(), denied);
|
||||
}
|
||||
None => {
|
||||
env.insert(CHILD_MODE_ENV.to_string(), "missing-route".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let test_exe = std::env::current_exe()?;
|
||||
let command = vec![
|
||||
test_exe.to_string_lossy().into_owned(),
|
||||
"--exact".to_string(),
|
||||
"restricted_child_exercises_http_and_socks".to_string(),
|
||||
"--nocapture".to_string(),
|
||||
"--test-threads=1".to_string(),
|
||||
];
|
||||
let cwd = std::env::current_dir()?;
|
||||
let spawned = unsafe {
|
||||
create_process_as_user(
|
||||
restricted_token.as_raw_handle() as isize,
|
||||
&command,
|
||||
&cwd,
|
||||
&env,
|
||||
/*logs_base_dir*/ None,
|
||||
/*stdio*/ None,
|
||||
/*console_mode*/ ConsoleMode::Inherit,
|
||||
/*use_private_desktop*/ false,
|
||||
)?
|
||||
};
|
||||
let process = unsafe {
|
||||
OwnedHandle::from_raw_handle(spawned.process_info.hProcess as *mut std::ffi::c_void)
|
||||
};
|
||||
let _thread = unsafe {
|
||||
OwnedHandle::from_raw_handle(spawned.process_info.hThread as *mut std::ffi::c_void)
|
||||
};
|
||||
|
||||
let wait = unsafe {
|
||||
WaitForSingleObject(
|
||||
process.as_raw_handle() as isize,
|
||||
/*dwMilliseconds*/ CHILD_TIMEOUT_MS,
|
||||
)
|
||||
};
|
||||
if wait != WAIT_OBJECT_0 {
|
||||
unsafe {
|
||||
TerminateProcess(process.as_raw_handle() as isize, 1);
|
||||
}
|
||||
}
|
||||
let mut exit_code = 1_u32;
|
||||
unsafe {
|
||||
GetExitCodeProcess(process.as_raw_handle() as isize, &mut exit_code);
|
||||
}
|
||||
anyhow::ensure!(
|
||||
wait == WAIT_OBJECT_0 && exit_code == 0,
|
||||
"restricted proxy child failed (wait={wait}, exit={exit_code})"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn required_env(key: &str) -> anyhow::Result<String> {
|
||||
std::env::var(key).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn http_status(proxy_addr: SocketAddr, authority: &str) -> std::io::Result<u16> {
|
||||
let mut stream = TcpStream::connect(proxy_addr)?;
|
||||
configure_stream(&stream)?;
|
||||
write!(
|
||||
stream,
|
||||
"GET http://{authority}/ HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n\r\n"
|
||||
)?;
|
||||
read_http_status(&mut stream)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum SocksOutcome {
|
||||
Connected,
|
||||
Denied(u8),
|
||||
}
|
||||
|
||||
fn socks_status(
|
||||
proxy_addr: SocketAddr,
|
||||
host: &str,
|
||||
origin_port: u16,
|
||||
) -> std::io::Result<SocksOutcome> {
|
||||
let mut stream = TcpStream::connect(proxy_addr)?;
|
||||
configure_stream(&stream)?;
|
||||
stream.write_all(&[5, 1, 0])?;
|
||||
let mut greeting = [0_u8; 2];
|
||||
stream.read_exact(&mut greeting)?;
|
||||
if greeting != [5, 0] {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"SOCKS5 proxy rejected no-authentication method",
|
||||
));
|
||||
}
|
||||
|
||||
let mut request = vec![5, 1, 0];
|
||||
if let Ok(ip) = host.parse::<Ipv4Addr>() {
|
||||
request.push(1);
|
||||
request.extend_from_slice(&ip.octets());
|
||||
} else {
|
||||
let host_len = u8::try_from(host.len()).map_err(|_| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidInput, "SOCKS5 hostname too long")
|
||||
})?;
|
||||
request.extend_from_slice(&[3, host_len]);
|
||||
request.extend_from_slice(host.as_bytes());
|
||||
}
|
||||
request.extend_from_slice(&origin_port.to_be_bytes());
|
||||
stream.write_all(&request)?;
|
||||
|
||||
let mut reply = [0_u8; 4];
|
||||
stream.read_exact(&mut reply)?;
|
||||
if reply[0] != 5 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"invalid SOCKS5 response version",
|
||||
));
|
||||
}
|
||||
if reply[1] != 0 {
|
||||
return Ok(SocksOutcome::Denied(reply[1]));
|
||||
}
|
||||
consume_socks_bound_address(&mut stream, reply[3])?;
|
||||
Ok(SocksOutcome::Connected)
|
||||
}
|
||||
|
||||
fn consume_socks_bound_address(stream: &mut TcpStream, address_type: u8) -> std::io::Result<()> {
|
||||
let address_len = match address_type {
|
||||
1 => 4,
|
||||
3 => {
|
||||
let mut len = [0_u8; 1];
|
||||
stream.read_exact(&mut len)?;
|
||||
usize::from(len[0])
|
||||
}
|
||||
4 => 16,
|
||||
_ => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"invalid SOCKS5 bound address type",
|
||||
));
|
||||
}
|
||||
};
|
||||
let mut address_and_port = vec![0_u8; address_len + 2];
|
||||
stream.read_exact(&mut address_and_port)
|
||||
}
|
||||
|
||||
fn configure_stream(stream: &TcpStream) -> std::io::Result<()> {
|
||||
let timeout = Some(Duration::from_secs(5));
|
||||
stream.set_read_timeout(timeout)?;
|
||||
stream.set_write_timeout(timeout)
|
||||
}
|
||||
|
||||
fn read_http_status(stream: &mut TcpStream) -> std::io::Result<u16> {
|
||||
let mut status_line = String::new();
|
||||
if BufReader::new(stream).read_line(&mut status_line)? == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"proxy closed before an HTTP status line",
|
||||
));
|
||||
}
|
||||
status_line
|
||||
.split_ascii_whitespace()
|
||||
.nth(1)
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "missing HTTP status code")
|
||||
})?
|
||||
.parse::<u16>()
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
|
||||
}
|
||||
@@ -532,6 +532,19 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
|
||||
let inner_command = std::mem::take(&mut request.command);
|
||||
let proxy_enforced = request.network.is_some();
|
||||
let network_proxy_restricting_sid = request
|
||||
.network
|
||||
.as_ref()
|
||||
.map(|network| {
|
||||
network
|
||||
.network_proxy_restricting_sid(request.network_environment_id.as_deref())
|
||||
.ok_or_else(|| {
|
||||
SandboxTransformError::WindowsSandboxPreparation(
|
||||
"managed Windows proxy route is missing its restricting SID".to_string(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let use_elevated =
|
||||
windows_sandbox_uses_elevated_backend(request.windows_sandbox_level, proxy_enforced);
|
||||
let overrides = if use_elevated {
|
||||
@@ -576,6 +589,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
request.windows_sandbox_level,
|
||||
request.windows_sandbox_private_desktop,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid.as_deref(),
|
||||
proxy_settings_mode,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct WindowsSandboxSpawnRequest<'a> {
|
||||
pub workspace_roots: &'a [AbsolutePathBuf],
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub proxy_enforced: bool,
|
||||
pub network_proxy_restricting_sid: Option<&'a str>,
|
||||
pub proxy_settings_mode: WindowsSandboxProxySettingsMode,
|
||||
pub filesystem_overrides: Option<&'a WindowsSandboxFilesystemOverrides>,
|
||||
pub use_private_desktop: bool,
|
||||
@@ -60,6 +61,9 @@ pub async fn spawn_process(request: SpawnRequest<'_>) -> Result<SpawnedProcess>
|
||||
env_map: request.env.clone(),
|
||||
windows_sandbox_level: windows.windows_sandbox_level,
|
||||
proxy_enforced: windows.proxy_enforced,
|
||||
network_proxy_restricting_sid: windows
|
||||
.network_proxy_restricting_sid
|
||||
.map(str::to_owned),
|
||||
proxy_settings_mode: windows.proxy_settings_mode,
|
||||
timeout_ms: None,
|
||||
read_roots_override: overrides
|
||||
|
||||
@@ -273,19 +273,37 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
|
||||
if cap_psids.is_empty() {
|
||||
anyhow::bail!("runner: empty capability SID list");
|
||||
}
|
||||
let network_proxy_restricting_sid = req
|
||||
.network_proxy_restricting_sid
|
||||
.as_deref()
|
||||
.map(LocalSid::from_string)
|
||||
.transpose()
|
||||
.context("ConvertStringSidToSidW failed for network proxy restricting SID")?;
|
||||
|
||||
// The token helpers still take raw SID pointers, but we keep ownership in `LocalSid`
|
||||
// wrappers for as long as possible. That way any failure after SID parsing but before the
|
||||
// child is fully spawned still releases the backing LocalAlloc memory automatically.
|
||||
let cap_psid_ptrs: Vec<*mut _> = cap_psids.iter().map(LocalSid::as_ptr).collect();
|
||||
let additional_restricting_sid_ptrs: Vec<*mut _> = network_proxy_restricting_sid
|
||||
.iter()
|
||||
.map(LocalSid::as_ptr)
|
||||
.collect();
|
||||
let base = OwnedWinHandle::new(unsafe { get_current_token_for_restriction()? });
|
||||
let h_token = OwnedWinHandle::new(unsafe {
|
||||
match token_mode {
|
||||
WindowsSandboxTokenMode::ReadOnlyCapability => {
|
||||
create_readonly_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
||||
create_readonly_token_with_caps_and_user_from(
|
||||
base.raw(),
|
||||
&cap_psid_ptrs,
|
||||
&additional_restricting_sid_ptrs,
|
||||
)
|
||||
}
|
||||
WindowsSandboxTokenMode::WritableRootsCapability => {
|
||||
create_workspace_write_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
||||
create_workspace_write_token_with_caps_and_user_from(
|
||||
base.raw(),
|
||||
&cap_psid_ptrs,
|
||||
&additional_restricting_sid_ptrs,
|
||||
)
|
||||
}
|
||||
}
|
||||
}?);
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::path::PathBuf;
|
||||
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// Protocol version shared by the parent process and elevated command runner.
|
||||
pub const IPC_PROTOCOL_VERSION: u8 = 4;
|
||||
pub const IPC_PROTOCOL_VERSION: u8 = 5;
|
||||
|
||||
/// Length-prefixed, JSON-encoded frame.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -65,6 +65,9 @@ pub struct SpawnRequest {
|
||||
pub codex_home: PathBuf,
|
||||
pub real_codex_home: PathBuf,
|
||||
pub cap_sids: Vec<String>,
|
||||
/// Optional managed-network identity added only to the child's restricting SID set.
|
||||
#[serde(default)]
|
||||
pub network_proxy_restricting_sid: Option<String>,
|
||||
pub timeout_ms: Option<u64>,
|
||||
pub tty: bool,
|
||||
#[serde(default)]
|
||||
@@ -224,6 +227,7 @@ mod tests {
|
||||
codex_home: PathBuf::from(r"C:\codex"),
|
||||
real_codex_home: PathBuf::from(r"C:\Users\codex"),
|
||||
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
|
||||
network_proxy_restricting_sid: Some("S-1-5-21-100-200-300-400".to_string()),
|
||||
timeout_ms: Some(1000),
|
||||
tty: false,
|
||||
stdin_open: false,
|
||||
@@ -245,6 +249,10 @@ mod tests {
|
||||
};
|
||||
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
|
||||
assert_eq!(workspace_roots, payload.workspace_roots);
|
||||
assert_eq!(
|
||||
Some("S-1-5-21-100-200-300-400"),
|
||||
payload.network_proxy_restricting_sid.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct ElevatedSandboxProfileCaptureRequest<'a> {
|
||||
pub cancellation: Option<crate::WindowsSandboxCancellationToken>,
|
||||
pub use_private_desktop: bool,
|
||||
pub proxy_enforced: bool,
|
||||
pub network_proxy_restricting_sid: Option<String>,
|
||||
pub read_roots_override: Option<&'a [PathBuf]>,
|
||||
pub read_roots_include_platform_defaults: bool,
|
||||
pub write_roots_override: Option<&'a [PathBuf]>,
|
||||
@@ -110,6 +111,7 @@ mod windows_impl {
|
||||
cancellation,
|
||||
use_private_desktop,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
write_roots_override,
|
||||
@@ -190,6 +192,7 @@ mod windows_impl {
|
||||
codex_home: sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids,
|
||||
network_proxy_restricting_sid,
|
||||
timeout_ms,
|
||||
tty: false,
|
||||
stdin_open: false,
|
||||
|
||||
@@ -44,6 +44,15 @@ pub enum WindowsSandboxProxySettingsMode {
|
||||
Preserve,
|
||||
}
|
||||
|
||||
/// Network settings installed by an administrator during managed Windows sandbox setup.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct WindowsSandboxProvisioningSettings {
|
||||
/// Loopback proxy ports permitted for the offline sandbox identity.
|
||||
pub proxy_ports: Vec<u16>,
|
||||
/// Whether the offline sandbox identity may exchange arbitrary loopback traffic.
|
||||
pub allow_local_binding: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod acl;
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -705,6 +705,10 @@ const PROXY_ENV_KEYS: &[&str] = &[
|
||||
"wss_proxy",
|
||||
];
|
||||
const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING";
|
||||
// Internal wire format shared with network-proxy/src/proxy.rs. The value is a comma-separated,
|
||||
// sorted list of non-zero loopback proxy ports used only when computing the Windows offline
|
||||
// sandbox setup marker.
|
||||
const WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY: &str = "CODEX_WINDOWS_SANDBOX_PROXY_PORTS";
|
||||
|
||||
pub(crate) fn offline_proxy_settings_from_env(
|
||||
env_map: &HashMap<String, String>,
|
||||
@@ -744,6 +748,14 @@ pub(crate) fn proxy_ports_from_env(env_map: &HashMap<String, String>) -> Vec<u16
|
||||
ports.insert(port);
|
||||
}
|
||||
}
|
||||
if let Some(value) = env_map.get(WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY) {
|
||||
ports.extend(
|
||||
value
|
||||
.split(',')
|
||||
.filter_map(|port| port.trim().parse::<u16>().ok())
|
||||
.filter(|port| *port != 0),
|
||||
);
|
||||
}
|
||||
ports.into_iter().collect()
|
||||
}
|
||||
|
||||
@@ -1034,7 +1046,11 @@ fn run_elevated_setup_inner(
|
||||
run_setup_exe(&payload, needs_elevation, request.codex_home)
|
||||
}
|
||||
|
||||
pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> Result<()> {
|
||||
pub fn run_elevated_provisioning_setup(
|
||||
codex_home: &Path,
|
||||
real_user: &str,
|
||||
settings: crate::WindowsSandboxProvisioningSettings,
|
||||
) -> Result<()> {
|
||||
let sbx_dir = sandbox_dir(codex_home);
|
||||
std::fs::create_dir_all(&sbx_dir).map_err(|err| {
|
||||
failure(
|
||||
@@ -1063,8 +1079,8 @@ pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> Re
|
||||
write_roots: Vec::new(),
|
||||
deny_read_paths: Vec::new(),
|
||||
deny_write_paths: Vec::new(),
|
||||
proxy_ports: Vec::new(),
|
||||
allow_local_binding: false,
|
||||
proxy_ports: settings.proxy_ports,
|
||||
allow_local_binding: settings.allow_local_binding,
|
||||
otel: codex_otel::global_statsig_metrics_settings(),
|
||||
real_user: real_user.to_string(),
|
||||
mode: SetupMode::ProvisionOnly,
|
||||
@@ -1264,6 +1280,7 @@ fn filter_sensitive_write_roots(mut roots: Vec<PathBuf>, codex_home: &Path) -> V
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::WINDOWS_PLATFORM_DEFAULT_READ_ROOTS;
|
||||
use super::WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY;
|
||||
use super::build_payload_roots;
|
||||
use super::find_setup_exe_for_current_exe;
|
||||
use super::gather_full_read_roots_for_permissions;
|
||||
@@ -1599,8 +1616,12 @@ mod tests {
|
||||
"HTTPS_PROXY".to_string(),
|
||||
"https://example.com:9999".to_string(),
|
||||
);
|
||||
env.insert(
|
||||
WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY.to_string(),
|
||||
"8080,43129,0,invalid, 43128,65536".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(proxy_ports_from_env(&env), vec![1081, 8080]);
|
||||
assert_eq!(proxy_ports_from_env(&env), vec![1081, 8080, 43128, 43129]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -381,7 +381,8 @@ pub unsafe fn create_workspace_write_token_with_caps_from(
|
||||
create_token_with_caps_from(base_token, psid_capabilities, &[])
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs plus the token user SID.
|
||||
/// Create a restricted token that includes all provided capability SIDs, the token user SID, and
|
||||
/// any additional restricting SIDs.
|
||||
///
|
||||
/// This is intended for the elevated sandbox backend, where the token user is the dedicated
|
||||
/// sandbox account rather than the real signed-in user.
|
||||
@@ -391,10 +392,13 @@ pub unsafe fn create_workspace_write_token_with_caps_from(
|
||||
pub unsafe fn create_workspace_write_token_with_caps_and_user_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
additional_restricting_sids: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
let mut user_sid_bytes = get_user_sid_bytes(base_token)?;
|
||||
let psid_user = user_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
create_token_with_caps_from(base_token, psid_capabilities, &[psid_user])
|
||||
create_token_with_caps_user_and_additional_restrictions_from(
|
||||
base_token,
|
||||
psid_capabilities,
|
||||
additional_restricting_sids,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs.
|
||||
@@ -408,7 +412,8 @@ pub unsafe fn create_readonly_token_with_caps_from(
|
||||
create_token_with_caps_from(base_token, psid_capabilities, &[])
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs plus the token user SID.
|
||||
/// Create a restricted token that includes all provided capability SIDs, the token user SID, and
|
||||
/// any additional restricting SIDs.
|
||||
///
|
||||
/// This is intended for the elevated sandbox backend, where the token user is the dedicated
|
||||
/// sandbox account rather than the real signed-in user.
|
||||
@@ -418,10 +423,26 @@ pub unsafe fn create_readonly_token_with_caps_from(
|
||||
pub unsafe fn create_readonly_token_with_caps_and_user_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
additional_restricting_sids: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
create_token_with_caps_user_and_additional_restrictions_from(
|
||||
base_token,
|
||||
psid_capabilities,
|
||||
additional_restricting_sids,
|
||||
)
|
||||
}
|
||||
|
||||
unsafe fn create_token_with_caps_user_and_additional_restrictions_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
additional_restricting_sids: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
let mut user_sid_bytes = get_user_sid_bytes(base_token)?;
|
||||
let psid_user = user_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
create_token_with_caps_from(base_token, psid_capabilities, &[psid_user])
|
||||
let mut extra_restricting_sids = Vec::with_capacity(additional_restricting_sids.len() + 1);
|
||||
extra_restricting_sids.push(psid_user);
|
||||
extra_restricting_sids.extend_from_slice(additional_restricting_sids);
|
||||
create_token_with_caps_from(base_token, psid_capabilities, &extra_restricting_sids)
|
||||
}
|
||||
|
||||
unsafe fn create_token_with_caps_from(
|
||||
@@ -472,6 +493,8 @@ unsafe fn create_token_with_caps_from(
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
|
||||
// Additional restricting SIDs are identity markers, not capabilities. Deliberately exclude
|
||||
// them from the default DACL so possessing a route identity cannot grant object access.
|
||||
let mut dacl_sids: Vec<*mut c_void> = Vec::with_capacity(psid_capabilities.len() + 2);
|
||||
dacl_sids.push(psid_logon);
|
||||
dacl_sids.push(psid_everyone);
|
||||
@@ -481,3 +504,7 @@ unsafe fn create_token_with_caps_from(
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok(new_token)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "token_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use super::*;
|
||||
use windows_sys::Win32::Security::EqualSid;
|
||||
use windows_sys::Win32::Security::TokenRestrictedSids;
|
||||
|
||||
unsafe fn token_has_restricting_sid(token: HANDLE, expected_sid: *mut c_void) -> Result<bool> {
|
||||
let mut needed = 0;
|
||||
GetTokenInformation(
|
||||
token,
|
||||
TokenRestrictedSids,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut needed,
|
||||
);
|
||||
if needed == 0 {
|
||||
return Err(anyhow!(
|
||||
"GetTokenInformation(TokenRestrictedSids) size query failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
|
||||
let mut buffer = vec![0_u8; needed as usize];
|
||||
if GetTokenInformation(
|
||||
token,
|
||||
TokenRestrictedSids,
|
||||
buffer.as_mut_ptr().cast(),
|
||||
needed,
|
||||
&mut needed,
|
||||
) == 0
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"GetTokenInformation(TokenRestrictedSids) failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
|
||||
let group_count = std::ptr::read_unaligned(buffer.as_ptr().cast::<u32>()) as usize;
|
||||
let after_count = buffer.as_ptr().add(std::mem::size_of::<u32>()) as usize;
|
||||
let align = std::mem::align_of::<SID_AND_ATTRIBUTES>();
|
||||
let entries_addr = (after_count + (align - 1)) & !(align - 1);
|
||||
let restricting_sids =
|
||||
std::slice::from_raw_parts(entries_addr as *const SID_AND_ATTRIBUTES, group_count);
|
||||
Ok(restricting_sids
|
||||
.iter()
|
||||
.any(|entry| EqualSid(entry.Sid, expected_sid) != 0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevated_token_includes_network_proxy_restricting_sid() -> Result<()> {
|
||||
let capability_sid = LocalSid::from_string("S-1-5-21-10-20-30-40")?;
|
||||
let network_proxy_sid = LocalSid::from_string("S-1-5-21-50-60-70-80")?;
|
||||
let base_token = unsafe { get_current_token_for_restriction()? };
|
||||
let restricted_token = unsafe {
|
||||
create_readonly_token_with_caps_and_user_from(
|
||||
base_token,
|
||||
&[capability_sid.as_ptr()],
|
||||
&[network_proxy_sid.as_ptr()],
|
||||
)?
|
||||
};
|
||||
|
||||
let has_network_proxy_sid =
|
||||
unsafe { token_has_restricting_sid(restricted_token, network_proxy_sid.as_ptr()) };
|
||||
unsafe {
|
||||
CloseHandle(restricted_token);
|
||||
CloseHandle(base_token);
|
||||
}
|
||||
|
||||
assert!(has_network_proxy_sid?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -116,6 +116,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
proxy_enforced: bool,
|
||||
network_proxy_restricting_sid: Option<String>,
|
||||
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
||||
timeout_ms: Option<u64>,
|
||||
read_roots_override: Option<&[PathBuf]>,
|
||||
@@ -171,6 +172,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
|
||||
codex_home: elevated.sandbox_base,
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids: elevated.cap_sids,
|
||||
network_proxy_restricting_sid,
|
||||
timeout_ms,
|
||||
tty,
|
||||
stdin_open,
|
||||
|
||||
@@ -76,6 +76,7 @@ fn retry_uses_original_unified_exec_request_and_stops_after_second_failure() {
|
||||
codex_home: PathBuf::from(r"C:\Users\codex\.sandbox"),
|
||||
real_codex_home: PathBuf::from(r"C:\Users\codex"),
|
||||
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
|
||||
network_proxy_restricting_sid: Some("S-1-5-21-100-200-300-400".to_string()),
|
||||
timeout_ms: Some(5_000),
|
||||
tty: true,
|
||||
stdin_open: true,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
mod backends;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -31,6 +32,7 @@ pub struct WindowsSandboxSessionRequest<'a> {
|
||||
pub env_map: HashMap<String, String>,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub proxy_enforced: bool,
|
||||
pub network_proxy_restricting_sid: Option<String>,
|
||||
pub proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
||||
pub timeout_ms: Option<u64>,
|
||||
pub read_roots_override: Option<&'a [PathBuf]>,
|
||||
@@ -57,6 +59,7 @@ pub async fn spawn_windows_sandbox_session_for_level(
|
||||
request.cwd,
|
||||
request.env_map,
|
||||
request.proxy_enforced,
|
||||
request.network_proxy_restricting_sid,
|
||||
request.proxy_settings_mode,
|
||||
request.timeout_ms,
|
||||
request.read_roots_override,
|
||||
@@ -70,6 +73,9 @@ pub async fn spawn_windows_sandbox_session_for_level(
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
if request.network_proxy_restricting_sid.is_some() {
|
||||
bail!("network proxy restricting SID requires the elevated Windows sandbox backend");
|
||||
}
|
||||
spawn_windows_sandbox_session_legacy(
|
||||
request.permission_profile,
|
||||
request.workspace_roots,
|
||||
@@ -129,6 +135,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
|
||||
cwd: &Path,
|
||||
env_map: HashMap<String, String>,
|
||||
proxy_enforced: bool,
|
||||
network_proxy_restricting_sid: Option<String>,
|
||||
timeout_ms: Option<u64>,
|
||||
read_roots_override: Option<&[PathBuf]>,
|
||||
read_roots_include_platform_defaults: bool,
|
||||
@@ -147,6 +154,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
|
||||
cwd,
|
||||
env_map,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid,
|
||||
crate::WindowsSandboxProxySettingsMode::Reconcile,
|
||||
timeout_ms,
|
||||
read_roots_override,
|
||||
|
||||
@@ -24,6 +24,7 @@ const CODEX_HOME_FLAG: &str = "--codex-home";
|
||||
const DENY_READ_PATHS_JSON_FLAG: &str = "--deny-read-paths-json";
|
||||
const DENY_WRITE_PATHS_JSON_FLAG: &str = "--deny-write-paths-json";
|
||||
const ENV_JSON_FLAG: &str = "--env-json";
|
||||
const NETWORK_PROXY_RESTRICTING_SID_FLAG: &str = "--network-proxy-restricting-sid";
|
||||
const PERMISSION_PROFILE_FLAG: &str = "--permission-profile";
|
||||
const PRIVATE_DESKTOP_FLAG: &str = "--windows-sandbox-private-desktop";
|
||||
const PRESERVE_PROXY_SETTINGS_FLAG: &str = "--preserve-proxy-settings";
|
||||
@@ -44,6 +45,7 @@ pub fn create_windows_sandbox_command_args_for_permission_profile(
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
windows_sandbox_private_desktop: bool,
|
||||
proxy_enforced: bool,
|
||||
network_proxy_restricting_sid: Option<&str>,
|
||||
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
||||
read_roots_override: Option<&[PathBuf]>,
|
||||
read_roots_include_platform_defaults: bool,
|
||||
@@ -84,6 +86,10 @@ pub fn create_windows_sandbox_command_args_for_permission_profile(
|
||||
if proxy_enforced {
|
||||
args.push(PROXY_ENFORCED_FLAG.to_string());
|
||||
}
|
||||
if let Some(network_proxy_restricting_sid) = network_proxy_restricting_sid {
|
||||
args.push(NETWORK_PROXY_RESTRICTING_SID_FLAG.to_string());
|
||||
args.push(network_proxy_restricting_sid.to_string());
|
||||
}
|
||||
if proxy_settings_mode == crate::WindowsSandboxProxySettingsMode::Preserve {
|
||||
args.push(PRESERVE_PROXY_SETTINGS_FLAG.to_string());
|
||||
}
|
||||
@@ -159,6 +165,7 @@ struct WindowsSandboxWrapperRequest {
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
windows_sandbox_private_desktop: bool,
|
||||
proxy_enforced: bool,
|
||||
network_proxy_restricting_sid: Option<String>,
|
||||
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
||||
read_roots_override: Option<Vec<PathBuf>>,
|
||||
read_roots_include_platform_defaults: bool,
|
||||
@@ -182,6 +189,7 @@ async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperReque
|
||||
env_map: request.env_map,
|
||||
windows_sandbox_level: request.windows_sandbox_level,
|
||||
proxy_enforced: request.proxy_enforced,
|
||||
network_proxy_restricting_sid: request.network_proxy_restricting_sid,
|
||||
proxy_settings_mode: request.proxy_settings_mode,
|
||||
timeout_ms: None,
|
||||
read_roots_override: request.read_roots_override.as_deref(),
|
||||
@@ -208,6 +216,7 @@ fn parse_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<WindowsSandbo
|
||||
let mut windows_sandbox_level = None;
|
||||
let mut windows_sandbox_private_desktop = false;
|
||||
let mut proxy_enforced = false;
|
||||
let mut network_proxy_restricting_sid = None;
|
||||
let mut proxy_settings_mode = crate::WindowsSandboxProxySettingsMode::Reconcile;
|
||||
let mut read_roots_override = None;
|
||||
let mut read_roots_include_platform_defaults = false;
|
||||
@@ -252,6 +261,9 @@ fn parse_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<WindowsSandbo
|
||||
proxy_settings_mode = crate::WindowsSandboxProxySettingsMode::Preserve;
|
||||
}
|
||||
PROXY_ENFORCED_FLAG => proxy_enforced = true,
|
||||
NETWORK_PROXY_RESTRICTING_SID_FLAG => {
|
||||
network_proxy_restricting_sid = Some(next_flag_value(&mut args, &arg)?);
|
||||
}
|
||||
READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG => {
|
||||
read_roots_include_platform_defaults = true;
|
||||
}
|
||||
@@ -293,6 +305,7 @@ fn parse_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<WindowsSandbo
|
||||
.ok_or_else(|| anyhow!("missing required {SANDBOX_LEVEL_FLAG}"))?,
|
||||
windows_sandbox_private_desktop,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid,
|
||||
proxy_settings_mode,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
|
||||
@@ -14,6 +14,7 @@ use super::COMMAND_CWD_FLAG;
|
||||
use super::DENY_READ_PATHS_JSON_FLAG;
|
||||
use super::DENY_WRITE_PATHS_JSON_FLAG;
|
||||
use super::ENV_JSON_FLAG;
|
||||
use super::NETWORK_PROXY_RESTRICTING_SID_FLAG;
|
||||
use super::PERMISSION_PROFILE_FLAG;
|
||||
use super::PRESERVE_PROXY_SETTINGS_FLAG;
|
||||
use super::PRIVATE_DESKTOP_FLAG;
|
||||
@@ -62,6 +63,7 @@ fn windows_wrapper_args_round_trip() {
|
||||
WindowsSandboxLevel::Elevated,
|
||||
/*windows_sandbox_private_desktop*/ true,
|
||||
/*proxy_enforced*/ true,
|
||||
/*network_proxy_restricting_sid*/ Some("S-1-5-21-100-200-300-400"),
|
||||
crate::WindowsSandboxProxySettingsMode::Preserve,
|
||||
Some(read_roots_override.as_slice()),
|
||||
/*read_roots_include_platform_defaults*/ true,
|
||||
@@ -80,6 +82,7 @@ fn windows_wrapper_args_round_trip() {
|
||||
assert!(args.contains(&SANDBOX_LEVEL_FLAG.to_string()));
|
||||
assert!(args.contains(&PRIVATE_DESKTOP_FLAG.to_string()));
|
||||
assert!(args.contains(&PROXY_ENFORCED_FLAG.to_string()));
|
||||
assert!(args.contains(&NETWORK_PROXY_RESTRICTING_SID_FLAG.to_string()));
|
||||
assert!(args.contains(&PRESERVE_PROXY_SETTINGS_FLAG.to_string()));
|
||||
assert!(args.contains(&READ_ROOTS_JSON_FLAG.to_string()));
|
||||
assert!(args.contains(&READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG.to_string()));
|
||||
@@ -101,6 +104,10 @@ fn windows_wrapper_args_round_trip() {
|
||||
assert_eq!(parsed.windows_sandbox_level, WindowsSandboxLevel::Elevated);
|
||||
assert_eq!(parsed.windows_sandbox_private_desktop, true);
|
||||
assert_eq!(parsed.proxy_enforced, true);
|
||||
assert_eq!(
|
||||
parsed.network_proxy_restricting_sid.as_deref(),
|
||||
Some("S-1-5-21-100-200-300-400")
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.proxy_settings_mode,
|
||||
crate::WindowsSandboxProxySettingsMode::Preserve
|
||||
|
||||
Reference in New Issue
Block a user