Validate network socket policies using the executor OS (#46302)

## Why

A controller and its executor can run different operating systems. Validating socket paths against the controller's OS can reject absolute paths that are valid on the executor, such as Windows paths on a Linux controller.

## What changed

- Thread `NetworkProxyExecutorOs` through network policy validation, proxy construction, and policy updates.
- Require allowed socket paths to be NUL-free and absolute for the executor OS, while preserving deny entries unchanged.
- Accept either Unix or Windows absolute syntax when executor metadata omits the OS, then validate against the executor's own OS at launch.
- Keep native path normalization and socket support checks at execution time.

## Testing

Add coverage for cross-platform absolute path syntax, invalid allow entries, preserved deny entries, and remote policy round trips that retain executor semantics through domain edits and proxy construction.

GitOrigin-RevId: 1ebc09cbce7138f60ec5fd62875591a3df52d067
This commit is contained in:
Sean Huang
2026-09-17 20:59:55 +00:00
committed by copyberry
parent 8f73cdee45
commit ea218f5cd8
23 changed files with 519 additions and 134 deletions
+14 -10
View File
@@ -16,6 +16,7 @@ use codex_features::FeaturesToml;
use codex_network_proxy::EnvironmentNetworkPolicy;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyConstraints;
use codex_network_proxy::NetworkProxyExecutorOs;
use codex_network_proxy::build_config_state;
use codex_protocol::models::PermissionProfile;
@@ -29,8 +30,13 @@ pub struct EnvironmentNetworkConfigError;
/// Drops listener addresses, which the executor chooses for each command, before
/// validating an environment's selected network configuration. Use the returned
/// value when deciding whether a policy is present and when preparing it.
///
/// Socket policy validation uses the supplied executor OS: Allow keys must be
/// NUL-free and absolute for that OS; Deny keys are preserved unchanged.
/// Path normalization and socket support are determined by the executor.
pub fn project_environment_profile_network(
network: Option<NetworkToml>,
executor_os: NetworkProxyExecutorOs,
) -> Result<Option<NetworkToml>, EnvironmentNetworkConfigError> {
let Some(mut network) = network else {
return Ok(None);
@@ -46,20 +52,16 @@ pub fn project_environment_profile_network(
allow_local_binding: network.allow_local_binding,
..Default::default()
};
let invalid_socket = network.unix_sockets.as_ref().is_some_and(|sockets| {
sockets
.entries
.keys()
.any(|path| !path.starts_with('/') || path.contains('\0'))
});
if network != supported || invalid_socket {
if network != supported {
return Err(EnvironmentNetworkConfigError);
}
// MITM and credential settings have been excluded. This only validates
// patterns and lexical socket paths; it does not discover executor files.
// Reuse shared, host-independent policy validation.
// With MITM and credentials excluded, this checks patterns and allowed
// socket paths without discovering executor files.
build_config_state(
network.to_network_proxy_config(),
NetworkProxyConstraints::default(),
executor_os,
)
.map_err(|_| EnvironmentNetworkConfigError)?;
Ok(Some(network))
@@ -67,9 +69,11 @@ pub fn project_environment_profile_network(
/// Validates the owner policy for its final permission profile using the native
/// environment resolver. Execution also applies controller and saved exec rules.
/// This checks the supplied executor's path syntax; socket support is checked at execution.
pub fn validate_environment_network_policy(
policy: &EnvironmentNetworkPolicy,
permission_profile: &PermissionProfile,
executor_os: NetworkProxyExecutorOs,
) -> Result<(), EnvironmentNetworkConfigError> {
NetworkProxySpec::for_environment(
/*controller*/ None,
@@ -77,7 +81,7 @@ pub fn validate_environment_network_policy(
permission_profile,
&Policy::empty(),
)
.and_then(|spec| spec.build_config_state_for_spec().map(|_| ()))
.and_then(|spec| spec.build_config_state_for_spec(executor_os).map(|_| ()))
.map_err(|_| EnvironmentNetworkConfigError)
}
@@ -38,10 +38,17 @@ fn environment_policy_presence_keeps_selected_and_managed_denials() {
}
#[test]
fn attachment_projection_preserves_policy_and_drops_listener_addresses() {
for raw in [
"",
r#"
fn attachment_projection_uses_executor_os_and_drops_listener_addresses() {
for executor_os in [
NetworkProxyExecutorOs::Linux,
NetworkProxyExecutorOs::Macos,
NetworkProxyExecutorOs::Windows,
NetworkProxyExecutorOs::Unknown,
] {
for (raw, windows_only) in [
("", false),
(
r#"
enabled = false
allow_upstream_proxy = false
dangerously_allow_all_unix_sockets = false
@@ -52,23 +59,69 @@ allow_local_binding = false
[unix_sockets]
'/tmp/allowed.sock' = 'allow'
'/tmp/blocked.sock' = 'deny'
'relative.sock' = 'deny'
'~/blocked.sock' = 'deny'
'C:\blocked.sock' = 'deny'
"#,
] {
let expected: NetworkToml = toml::from_str(raw).unwrap();
let network = NetworkToml {
proxy_url: Some("not a listener URL".to_string()),
socks_url: Some("socks5://127.0.0.1:1080".to_string()),
..expected.clone()
};
false,
),
(
r#"[unix_sockets]
'C:\allowed.sock' = 'allow'
'\\server\share\allowed.sock' = 'allow'
"#,
true,
),
] {
let expected: NetworkToml = toml::from_str(raw).unwrap();
let accepted = !windows_only
|| matches!(
executor_os,
NetworkProxyExecutorOs::Windows | NetworkProxyExecutorOs::Unknown
);
assert_eq!(
build_config_state(
expected.to_network_proxy_config(),
NetworkProxyConstraints::default(),
executor_os,
)
.is_ok(),
accepted,
"native validation: {executor_os:?}, {raw}"
);
assert_eq!(
validate_environment_network_policy(
&EnvironmentNetworkPolicy::from_config(
&expected.to_network_proxy_config(),
/*managed_allowed_domains_only*/ false,
),
&PermissionProfile::read_only(),
executor_os,
)
.is_ok(),
accepted,
"environment validation: {executor_os:?}, {raw}"
);
let network = NetworkToml {
proxy_url: Some("not a listener URL".to_string()),
socks_url: Some("socks5://127.0.0.1:1080".to_string()),
..expected.clone()
};
assert_eq!(
project_environment_profile_network(Some(network), executor_os),
if accepted {
Ok(Some(expected))
} else {
Err(EnvironmentNetworkConfigError)
},
"projection: {executor_os:?}, {raw}"
);
}
assert_eq!(
project_environment_profile_network(Some(network)),
Ok(Some(expected))
project_environment_profile_network(/*network*/ None, executor_os),
Ok(None)
);
}
assert_eq!(
project_environment_profile_network(/*network*/ None),
Ok(None)
);
}
#[test]
@@ -86,12 +139,13 @@ path_prefixes = ['/']
action = ['redact']
"#,
"[unix_sockets]\n'relative.sock' = 'allow'",
"[unix_sockets]\n'relative.sock' = 'deny'",
"[unix_sockets]\n'~/allowed.sock' = 'allow'",
"[unix_sockets]\n\"/tmp/\\u0000allowed.sock\" = 'allow'",
"[domains]\n'[' = 'allow'",
] {
let network: NetworkToml = toml::from_str(raw).unwrap();
assert_eq!(
project_environment_profile_network(Some(network)),
project_environment_profile_network(Some(network), NetworkProxyExecutorOs::Unknown),
Err(EnvironmentNetworkConfigError),
"{raw}"
);
+17 -7
View File
@@ -12,6 +12,7 @@ use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyConstraints;
use codex_network_proxy::NetworkProxyExecutorOs;
use codex_network_proxy::NetworkProxyHandle;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::build_config_state;
@@ -196,7 +197,10 @@ impl NetworkProxySpec {
enable_network_approval_flow: bool,
audit_metadata: NetworkProxyAuditMetadata,
) -> std::io::Result<StartedNetworkProxy> {
let state = self.build_state_with_audit_metadata(audit_metadata)?;
let state = self.build_state_with_audit_metadata(
audit_metadata,
NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
)?;
let mut builder = NetworkProxy::builder()
.state(Arc::new(state))
.managed_proxy_routing(managed_proxy_routing);
@@ -337,7 +341,9 @@ impl NetworkProxySpec {
&self,
started_proxy: &StartedNetworkProxy,
) -> std::io::Result<()> {
let state = self.build_config_state_for_spec()?;
let state = self.build_config_state_for_spec(NetworkProxyExecutorOs::from_platform_os(
Some(std::env::consts::OS),
))?;
started_proxy
.proxy()
.replace_config_state(state)
@@ -350,8 +356,9 @@ impl NetworkProxySpec {
pub(crate) fn build_state_with_audit_metadata(
&self,
audit_metadata: NetworkProxyAuditMetadata,
executor_os: NetworkProxyExecutorOs,
) -> std::io::Result<NetworkProxyState> {
let state = self.build_config_state_for_spec()?;
let state = self.build_config_state_for_spec(executor_os)?;
let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone()));
Ok(NetworkProxyState::with_reloader_and_audit_metadata(
state,
@@ -360,10 +367,13 @@ impl NetworkProxySpec {
))
}
pub(super) fn build_config_state_for_spec(&self) -> std::io::Result<ConfigState> {
build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| {
std::io::Error::other(format!("failed to build network proxy state: {err}"))
})
pub(super) fn build_config_state_for_spec(
&self,
executor_os: NetworkProxyExecutorOs,
) -> std::io::Result<ConfigState> {
build_config_state(self.config.clone(), self.constraints.clone(), executor_os).map_err(
|err| std::io::Error::other(format!("failed to build network proxy state: {err}")),
)
}
fn apply_requirements(
@@ -84,7 +84,11 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
Some(expected),
"{name}"
);
let state = Arc::new(controller.build_state_with_audit_metadata(Default::default())?);
let state =
Arc::new(controller.build_state_with_audit_metadata(
Default::default(),
NetworkProxyExecutorOs::Linux,
)?);
let proxy = NetworkProxy::builder()
.state(Arc::clone(&state))
.managed_by_codex(false)
@@ -138,7 +142,9 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun
&profile,
)?;
proxy
.replace_config_state(restricted.build_config_state_for_spec()?)
.replace_config_state(
restricted.build_config_state_for_spec(NetworkProxyExecutorOs::Linux)?,
)
.await?;
assert!(
!scoped
@@ -169,7 +175,7 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() {
};
let state = spec
.build_state_with_audit_metadata(metadata.clone())
.build_state_with_audit_metadata(metadata.clone(), NetworkProxyExecutorOs::Linux)
.expect("state should build");
assert_eq!(state.audit_metadata(), &metadata);
}
@@ -252,11 +258,15 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials()
let owner_policy =
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false);
assert_eq!(
validate_environment_network_policy(&owner_policy, &profile),
validate_environment_network_policy(&owner_policy, &profile, NetworkProxyExecutorOs::Linux),
Ok(())
);
assert_eq!(
validate_environment_network_policy(&owner_policy, &PermissionProfile::Disabled),
validate_environment_network_policy(
&owner_policy,
&PermissionProfile::Disabled,
NetworkProxyExecutorOs::Linux
),
Err(EnvironmentNetworkConfigError)
);
let compose = NetworkProxySpec::for_environment;
@@ -324,14 +334,22 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials()
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false);
assert!(compose(Some(&spec), &wildcard_policy, &profile, &empty).is_err());
assert_eq!(
validate_environment_network_policy(&wildcard_policy, &profile),
validate_environment_network_policy(
&wildcard_policy,
&profile,
NetworkProxyExecutorOs::Linux
),
Err(EnvironmentNetworkConfigError)
);
owner.set_allowed_domains(vec!["[".to_string()]);
let malformed_policy =
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false);
assert_eq!(
validate_environment_network_policy(&malformed_policy, &profile),
validate_environment_network_policy(
&malformed_policy,
&profile,
NetworkProxyExecutorOs::Linux
),
Err(EnvironmentNetworkConfigError)
);
}
@@ -521,6 +521,9 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
.replace_config_state(codex_network_proxy::build_config_state(
config,
codex_network_proxy::NetworkProxyConstraints::default(),
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(
std::env::consts::OS,
)),
)?)
.await?;
assert_eq!(
@@ -569,6 +572,9 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
.replace_config_state(codex_network_proxy::build_config_state(
config,
codex_network_proxy::NetworkProxyConstraints::default(),
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(
std::env::consts::OS,
)),
)?)
.await?;
fs::write(dir.path().join("finish-startup"), "").await?;
@@ -18,6 +18,7 @@ use codex_network_proxy::NetworkPolicyDecider;
use codex_network_proxy::NetworkPolicyRequest;
use codex_network_proxy::NetworkProtocol;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyExecutorOs;
use codex_network_proxy::NetworkRequestCancellation;
use codex_network_proxy::NetworkRequestCancellationReason;
use codex_network_proxy::NetworkRequestDisconnect;
@@ -1151,9 +1152,16 @@ pub(crate) async fn begin_network_approval(
})?
} else {
// This carrier never listens: the executor starts the real per-command proxy.
let executor_os = NetworkProxyExecutorOs::from_platform_os(
environments
.turn_environments()
.find(|environment| environment.selection.environment_id == environment_id)
.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,
)
.map_err(|error| {
ToolError::Rejected(format!(
@@ -76,8 +76,11 @@ pub(super) async fn test_credential_broker_network_proxy() -> anyhow::Result<Net
async fn test_network_proxy_with_config(
config: NetworkProxyConfig,
) -> anyhow::Result<NetworkProxy> {
let state =
codex_network_proxy::build_config_state(config, NetworkProxyConstraints::default())?;
let state = codex_network_proxy::build_config_state(
config,
NetworkProxyConstraints::default(),
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
)?;
NetworkProxy::builder()
.state(Arc::new(NetworkProxyState::with_reloader(
state,