Default local binding to true for MXC managed networking (#46523)

## Why

MXC's native host-loopback access is bidirectional, so it cannot enforce `allow_local_binding = false`. Treating an omitted setting as `false` prevents managed networking from working with the default configuration.

## What changed

- Preserve an omitted `allow_local_binding` until the executor's sandbox policy is known. Default to `true` for Windows MXC and `false` elsewhere, including remote execution.
- Reject an effective `false` for MXC managed networking after applying policy restrictions, without enabling disabled networking.
- Use the executor's resolved value for remote network approval decisions while preserving explicit controller restrictions.
- Document that local binding permits local servers and direct host-loopback connections and skips additional private-network destination checks; proxy domain rules still apply.

## Testing

Add coverage for per-executor defaults, explicit values, MXC rejection of `false`, and remote network review cleanup with the resolved policy.

GitOrigin-RevId: 0fa7c1eaec68bebada2b8f7af45688315eea70a9
This commit is contained in:
iceweasel-oai
2026-09-19 00:00:01 +00:00
committed by copyberry
parent 3fd9e7e30e
commit 74af2496c9
40 changed files with 380 additions and 123 deletions
@@ -1,5 +1,6 @@
use super::*;
use codex_core::exec_env::inject_apply_patch_env;
use codex_core::windows_sandbox::local_binding_policy_for_sandbox;
use codex_core::windows_sandbox::managed_proxy_routing_for_windows_sandbox;
use codex_protocol::shell_environment::is_non_inheritable_env_var;
@@ -266,6 +267,10 @@ impl CommandExecRequestProcessor {
managed_proxy_routing_for_windows_sandbox(
self.config.permissions.windows_sandbox_type,
),
local_binding_policy_for_sandbox(
self.config.permissions.windows_sandbox_type,
Some(std::env::consts::OS),
),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
+5
View File
@@ -19,6 +19,7 @@ use codex_core::exec_env::create_env;
#[cfg(target_os = "macos")]
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_core::windows_sandbox::local_binding_policy_for_sandbox;
use codex_core::windows_sandbox::managed_proxy_routing_for_windows_sandbox;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::PermissionProfile;
@@ -356,6 +357,10 @@ async fn run_command_under_sandbox(
spec.start_proxy(
&permission_profile,
managed_proxy_routing_for_windows_sandbox(config.permissions.windows_sandbox_type),
local_binding_policy_for_sandbox(
config.permissions.windows_sandbox_type,
Some(std::env::consts::OS),
),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
@@ -435,6 +435,7 @@ pub struct NetworkRequirementsToml {
/// network enforcement is active. User allowlist entries are ignored.
pub managed_allowed_domains_only: Option<bool>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
/// MXC requires true when managed networking is enabled; an explicit false is rejected.
pub allow_local_binding: Option<bool>,
/// Requirements-only header injections. These annotate matching requests
/// without changing whether non-matching requests are allowed.
+4 -1
View File
@@ -340,6 +340,9 @@ pub struct NetworkToml {
pub mode: Option<NetworkMode>,
pub domains: Option<NetworkDomainPermissionsToml>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
/// Permits local servers and direct host-loopback connections and skips the proxy's
/// additional private-network destination checks. Proxy domain rules still apply.
/// Defaults to true for MXC, which cannot enforce false; otherwise defaults to false.
pub allow_local_binding: Option<bool>,
pub mitm: Option<NetworkMitmToml>,
}
@@ -515,7 +518,7 @@ impl NetworkToml {
config.unix_sockets = Some(proxy_unix_sockets);
}
if let Some(allow_local_binding) = self.allow_local_binding {
config.allow_local_binding = allow_local_binding;
config.allow_local_binding = Some(allow_local_binding);
}
if let Some(mitm) = self.mitm.as_ref() {
config.mitm_hooks = mitm.to_runtime_hooks(mitm.actions.as_ref());
+2
View File
@@ -2817,6 +2817,7 @@
"additionalProperties": false,
"properties": {
"allow_local_binding": {
"description": "Permits local servers and direct host-loopback connections and skips the proxy's additional private-network destination checks. Proxy domain rules still apply. Defaults to true for MXC, which cannot enforce false; otherwise defaults to false.",
"type": "boolean"
},
"allow_upstream_proxy": {
@@ -2895,6 +2896,7 @@
"additionalProperties": false,
"properties": {
"allow_local_binding": {
"description": "Permits local servers and direct host-loopback connections and skips the proxy's additional private-network destination checks. Proxy domain rules still apply. Defaults to true for MXC, which cannot enforce false; otherwise defaults to false.",
"type": "boolean"
},
"allow_upstream_proxy": {
@@ -67,6 +67,7 @@ pub fn validate_environment_network_policy(
policy,
permission_profile,
&Policy::empty(),
codex_network_proxy::LocalBindingPolicy::DefaultFalse,
)
.and_then(|spec| spec.build_config_state_for_spec(executor_os).map(|_| ()))
.map_err(|_| EnvironmentNetworkConfigError)
+16 -5
View File
@@ -5,6 +5,7 @@ use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigReloaderFuture;
use codex_network_proxy::ConfigState;
use codex_network_proxy::EnvironmentNetworkPolicy;
use codex_network_proxy::LocalBindingPolicy;
use codex_network_proxy::ManagedProxyRouting;
use codex_network_proxy::NetworkDecision;
use codex_network_proxy::NetworkPolicyDecider;
@@ -111,7 +112,7 @@ impl NetworkProxySpec {
#[cfg(any(target_os = "windows", test))]
pub(crate) fn allow_local_binding(&self) -> bool {
self.config.allow_local_binding
self.config.allow_local_binding()
}
/// Returns the firewall settings and listener identities used by the Windows provisioning service.
@@ -145,7 +146,7 @@ impl NetworkProxySpec {
Ok((
codex_windows_sandbox::WindowsSandboxProvisioningSettings {
proxy_ports,
allow_local_binding: self.config.allow_local_binding,
allow_local_binding: self.config.allow_local_binding(),
},
codex_windows_sandbox::WindowsSandboxProxyListeners {
http_ports: vec![http_port],
@@ -188,16 +189,22 @@ impl NetworkProxySpec {
})
}
#[allow(clippy::too_many_arguments)]
pub async fn start_proxy(
&self,
permission_profile: &PermissionProfile,
managed_proxy_routing: ManagedProxyRouting,
local_binding_policy: LocalBindingPolicy,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
enable_network_approval_flow: bool,
audit_metadata: NetworkProxyAuditMetadata,
) -> std::io::Result<StartedNetworkProxy> {
let state = self.build_state_with_audit_metadata(audit_metadata, Platform::native())?;
let state = self.build_state_with_audit_metadata(
audit_metadata,
Platform::native(),
local_binding_policy,
)?;
let mut builder = NetworkProxy::builder()
.state(Arc::new(state))
.managed_proxy_routing(managed_proxy_routing);
@@ -248,6 +255,7 @@ impl NetworkProxySpec {
policy: &EnvironmentNetworkPolicy,
permission_profile: &PermissionProfile,
exec_policy: &Policy,
local_binding_policy: LocalBindingPolicy,
) -> std::io::Result<Self> {
if matches!(permission_profile, PermissionProfile::Disabled) {
return Err(std::io::Error::new(
@@ -268,13 +276,14 @@ impl NetworkProxySpec {
enabled: true,
// Without a controller, the owner supplies the entire permission ceiling.
dangerously_allow_all_unix_sockets: Some(true),
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
},
/*requirements*/ None,
permission_profile,
)?,
};
spec.config.allow_local_binding = Some(local_binding_policy.resolve(&spec.config));
policy.apply_to(&mut spec.config);
let protected_denials = spec.config.denied_domains().unwrap_or_default();
@@ -353,6 +362,7 @@ impl NetworkProxySpec {
&self,
audit_metadata: NetworkProxyAuditMetadata,
executor_os: Platform,
local_binding_policy: LocalBindingPolicy,
) -> std::io::Result<NetworkProxyState> {
let state = self.build_config_state_for_spec(executor_os)?;
let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone()));
@@ -360,6 +370,7 @@ impl NetworkProxySpec {
state,
reloader,
audit_metadata,
local_binding_policy,
))
}
@@ -468,7 +479,7 @@ impl NetworkProxySpec {
constraints.allow_unix_sockets = Some(allow_unix_sockets);
}
if let Some(allow_local_binding) = requirements.allow_local_binding {
config.allow_local_binding = allow_local_binding;
config.allow_local_binding = Some(allow_local_binding);
constraints.allow_local_binding = Some(allow_local_binding);
}
@@ -5,6 +5,7 @@ use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
use codex_execpolicy::Decision::Allow;
use codex_execpolicy::NetworkRuleProtocol::Https;
use codex_network_proxy::LocalBindingPolicy::DefaultFalse;
use codex_network_proxy::NetworkDomainPermission;
use codex_network_proxy::NetworkUnixSocketPermission;
use codex_network_proxy::NetworkUnixSocketPermissions;
@@ -78,15 +79,18 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
&owner,
&profile,
&Policy::empty(),
DefaultFalse,
)?;
assert_eq!(
composed.config.dangerously_allow_all_unix_sockets,
Some(expected),
"{name}"
);
let state = Arc::new(
controller.build_state_with_audit_metadata(Default::default(), Platform::Linux)?,
);
let state = Arc::new(controller.build_state_with_audit_metadata(
Default::default(),
Platform::Linux,
DefaultFalse,
)?);
let proxy = NetworkProxy::builder()
.state(Arc::clone(&state))
.managed_by_codex(false)
@@ -99,7 +103,7 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
Some(composed.environment_policy()),
/*fallback_policy_decider*/ None,
)?;
let launch = scoped.remote_launch_config().await?;
let launch = scoped.remote_launch_config(DefaultFalse).await?;
assert_eq!(
launch.proxy.dangerously_allow_all_unix_sockets, expected,
"{name}"
@@ -115,7 +119,7 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
// Inheritance must not grant sockets to ordinary controller-only commands.
assert!(
!proxy
.remote_launch_config()
.remote_launch_config(DefaultFalse)
.await?
.proxy
.dangerously_allow_all_unix_sockets
@@ -123,7 +127,7 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
state.add_allowed_domain("granted.example").await?;
assert!(
scoped
.remote_launch_config()
.remote_launch_config(DefaultFalse)
.await?
.proxy
.dangerously_allow_all_unix_sockets
@@ -144,7 +148,7 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
.await?;
assert!(
!scoped
.remote_launch_config()
.remote_launch_config(DefaultFalse)
.await?
.proxy
.dangerously_allow_all_unix_sockets
@@ -171,7 +175,7 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() {
};
let state = spec
.build_state_with_audit_metadata(metadata.clone(), Platform::Linux)
.build_state_with_audit_metadata(metadata.clone(), Platform::Linux, DefaultFalse)
.expect("state should build");
assert_eq!(state.audit_metadata(), &metadata);
}
@@ -184,7 +188,7 @@ fn windows_sandbox_proxy_listeners_preserve_effective_protocol_roles() {
enabled: true,
proxy_url: "http://127.0.0.1:48081".to_string(),
socks_url: "socks5h://127.0.0.1:3128".to_string(),
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
},
/*requirements*/ None,
@@ -250,7 +254,7 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials()
"/tmp/allowed.sock".to_string(),
]);
owner.dangerously_allow_all_unix_sockets = Some(true);
owner.allow_local_binding = true;
owner.allow_local_binding = Some(true);
let owner_policy =
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false);
assert_eq!(
@@ -265,7 +269,12 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials()
),
Err(EnvironmentNetworkConfigError)
);
let compose = NetworkProxySpec::for_environment;
let compose = |controller: Option<&NetworkProxySpec>,
policy: &EnvironmentNetworkPolicy,
profile: &PermissionProfile,
rules: &Policy| {
NetworkProxySpec::for_environment(controller, policy, profile, rules, DefaultFalse)
};
let empty = Policy::empty();
let disabled_controller = NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
@@ -297,7 +306,7 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials()
owner.unix_sockets.clone_from(&spec.config.unix_sockets);
owner.allow_upstream_proxy = false;
owner.dangerously_allow_all_unix_sockets = Some(false);
owner.allow_local_binding = false;
owner.allow_local_binding = Some(false);
assert_eq!(
restricted.environment_policy(),
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false)
+1
View File
@@ -67,6 +67,7 @@ fn validate_environment_config(
policy,
config.permission_profile.permission_profile(),
&Policy::empty(),
codex_network_proxy::LocalBindingPolicy::DefaultFalse,
)
.map_err(|error| {
CodexErr::InvalidRequest(format!("invalid environment network policy: {error}"))
+2
View File
@@ -335,6 +335,7 @@ use crate::turn_timing::TurnTimingState;
use crate::turn_timing::record_turn_ttfm_metric;
use crate::unified_exec::UnifiedExecProcessManager;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use crate::windows_sandbox::local_binding_policy_for_sandbox;
use crate::windows_sandbox::managed_proxy_routing_for_windows_sandbox;
use codex_core_plugins::PluginCommandAttribution;
use codex_core_plugins::PluginsManager;
@@ -1221,6 +1222,7 @@ impl Session {
.start_proxy(
permission_profile,
managed_proxy_routing_for_windows_sandbox(windows_sandbox_type),
local_binding_policy_for_sandbox(windows_sandbox_type, Some(std::env::consts::OS)),
network_policy_decider,
blocked_request_observer,
managed_network_requirements_enabled,
+3 -1
View File
@@ -239,6 +239,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
.start_proxy(
&permission_profile,
codex_network_proxy::ManagedProxyRouting::SharedIngress,
codex_network_proxy::LocalBindingPolicy::DefaultFalse,
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
/*enable_network_approval_flow*/ false,
@@ -627,7 +628,7 @@ async fn snapshot_discovers_and_redacts_shell_initialized_credentials() -> Resul
let mut network_config = NetworkProxyConfig::default();
network_config.set_credential_broker_enabled(/*enabled*/ true);
network_config.allow_local_binding = true;
network_config.allow_local_binding = Some(true);
network_config.credential_providers.insert(
"local".to_string(),
CredentialProviderConfig {
@@ -670,6 +671,7 @@ async fn snapshot_discovers_and_redacts_shell_initialized_credentials() -> Resul
.start_proxy(
&permission_profile,
codex_network_proxy::ManagedProxyRouting::SharedIngress,
codex_network_proxy::LocalBindingPolicy::DefaultFalse,
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
/*enable_network_approval_flow*/ false,
+14 -4
View File
@@ -1117,6 +1117,9 @@ pub(crate) async fn begin_network_approval(
}
let controller = turn.config.permissions.network.as_ref();
let environment = environments
.turn_environments()
.find(|environment| environment.selection.environment_id == environment_id);
let owner_spec = network_policy
.as_ref()
.map(|policy| {
@@ -1126,6 +1129,15 @@ pub(crate) async fn begin_network_approval(
policy,
&permission_profile,
session.services.exec_policy.current().as_ref(),
environment.map_or(
codex_network_proxy::LocalBindingPolicy::DefaultFalse,
|environment| {
crate::windows_sandbox::local_binding_policy_for_sandbox(
environment.config().windows_sandbox_type,
environment.executor_platform_os.as_deref(),
)
},
),
)
})
.transpose()
@@ -1153,15 +1165,13 @@ pub(crate) async fn begin_network_approval(
} else {
// This carrier never listens: the executor starts the real per-command proxy.
let executor_os = Platform::from_platform_os(
environments
.turn_environments()
.find(|environment| environment.selection.environment_id == environment_id)
.and_then(|environment| environment.executor_platform_os.as_deref()),
environment.and_then(|environment| environment.executor_platform_os.as_deref()),
);
let state = owner_spec
.build_state_with_audit_metadata(
session.services.network_proxy_audit_metadata.clone(),
executor_os,
codex_network_proxy::LocalBindingPolicy::DefaultFalse,
)
.map_err(|error| {
ToolError::Rejected(format!(
@@ -354,13 +354,21 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
};
let (mut env, managed_network_context, network_proxy_launch) = match managed_network {
Some(network) if environment_is_remote => {
let mut launch = network.remote_launch_config().await.map_err(|err| {
ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string())))
})?;
let mut launch = network
.remote_launch_config(crate::windows_sandbox::local_binding_policy_for_sandbox(
req.turn_environment.config().windows_sandbox_type,
req.turn_environment.executor_platform_os.as_deref(),
))
.await
.map_err(|err| {
ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string())))
})?;
if routes_approval_policy_to_guardian(
ctx.step_context.settings.approval_policy(),
ctx.step_context.settings.approvals_reviewer(),
) && network.remote_policy_decider().is_some()
) && network
.remote_policy_decider(launch.proxy.allow_local_binding)
.is_some()
{
let timeout = ctx
.session
@@ -1278,7 +1278,7 @@ impl UnifiedExecProcessManager {
let network_policy_decider = network_proxy_launch
.as_ref()
.filter(|launch| launch.policy_decision_timeout_ms.is_some())
.and_then(|_| network.and_then(NetworkProxy::remote_policy_decider));
.and_then(|launch| network?.remote_policy_decider(launch.proxy.allow_local_binding));
if environment.is_remote() && network.is_some() && network_proxy_launch.is_none() {
request.exec_server_enforce_managed_network = false;
}
+12
View File
@@ -18,6 +18,18 @@ use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;
/// Selects local binding policy from the sandbox and executor OS.
pub fn local_binding_policy_for_sandbox(
sandbox_type: SandboxType,
platform_os: Option<&str>,
) -> codex_network_proxy::LocalBindingPolicy {
if sandbox_type == SandboxType::WindowsMxc && platform_os == Some("windows") {
codex_network_proxy::LocalBindingPolicy::RequireTrue
} else {
codex_network_proxy::LocalBindingPolicy::DefaultFalse
}
}
pub fn managed_proxy_routing_for_windows_sandbox(
sandbox_type: SandboxType,
) -> codex_network_proxy::ManagedProxyRouting {
+1 -1
View File
@@ -125,7 +125,7 @@ fn provisioning_settings_omit_the_disabled_socks_proxy() {
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,
allow_local_binding: Some(true),
..Default::default()
};
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
@@ -1858,7 +1858,7 @@ async fn thread_turnover_closes_managed_proxy_tunnels() -> Result<()> {
let mut network = NetworkProxyConfig {
enabled: true,
mode: codex_network_proxy::NetworkMode::Full,
allow_local_binding: true,
allow_local_binding: Some(true),
allow_upstream_proxy: false,
..NetworkProxyConfig::default()
};
@@ -2413,7 +2413,7 @@ async fn owner_network_policy_follows_the_selected_remote_command() -> Result<()
NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig {
enabled: true,
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
},
/*requirements*/ None,
@@ -2458,7 +2458,7 @@ async fn owner_network_policy_follows_the_selected_remote_command() -> Result<()
}
let marker = format!("{scenario}_OWNER_{suffix}");
let mut proxy_config = NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
};
proxy_config.set_allowed_domains(vec![allowed_domain.to_string()]);
@@ -1,14 +1,13 @@
//! Exercises process cleanup while a remote network review is still pending.
//! Exercises remote network review startup and cleanup, including MXC's local-binding default.
use super::PushedExecScenario;
use super::accept_initialized_exec_server;
use super::read_exec_server_json;
use super::respond_environment_info;
use super::send_exec_server_json;
use anyhow::Context;
use anyhow::Result;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_core::TurnInputRequest;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::items::CommandExecutionStatus;
@@ -16,8 +15,8 @@ use codex_protocol::items::TurnItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::sandbox::SandboxType;
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;
@@ -34,13 +33,22 @@ use serde_json::Value;
use serde_json::json;
use std::fs;
use std::time::Duration;
use test_case::test_case;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_tungstenite::WebSocketStream;
#[test_case(SandboxType::WindowsRestrictedToken, "allow_local_binding = true", Ok(true); "explicit_local_binding")]
#[test_case(SandboxType::WindowsMxc, "", Ok(true); "mxc_default_local_binding")]
#[test_case(SandboxType::WindowsMxc, "allow_local_binding = false", Err("MXC cannot enforce allow_local_binding=false"); "mxc_rejects_false")]
#[test_case(SandboxType::WindowsRestrictedToken, "", Ok(false); "non_mxc_default_local_binding")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn completed_remote_process_withdraws_pending_network_review() -> Result<()> {
async fn remote_local_binding_policy_and_review_cleanup(
sandbox_type: SandboxType,
local_binding: &'static str,
expected_local_binding: Result<bool, &'static str>,
) -> Result<()> {
let server = start_mock_server().await;
let listener = TcpListener::bind("127.0.0.1:0").await?;
let exec_server_url = format!("ws://{}", listener.local_addr()?);
@@ -99,15 +107,26 @@ async fn completed_remote_process_withdraws_pending_network_review() -> Result<(
.as_u64()
.is_some()
);
let allow_local_binding =
expected_local_binding.expect("invalid MXC config must not launch");
assert_eq!(
start["params"]["networkProxy"]["proxy"]["allowLocalBinding"],
allow_local_binding
);
let process_id = &start["params"]["processId"];
send_exec_server_json(
&mut websocket,
json!({"id": start["id"], "result": {"processId": process_id}}),
)
.await;
let host = if allow_local_binding {
"10.0.0.1"
} else {
"8.8.8.8"
};
send_exec_server_json(&mut websocket, json!({
"id": 1, "method": "network/policyRequest",
"params": {"processId": process_id, "request": {"protocol": "https_connect", "host": "review.example", "port": 443}}
"params": {"processId": process_id, "request": {"protocol": "https_connect", "host": host, "port": 443}}
})).await;
timeout(Duration::from_secs(/*secs*/ 10), async {
while !pending_review.requests().iter().any(|request| {
@@ -155,24 +174,31 @@ async fn completed_remote_process_withdraws_pending_network_review() -> Result<(
});
let test = test_codex()
.with_exec_server_url(exec_server_url)
.with_cloud_config_bundle(managed_network_requirements_loader())
.with_pre_build_hook(|home| {
.with_cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
"[experimental_network]\nenabled = true\n{local_binding}"
)),
)
.with_pre_build_hook(move |home| {
fs::write(
home.join("config.toml"),
r#"default_permissions = "workspace"
format!(
r#"default_permissions = "workspace"
[permissions.workspace.filesystem]
":minimal" = "read"
[permissions.workspace.network]
enabled = true
mode = "full"
allow_local_binding = true
"#,
{local_binding}
"#
),
)
.expect("managed network config");
})
.with_config(|config| {
.with_config(move |config| {
config.project_doc_max_bytes = 0;
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config.permissions.windows_sandbox_type = sandbox_type;
#[cfg(windows)]
config.set_windows_sandbox_enabled(/*value*/ true);
})
@@ -198,6 +224,21 @@ allow_local_binding = true
}),
)
.await?;
timeout(Duration::from_secs(/*secs*/ 15), async {
while final_response.function_call_output_text(call_id).is_none() {
tokio::time::sleep(Duration::from_millis(/*millis*/ 10)).await;
}
})
.await?;
let output = final_response
.function_call_output_text(call_id)
.context("tool output")?;
if let Err(expected_error) = expected_local_binding {
assert!(output.contains(expected_error), "{output}");
exec_server.abort();
assert!(exec_server.await.is_err_and(|err| err.is_cancelled()));
return Ok(());
}
let completed = timeout(Duration::from_secs(/*secs*/ 15), async {
loop {
match test.codex.next_event().await?.msg {
@@ -225,15 +266,6 @@ allow_local_binding = true
Some("build complete\n")
)
);
timeout(Duration::from_secs(/*secs*/ 10), async {
while final_response.function_call_output_text(call_id).is_none() {
tokio::time::sleep(Duration::from_millis(/*millis*/ 10)).await;
}
})
.await?;
let output = final_response
.function_call_output_text(call_id)
.context("tool output")?;
assert!(output.contains("Process exited with code 0"), "{output}");
assert!(output.contains("build complete"), "{output}");
assert_eq!(parent.requests().len(), 1);
@@ -257,7 +289,14 @@ fn review_body(request: &wiremock::Request) -> Value {
async fn respond_setup_request(websocket: &mut WebSocketStream<TcpStream>, request: &Value) {
match request["method"].as_str() {
Some("environment/info") => {
respond_environment_info(websocket, &request["id"], PushedExecScenario::Complete).await;
send_exec_server_json(websocket, json!({
"id": request["id"],
"result": {
"shell": {"name": "powershell", "path": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"},
"platformOs": "windows",
"capabilities": {"networkProxyLaunch": true}
}
})).await;
}
Some("fs/getMetadata" | "fs/readFile" | "fs/readDirectory") => {
send_exec_server_json(
@@ -289,6 +328,6 @@ async fn respond_setup_request(websocket: &mut WebSocketStream<TcpStream>, reque
)
.await;
}
method => panic!("unexpected setup request: {method:?}"),
_ => panic!("unexpected setup request: {request}"),
}
}
+1 -1
View File
@@ -574,7 +574,7 @@ async fn shell_snapshot_v2_prewarm_stops_on_shutdown() -> Result<()> {
}; "untrusted")]
#[test_case::test_case(|config| {
config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig { enabled: true, allow_local_binding: true, ..Default::default() },
NetworkProxyConfig { enabled: true, allow_local_binding: Some(true), ..Default::default() },
/*requirements*/ None,
config.permissions.permission_profile(),
).expect("configure managed network"));
+3
View File
@@ -480,6 +480,9 @@ pub struct NetworkProxyConfigToml {
pub domains: Option<BTreeMap<String, NetworkProxyDomainPermissionToml>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unix_sockets: Option<BTreeMap<String, NetworkProxyUnixSocketPermissionToml>>,
/// Permits local servers and direct host-loopback connections and skips the proxy's
/// additional private-network destination checks. Proxy domain rules still apply.
/// Defaults to true for MXC, which cannot enforce false; otherwise defaults to false.
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_local_binding: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
+8 -4
View File
@@ -46,10 +46,14 @@ to 4096 bytes; the helper removes them before native process creation. Use
semantics and scan limits as the existing Windows sandbox.
- Native deny paths depend on the host's capability probe. An installed Windows
update alone is not treated as evidence that every policy feature is enabled.
- This adapter rejects managed networking with `allow_local_binding=false`:
its host-loopback permission is bidirectional. MXC's proxy-peer identity mode
is not integrated here. With local binding enabled, direct DNS remains denied,
matching the existing Windows sandbox.
- When MXC is the executor's selected backend, managed networking defaults
`allow_local_binding` to `true`. An effective `false` after applying managed
requirements is a configuration error: native host-loopback access
is bidirectional, and MXC's proxy-peer identity mode is not integrated here.
`true` permits local servers and direct host-loopback connections and removes
the proxy's additional private-network destination checks. Proxy domain rules
still apply to proxied traffic; direct DNS remains denied. This default also
applies to remote Windows executors and does not enable disabled networking.
- Windows volume-root grants do not recurse. The adapter grants the root and
its immediate children; directories added or newly mounted during a running
command are not implicitly granted.
+2 -1
View File
@@ -47,7 +47,8 @@ mode = "full" # default when unset; use "limited" for read-only mode
# Hostnames that resolve to local/private IPs are still blocked even if allowlisted.
# Clients that always bypass proxies for loopback, such as Go's `net/http`, remain blocked by
# the operating-system sandbox when local binding is disabled.
allow_local_binding = false
# Omitted: true for MXC, false elsewhere. MXC rejects an effective false.
# allow_local_binding = false
# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
# absolute socket path from `x-unix-socket`.
@@ -159,7 +159,7 @@ impl TestProxy {
mode,
mitm: true,
credential_broker: true,
allow_local_binding: true,
allow_local_binding: Some(true),
credential_providers: BTreeMap::from([(
"local".to_string(),
CredentialProviderConfig {
+9 -4
View File
@@ -139,7 +139,9 @@ pub struct NetworkProxyConfig {
pub domains: Option<NetworkDomainPermissions>,
#[serde(default)]
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
pub allow_local_binding: bool,
/// Omission is resolved for the selected sandbox; ordinary proxy execution defaults to false.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_local_binding: Option<bool>,
#[serde(default)]
pub mitm: bool,
#[serde(default)]
@@ -175,7 +177,7 @@ impl Default for NetworkProxyConfig {
mode: NetworkMode::default(),
domains: None,
unix_sockets: None,
allow_local_binding: false,
allow_local_binding: None,
mitm: false,
credential_broker: false,
credential_broker_enabled_mitm: false,
@@ -189,6 +191,10 @@ impl Default for NetworkProxyConfig {
}
impl NetworkProxyConfig {
pub fn allow_local_binding(&self) -> bool {
self.allow_local_binding.unwrap_or(false)
}
pub fn set_credential_broker_enabled(&mut self, enabled: bool) {
self.credential_broker = enabled;
if enabled {
@@ -710,7 +716,7 @@ mod tests {
mode: NetworkMode::Full,
domains: None,
unix_sockets: None,
allow_local_binding: false,
allow_local_binding: None,
mitm: false,
credential_broker: false,
credential_broker_enabled_mitm: false,
@@ -905,7 +911,6 @@ mod tests {
"example.com": "deny",
},
"unix_sockets": null,
"allow_local_binding": false,
"mitm": false,
"credential_broker": false,
"dangerously_allow_plaintext_credential_injection": false,
+1 -1
View File
@@ -178,7 +178,7 @@ mod tests {
let target = listener.local_addr().expect("local addr");
let connector = TargetCheckedTcpConnector::new(Arc::new(network_proxy_state_for_policy(
NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
},
)));
@@ -71,7 +71,7 @@ async fn canceling_wait_closes_proxy_connections() -> Result<()> {
async fn assert_stop_closes_connections(stop: StopProxy) -> Result<()> {
let mut config = NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
allow_upstream_proxy: false,
enable_socks5_udp: false,
..NetworkProxyConfig::default()
@@ -287,14 +287,14 @@ impl CredentialBroker {
.then(|| active_credential_sources(&state, &HashMap::new()));
if state.enabled != config.credential_broker
|| state.openai_api_host != config.credential_broker_openai_host
|| state.allow_local_binding != config.allow_local_binding
|| state.allow_local_binding != config.allow_local_binding()
|| state.configured_provider_configs != config.credential_providers
|| previous_context_sources.is_some()
{
state.config_revision += 1;
}
state.context.clone_from(&config.credential_broker_context);
state.allow_local_binding = config.allow_local_binding;
state.allow_local_binding = config.allow_local_binding();
if state.enabled != config.credential_broker {
state.enabled = config.credential_broker;
state.credentials.clear();
@@ -280,7 +280,7 @@ fn local_proxy_bypass_preserves_credentials_and_aliases_across_reload() {
assert_ne!(dummy, token);
assert_ne!(github_dummy, github);
config.allow_local_binding = true;
config.allow_local_binding = Some(true);
let revision = broker.config_revision();
broker.configure(&config);
assert_eq!(broker.config_revision(), revision + 1);
@@ -305,7 +305,7 @@ fn local_proxy_bypass_preserves_credentials_and_aliases_across_reload() {
}
);
}
config.allow_local_binding = false;
config.allow_local_binding = Some(false);
let mut snapshot_env = env.clone();
broker.virtualize_snapshot_env(&mut snapshot_env, /*environment_id*/ None);
assert_eq!(snapshot_env["VENDOR_TOKEN"], dummy);
@@ -325,7 +325,7 @@ fn child_alias_identity_survives_scoped_dummies_and_partial_direct_restoration()
let broker = CredentialBroker::new(/*enabled*/ true);
broker.configure(&NetworkProxyConfig {
credential_broker: true,
allow_local_binding,
allow_local_binding: Some(allow_local_binding),
credential_providers: BTreeMap::from([(
"local".to_string(),
CredentialProviderConfig {
@@ -386,7 +386,7 @@ fn local_proxy_bypass_is_scoped_for_inherited_credentials_and_aliases() {
let broker = CredentialBroker::new(/*enabled*/ true);
broker.configure(&NetworkProxyConfig {
credential_broker: true,
allow_local_binding: true,
allow_local_binding: Some(true),
credential_providers: BTreeMap::from([(
"vendor".to_string(),
CredentialProviderConfig {
@@ -30,7 +30,7 @@ impl EnvironmentNetworkPolicy {
dangerously_allow_all_unix_sockets: config
.dangerously_allow_all_unix_sockets
.unwrap_or(false),
allow_local_binding: config.allow_local_binding,
allow_local_binding: config.allow_local_binding(),
managed_allowed_domains_only,
}
}
@@ -87,6 +87,6 @@ impl EnvironmentNetworkPolicy {
config.dangerously_allow_all_unix_sockets =
Some(inherited_permits_all && owner_permits_all);
config.allow_upstream_proxy &= self.allow_upstream_proxy;
config.allow_local_binding &= self.allow_local_binding;
config.allow_local_binding = Some(config.allow_local_binding() && self.allow_local_binding);
}
}
+3 -3
View File
@@ -1264,7 +1264,7 @@ mod tests {
async fn http_connect_accept_allows_allowlisted_host_in_full_mode() {
let policy = {
let mut policy = NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
};
policy.set_allowed_domains(vec!["example.com".to_string()]);
@@ -1562,7 +1562,7 @@ mod tests {
..NetworkProxyConfig::default()
};
network.set_allowed_domains(vec!["127.0.0.1".to_string()]);
network.allow_local_binding = true;
network.allow_local_binding = Some(true);
network
}));
let mut env = HashMap::from([
@@ -1639,7 +1639,7 @@ mod tests {
let state = Arc::new(network_proxy_state_for_policy({
let mut network = NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
mitm: true,
mitm_hooks: vec![crate::mitm_hook::MitmHookConfig {
host: "127.0.0.1".to_string(),
+1
View File
@@ -114,6 +114,7 @@ pub use runtime::BlockedRequestObserverFuture;
pub use runtime::ConfigReloader;
pub use runtime::ConfigReloaderFuture;
pub use runtime::ConfigState;
pub use runtime::LocalBindingPolicy;
pub use runtime::NetworkProxyState;
pub use state::NetworkProxyAuditMetadata;
pub use state::NetworkProxyConstraintError;
+1 -1
View File
@@ -124,7 +124,7 @@ async fn mitm_policy_rechecks_local_private_target_after_connect() {
let app_state = Arc::new(network_proxy_state_for_policy({
let mut network = NetworkProxyConfig::default();
network.set_allowed_domains(vec!["example.com".to_string()]);
network.allow_local_binding = false;
network.allow_local_binding = Some(false);
network
}));
let ctx = policy_ctx(
+7 -2
View File
@@ -683,7 +683,12 @@ mod tests {
let reloader = Arc::new(StaticReloader {
state: state.clone(),
});
NetworkProxyState::with_reloader_and_audit_metadata(state, reloader, metadata)
NetworkProxyState::with_reloader_and_audit_metadata(
state,
reloader,
metadata,
crate::LocalBindingPolicy::DefaultFalse,
)
}
fn is_rfc3339_utc_millis(timestamp: &str) -> bool {
@@ -1078,7 +1083,7 @@ mod tests {
let state = network_proxy_state_for_policy({
let mut network = NetworkProxyConfig::default();
network.set_allowed_domains(vec!["example.com".to_string()]);
network.allow_local_binding = false;
network.allow_local_binding = Some(false);
network
});
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
+59 -22
View File
@@ -225,6 +225,7 @@ impl NetworkProxyBuilder {
.set_blocked_request_observer(self.blocked_request_observer.clone())
.await;
let (current_cfg, _, executor_os) = state.current_cfg_with_brokerage_provenance().await?;
let current_cfg = state.local_binding_policy.resolved_config(&current_cfg);
#[cfg(target_os = "windows")]
let (current_cfg, runtime_settings, mut windows_ingress) = {
let current_cfg = config::NetworkProxyConfig {
@@ -454,7 +455,7 @@ impl NetworkProxyRuntimeSettings {
None
};
Ok(Self {
allow_local_binding: config.allow_local_binding,
allow_local_binding: config.allow_local_binding(),
allow_unix_sockets: if cfg!(target_os = "windows") {
Arc::default()
} else {
@@ -925,12 +926,16 @@ impl NetworkProxy {
}
/// Captures the static inputs needed to launch a matching executor-local proxy.
pub async fn remote_launch_config(&self) -> Result<crate::RemoteNetworkProxyLaunchConfig> {
pub async fn remote_launch_config(
&self,
local_binding_policy: crate::LocalBindingPolicy,
) -> Result<crate::RemoteNetworkProxyLaunchConfig> {
let (mut config, brokerage_created_default_allowlist, _) =
self.state.current_cfg_with_brokerage_provenance().await?;
// Proxy enablement and credential brokerage remain controller-owned.
let mut broker_only_config = config::NetworkProxyConfig {
enabled: config.enabled,
allow_local_binding: config.allow_local_binding.filter(|binding| !binding),
credential_providers: config.credential_providers.clone(),
credential_broker_openai_host: config.credential_broker_openai_host.clone(),
credential_broker_context: config.credential_broker_context.clone(),
@@ -960,9 +965,6 @@ impl NetworkProxy {
.execution_scope
.as_ref()
.and_then(|scope| scope.environment_policy.as_ref());
if let Some(policy) = environment_policy {
policy.apply_to(&mut config);
}
if config.credential_broker {
config.enabled &= !broker_only;
config.credential_broker = false;
@@ -971,10 +973,20 @@ impl NetworkProxy {
config.mitm = false;
}
}
config.allow_local_binding = Some(local_binding_policy.resolve(&config));
if let Some(policy) = environment_policy {
policy.apply_to(&mut config);
}
anyhow::ensure!(
environment_policy.is_none() || config.enabled,
"environment network policy requires an enabled executor proxy"
);
anyhow::ensure!(
!config.enabled
|| local_binding_policy != crate::LocalBindingPolicy::RequireTrue
|| config.allow_local_binding(),
"MXC cannot enforce allow_local_binding=false: native host-loopback access is bidirectional; use true or a different sandbox backend"
);
let proxy = crate::RemoteNetworkProxyConfig::from_effective_config(&config)?;
tracing::debug!(network_config = ?proxy, "resolved remote execution network configuration");
let (environment_id, execution_id) = self
@@ -996,8 +1008,11 @@ impl NetworkProxy {
})
}
/// Returns the policy decider with trusted execution attribution restored.
pub fn remote_policy_decider(&self) -> Option<Arc<dyn NetworkPolicyDecider>> {
/// Returns the policy decider with executor local-binding policy and trusted attribution.
pub fn remote_policy_decider(
&self,
allow_local_binding: bool,
) -> Option<Arc<dyn NetworkPolicyDecider>> {
let scope = self.execution_scope.as_ref()?;
self.state.for_execution_token(&scope.attribution_token)?;
let decider = Arc::clone(self.policy_decider.as_ref()?);
@@ -1019,7 +1034,9 @@ impl NetworkProxy {
crate::NetworkDecision::deny(crate::reasons::REASON_NOT_ALLOWED)
}
decision = async {
match state.host_blocked(&request.host, request.port).await {
match state.host_blocked_with_local_binding(
&request.host, request.port, Some(allow_local_binding),
).await {
// Controller approval alone cannot bypass attachment policy.
Ok(HostBlockDecision::Allowed) if !environment_policy_applies => {
NetworkDecision::Allow
@@ -1527,7 +1544,11 @@ impl NetworkProxy {
new_state.config.enable_socks5_udp == current_cfg.enable_socks5_udp,
"cannot update network.enable_socks5_udp on a running proxy"
);
let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config)?;
let config = self
.state
.local_binding_policy
.resolved_config(&new_state.config);
let settings = NetworkProxyRuntimeSettings::from_config(&config)?;
self.state.replace_config_state(new_state).await?;
let mut guard = self
.runtime_settings
@@ -1745,6 +1766,8 @@ impl Drop for NetworkProxyHandle {
#[cfg(test)]
mod tests {
use super::*;
use crate::LocalBindingPolicy::DefaultFalse;
use crate::LocalBindingPolicy::RequireTrue;
use crate::config::NetworkProxyConfig;
use crate::state::network_proxy_state_for_policy;
use pretty_assertions::assert_eq;
@@ -1838,7 +1861,7 @@ mod tests {
);
assert_eq!(proxy.current_cfg().await?, config);
let remote = proxy.remote_launch_config().await?;
let remote = proxy.remote_launch_config(DefaultFalse).await?;
assert_eq!(
(
remote.proxy.unix_sockets,
@@ -1862,7 +1885,7 @@ mod tests {
let captured = Arc::new(Mutex::new(None));
let captured_request = Arc::clone(&captured);
let mut config = NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
};
config.set_allowed_domains(vec!["controller.example".to_string()]);
@@ -1895,7 +1918,7 @@ mod tests {
Some(Arc::clone(&fallback_policy_decider)),
)?;
let decider = scoped
.remote_policy_decider()
.remote_policy_decider(/*allow_local_binding*/ true)
.expect("execution-scoped proxy should expose its policy decider");
let mut request = https_request("controller.example");
request.environment_id = Some("forged-environment".to_string());
@@ -1934,7 +1957,11 @@ mod tests {
owner_config.set_denied_domains(vec!["denied.example".to_string()]);
assert_eq!(
scoped.remote_launch_config().await?.proxy.domains,
scoped
.remote_launch_config(DefaultFalse)
.await?
.proxy
.domains,
owner_config.domains
);
let strict_scoped = proxy.for_execution(
@@ -1947,7 +1974,11 @@ mod tests {
)),
Some(fallback_policy_decider),
)?;
assert!(strict_scoped.remote_policy_decider().is_none());
assert!(
strict_scoped
.remote_policy_decider(/*allow_local_binding*/ true)
.is_none()
);
Ok(())
}
@@ -1956,7 +1987,7 @@ mod tests {
let decision_started = Arc::new(tokio::sync::Notify::new());
let decision_started_for_decider = Arc::clone(&decision_started);
let config = NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
};
let proxy = NetworkProxy::builder()
@@ -1979,7 +2010,7 @@ mod tests {
/*fallback_policy_decider*/ None,
)?;
let decider = scoped
.remote_policy_decider()
.remote_policy_decider(/*allow_local_binding*/ true)
.expect("execution-scoped proxy should expose its policy decider");
let request = https_request("pending.example");
let decision = tokio::spawn(async move { decider.decide(request).await });
@@ -2067,7 +2098,7 @@ mod tests {
enabled: true,
proxy_url: "http://127.0.0.1:1".to_string(),
socks_url: "http://127.0.0.1:2".to_string(),
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
}));
let third = NetworkProxy::builder()
@@ -2087,7 +2118,7 @@ mod tests {
enabled: true,
proxy_url: format!("http://{http_addr}"),
socks_url: format!("http://{socks_addr}"),
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
},
Default::default(),
@@ -2149,7 +2180,7 @@ mod tests {
enabled: true,
proxy_url: "http://127.0.0.1:43128".to_string(),
socks_url: "http://127.0.0.1:48081".to_string(),
allow_local_binding: true,
allow_local_binding: Some(true),
unix_sockets: Some(crate::config::NetworkUnixSocketPermissions {
entries: [
(
@@ -2335,6 +2366,7 @@ mod tests {
let mut config = NetworkProxyConfig {
dangerously_allow_plaintext_credential_injection: true,
dangerously_allow_all_unix_sockets: Some(false),
allow_local_binding: Some(false),
..NetworkProxyConfig::default()
};
config.set_allow_unix_sockets(Vec::new());
@@ -2364,7 +2396,7 @@ mod tests {
/*environment_policy*/ None,
/*fallback_policy_decider*/ None,
)?;
let launch = scoped.remote_launch_config().await?;
let launch = scoped.remote_launch_config(RequireTrue).await?;
let prepared = scoped.prepare_for_optional_environment(
HashMap::from([(
PROXY_ATTRIBUTION_TOKEN_ENV_KEY.to_string(),
@@ -2389,7 +2421,12 @@ mod tests {
)),
/*fallback_policy_decider*/ None,
)?;
assert!(owner_scoped.remote_launch_config().await.is_err());
assert!(
owner_scoped
.remote_launch_config(DefaultFalse)
.await
.is_err()
);
for (enabled, mode, allowed_domain, proxy_url) in [
(false, config::NetworkMode::Full, Some("*"), None),
(false, config::NetworkMode::Full, Some("example.com"), None),
@@ -2418,7 +2455,7 @@ mod tests {
.state(Arc::new(network_proxy_state_for_policy(config)))
.build()
.await?;
let launch = proxy.remote_launch_config().await?;
let launch = proxy.remote_launch_config(DefaultFalse).await?;
assert!(launch.proxy.enabled);
assert_eq!(launch.proxy.mode, mode);
}
+2 -2
View File
@@ -93,7 +93,7 @@ impl RemoteNetworkProxyConfig {
mode: config.mode,
domains: config.domains.clone(),
unix_sockets: config.unix_sockets.clone(),
allow_local_binding: config.allow_local_binding,
allow_local_binding: config.allow_local_binding(),
})
}
@@ -107,7 +107,7 @@ impl RemoteNetworkProxyConfig {
mode: self.mode,
domains: self.domains,
unix_sockets: self.unix_sockets,
allow_local_binding: self.allow_local_binding,
allow_local_binding: Some(self.allow_local_binding),
..NetworkProxyConfig::default()
}
}
@@ -2,6 +2,8 @@ use pretty_assertions::assert_eq;
use super::RemoteNetworkProxyConfig;
use super::RemoteNetworkProxyLaunchConfig;
use crate::LocalBindingPolicy::DefaultFalse;
use crate::LocalBindingPolicy::RequireTrue;
use crate::MitmHookConfig;
use crate::NetworkMode;
use crate::NetworkProxy;
@@ -37,6 +39,7 @@ fn optional_socket_policy_preserves_input_and_resolves_at_remote_boundary() {
allow_all.unwrap_or(false)
);
config.dangerously_allow_all_unix_sockets = Some(allow_all.unwrap_or(false));
config.allow_local_binding = Some(false);
assert_eq!(remote.into_network_proxy_config(), config);
}
}
@@ -56,7 +59,7 @@ async fn round_trip_preserves_supported_effective_settings() {
allow_upstream_proxy: false,
dangerously_allow_all_unix_sockets: Some(true),
mode: NetworkMode::Limited,
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
};
config.set_allowed_domains(vec!["example.com".into()]);
@@ -92,7 +95,7 @@ async fn round_trip_preserves_supported_effective_settings() {
);
assert_eq!(
proxy
.remote_launch_config()
.remote_launch_config(DefaultFalse)
.await
.unwrap()
.proxy
@@ -210,3 +213,36 @@ fn policy_decision_callback_timeout_round_trips() {
launch
);
}
#[tokio::test]
async fn local_binding_is_resolved_for_each_executor() -> anyhow::Result<()> {
for (binding, policy, expected) in [
(None, DefaultFalse, Some(false)),
(None, RequireTrue, Some(true)),
(Some(false), DefaultFalse, Some(false)),
(Some(false), RequireTrue, None),
(Some(true), DefaultFalse, Some(true)),
] {
let config = NetworkProxyConfig {
enabled: true,
allow_local_binding: binding,
..Default::default()
};
let mut state = crate::runtime::network_proxy_state_for_policy(config);
state.local_binding_policy = RequireTrue;
let proxy = NetworkProxy::builder()
.state(Arc::new(state))
.managed_by_codex(false)
.build()
.await?;
assert_eq!(
proxy
.remote_launch_config(policy)
.await
.ok()
.map(|launch| launch.proxy.allow_local_binding),
expected,
);
}
Ok(())
}
+66 -12
View File
@@ -232,8 +232,36 @@ where
}
}
/// How the executor resolves and validates local binding when managed networking is enabled.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LocalBindingPolicy {
/// Omission defaults to false; either explicit value is accepted.
#[default]
DefaultFalse,
/// Omission defaults to true; explicit false is rejected.
RequireTrue,
}
impl LocalBindingPolicy {
/// Resolves omission without changing the configured value.
pub fn resolve(self, config: &NetworkProxyConfig) -> bool {
config
.allow_local_binding
.unwrap_or(config.enabled && self == Self::RequireTrue)
}
pub(crate) fn resolved_config(self, config: &NetworkProxyConfig) -> NetworkProxyConfig {
NetworkProxyConfig {
allow_local_binding: Some(self.resolve(config)),
..config.clone()
}
}
}
pub struct NetworkProxyState {
state: Arc<RwLock<ConfigState>>,
/// Belongs to this proxy; config retains omission for launches on other executors.
pub(crate) local_binding_policy: LocalBindingPolicy,
reloader: Arc<dyn ConfigReloader>,
blocked_request_observer: Arc<RwLock<Option<Arc<dyn BlockedRequestObserver>>>>,
pub(crate) policy_audit_observer: Option<NetworkPolicyAuditObserver>,
@@ -271,6 +299,7 @@ impl Clone for NetworkProxyState {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
local_binding_policy: self.local_binding_policy,
reloader: self.reloader.clone(),
blocked_request_observer: self.blocked_request_observer.clone(),
policy_audit_observer: self.policy_audit_observer.clone(),
@@ -312,6 +341,7 @@ impl NetworkProxyState {
state,
Arc::new(StaticConfigReloader),
audit_metadata,
LocalBindingPolicy::DefaultFalse,
)
})
}
@@ -321,6 +351,7 @@ impl NetworkProxyState {
state,
reloader,
NetworkProxyAuditMetadata::default(),
LocalBindingPolicy::DefaultFalse,
)
}
@@ -333,6 +364,7 @@ impl NetworkProxyState {
state,
reloader,
NetworkProxyAuditMetadata::default(),
LocalBindingPolicy::DefaultFalse,
blocked_request_observer,
)
}
@@ -341,11 +373,13 @@ impl NetworkProxyState {
state: ConfigState,
reloader: Arc<dyn ConfigReloader>,
audit_metadata: NetworkProxyAuditMetadata,
local_binding_policy: LocalBindingPolicy,
) -> Self {
Self::with_reloader_and_audit_metadata_and_blocked_observer(
state,
reloader,
audit_metadata,
local_binding_policy,
/*blocked_request_observer*/ None,
)
}
@@ -354,13 +388,15 @@ impl NetworkProxyState {
state: ConfigState,
reloader: Arc<dyn ConfigReloader>,
audit_metadata: NetworkProxyAuditMetadata,
local_binding_policy: LocalBindingPolicy,
blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
) -> Self {
let credential_broker = CredentialBroker::new(state.config.credential_broker);
credential_broker.configure(&state.config);
credential_broker.configure(&local_binding_policy.resolved_config(&state.config));
Self {
credential_broker,
state: Arc::new(RwLock::new(state)),
local_binding_policy,
reloader,
blocked_request_observer: Arc::new(RwLock::new(blocked_request_observer)),
policy_audit_observer: None,
@@ -616,7 +652,8 @@ impl NetworkProxyState {
Ok(mut new_state) => {
{
let mut guard = self.state.write().await;
self.credential_broker.configure(&new_state.config);
self.credential_broker
.configure(&self.local_binding_policy.resolved_config(&new_state.config));
// Log policy diffs without dumping potentially sensitive config values.
log_policy_changes(&guard.config, &new_state.config);
new_state.blocked = guard.blocked.clone();
@@ -637,7 +674,8 @@ impl NetworkProxyState {
pub async fn replace_config_state(&self, mut new_state: ConfigState) -> Result<()> {
self.reload_if_needed().await?;
let mut guard = self.state.write().await;
self.credential_broker.configure(&new_state.config);
self.credential_broker
.configure(&self.local_binding_policy.resolved_config(&new_state.config));
log_policy_changes(&guard.config, &new_state.config);
new_state.blocked = guard.blocked.clone();
new_state.blocked_total = guard.blocked_total;
@@ -647,6 +685,16 @@ impl NetworkProxyState {
}
pub async fn host_blocked(&self, host: &str, port: u16) -> Result<HostBlockDecision> {
self.host_blocked_with_local_binding(host, port, /*allow_local_binding*/ None)
.await
}
pub(crate) async fn host_blocked_with_local_binding(
&self,
host: &str,
port: u16,
allow_local_binding: Option<bool>,
) -> Result<HostBlockDecision> {
self.reload_if_needed().await?;
let host = match Host::parse(host) {
Ok(host) => host,
@@ -658,7 +706,10 @@ impl NetworkProxyState {
(
guard.deny_set.clone(),
guard.allow_set.clone(),
guard.config.allow_local_binding,
// Keep explicit controller restrictions even if the executor launched with true.
allow_local_binding
.unwrap_or_else(|| self.local_binding_policy.resolve(&guard.config))
&& guard.config.allow_local_binding != Some(false),
allowed_domains,
)
};
@@ -848,7 +899,7 @@ impl NetworkProxyState {
pub async fn allow_local_binding(&self) -> Result<bool> {
self.reload_if_needed().await?;
let guard = self.state.read().await;
Ok(guard.config.allow_local_binding)
Ok(self.local_binding_policy.resolve(&guard.config))
}
pub async fn network_mode(&self) -> Result<NetworkMode> {
@@ -867,7 +918,8 @@ impl NetworkProxyState {
(candidate, guard.constraints.clone())
};
validate_policy_against_constraints(&candidate, &constraints)
let resolved = self.local_binding_policy.resolved_config(&candidate);
validate_policy_against_constraints(&resolved, &constraints)
.map_err(NetworkProxyConstraintError::into_anyhow)
.context("network.mode constrained by managed config")?;
@@ -969,7 +1021,8 @@ impl NetworkProxyState {
normalize_host,
);
validate_policy_against_constraints(&candidate, &constraints)
let resolved = self.local_binding_policy.resolved_config(&candidate);
validate_policy_against_constraints(&resolved, &constraints)
.map_err(NetworkProxyConstraintError::into_anyhow)
.with_context(|| format!("{constraint_field} constrained by managed config"))?;
@@ -1001,7 +1054,8 @@ impl NetworkProxyState {
Some(mut new_state) => {
{
let mut guard = self.state.write().await;
self.credential_broker.configure(&new_state.config);
self.credential_broker
.configure(&self.local_binding_policy.resolved_config(&new_state.config));
log_policy_changes(&guard.config, &new_state.config);
new_state.blocked = guard.blocked.clone();
new_state.blocked_total = guard.blocked_total;
@@ -1733,7 +1787,7 @@ mod tests {
#[tokio::test]
async fn host_blocked_requires_exact_scoped_ipv6_allowlist_match() {
let state = network_proxy_state_for_policy(NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..network_settings(&["fe80::1%eth0"], &[])
});
@@ -1756,7 +1810,7 @@ mod tests {
#[tokio::test]
async fn host_blocked_denies_scoped_ipv6_literal_before_local_binding() {
let state = network_proxy_state_for_policy(NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..network_settings(&["*"], &["fd00::1"])
});
@@ -1772,7 +1826,7 @@ mod tests {
#[tokio::test]
async fn host_blocked_requires_exact_scoped_ipv6_denylist_match() {
let state = network_proxy_state_for_policy(NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..network_settings(&["*"], &["fd00::1%eth0"])
});
@@ -2049,7 +2103,7 @@ mod tests {
let config = NetworkProxyConfig {
enabled: true,
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
};
+1 -1
View File
@@ -228,7 +228,7 @@ pub fn validate_policy_against_constraints(
)?;
if let Some(allow_local_binding) = constraints.allow_local_binding {
validate(config.allow_local_binding, move |candidate| {
validate(config.allow_local_binding(), move |candidate| {
if *candidate && !allow_local_binding {
Err(invalid_value(
"network.allow_local_binding",
@@ -41,7 +41,7 @@ async fn connector_reaches_native_hostname_over_ipv4_and_ipv6() {
let target = listener.local_addr().expect("local addr");
let connector = TargetCheckedTcpConnector::new(Arc::new(network_proxy_state_for_policy(
NetworkProxyConfig {
allow_local_binding: true,
allow_local_binding: Some(true),
..NetworkProxyConfig::default()
},
)));
@@ -297,7 +297,7 @@ async fn build_proxy(
socks_url: format!("socks5://{socks_addr}"),
enable_socks5,
enable_socks5_udp: false,
allow_local_binding: true,
allow_local_binding: Some(true),
mode: NetworkMode::Full,
..NetworkProxyConfig::default()
};