mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
@@ -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}"
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1819,6 +1819,9 @@ mod tests {
|
||||
.expect("build remote network proxy config");
|
||||
let state = NetworkProxyState::from_remote_launch_config(
|
||||
RemoteNetworkProxyLaunchConfig::new(proxy_config),
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(
|
||||
std::env::consts::OS,
|
||||
)),
|
||||
)
|
||||
.expect("build network proxy state");
|
||||
let proxy = NetworkProxy::builder()
|
||||
|
||||
@@ -348,8 +348,11 @@ async fn prepare_managed_network(
|
||||
let Some(network_proxy) = network_proxy.cloned() else {
|
||||
return Ok((env, managed_network.cloned(), None, None));
|
||||
};
|
||||
let mut state = NetworkProxyState::from_remote_launch_config(network_proxy)
|
||||
.map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?;
|
||||
let mut state = NetworkProxyState::from_remote_launch_config(
|
||||
network_proxy,
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?;
|
||||
if let Some(observer) = network_policy_audit_observer {
|
||||
state.set_policy_audit_observer(observer);
|
||||
}
|
||||
|
||||
@@ -140,8 +140,14 @@ for family in (socket.AF_NETLINK, getattr(socket, 'AF_VSOCK', 40)):
|
||||
if allow_path {
|
||||
config.set_allow_unix_sockets(vec![socket_path.to_string_lossy().into_owned()]);
|
||||
}
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default())
|
||||
.expect("valid managed network configuration");
|
||||
let state = build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(
|
||||
std::env::consts::OS,
|
||||
)),
|
||||
)
|
||||
.expect("valid managed network configuration");
|
||||
let network = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
state.clone(),
|
||||
|
||||
@@ -172,7 +172,12 @@ impl TestProxy {
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
config.set_allowed_domains(vec!["127.0.0.1".to_string()]);
|
||||
let config_state = build_config_state(config, NetworkProxyConstraints::default()).unwrap();
|
||||
let config_state = build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap();
|
||||
let state = Arc::new(NetworkProxyState::with_reloader(
|
||||
config_state.clone(),
|
||||
Arc::new(StaticReloader(config_state)),
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::path::Path;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
use crate::NetworkProxyExecutorOs;
|
||||
use crate::mitm_hook::MitmHookConfig;
|
||||
use crate::policy::normalize_host;
|
||||
|
||||
@@ -106,6 +107,9 @@ pub enum NetworkUnixSocketPermission {
|
||||
Deny,
|
||||
}
|
||||
|
||||
/// Socket policy keys are preserved across controller and executor hosts.
|
||||
/// Allow entries must be NUL-free and absolute according to the executor OS;
|
||||
/// Deny entries are retained without path validation or expansion.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub struct NetworkUnixSocketPermissions {
|
||||
#[serde(flatten)]
|
||||
@@ -487,21 +491,28 @@ impl ValidatedUnixSocketPath {
|
||||
if let Some(path) = UnixStyleAbsolutePath::parse(socket_path) {
|
||||
return Ok(Self::UnixStyleAbsolute(path));
|
||||
}
|
||||
|
||||
bail!("expected an absolute path, got {socket_path:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()> {
|
||||
pub(crate) fn validate_unix_socket_allowlist_paths(
|
||||
cfg: &NetworkProxyConfig,
|
||||
executor_os: NetworkProxyExecutorOs,
|
||||
) -> Result<()> {
|
||||
for (index, socket_path) in cfg.allow_unix_sockets().iter().enumerate() {
|
||||
ValidatedUnixSocketPath::parse(socket_path)
|
||||
.with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?;
|
||||
anyhow::ensure!(
|
||||
executor_os.socket_path_is_absolute(socket_path) && !socket_path.contains('\0'),
|
||||
"invalid network.allow_unix_sockets[{index}]: expected a NUL-free absolute path for {executor_os:?}, got {socket_path:?}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
|
||||
validate_unix_socket_allowlist_paths(cfg)?;
|
||||
pub fn resolve_runtime(
|
||||
cfg: &NetworkProxyConfig,
|
||||
executor_os: NetworkProxyExecutorOs,
|
||||
) -> Result<RuntimeConfig> {
|
||||
validate_unix_socket_allowlist_paths(cfg, executor_os)?;
|
||||
|
||||
let http_addr = resolve_addr(&cfg.proxy_url, /*default_port*/ 3128)
|
||||
.with_context(|| format!("invalid network.proxy_url: {}", cfg.proxy_url))?;
|
||||
@@ -517,7 +528,10 @@ 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)?;
|
||||
let runtime = resolve_runtime(
|
||||
cfg,
|
||||
NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
if runtime.http_addr.port() == 0 {
|
||||
bail!("network.proxy_url must use a fixed non-zero port for managed proxy provisioning");
|
||||
}
|
||||
@@ -1085,29 +1099,77 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() {
|
||||
let cfg = settings_with_unix_sockets(&["relative.sock"]);
|
||||
|
||||
let err = match resolve_runtime(&cfg) {
|
||||
Ok(runtime) => panic!(
|
||||
"relative allow_unix_sockets should fail, but resolve_runtime succeeded: {:?}",
|
||||
runtime.http_addr
|
||||
),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert!(
|
||||
err.to_string().contains("network.allow_unix_sockets[0]"),
|
||||
"error should point at the invalid allow_unix_sockets entry: {err:#}"
|
||||
);
|
||||
fn resolve_runtime_validates_allow_unix_sockets_for_executor_os() {
|
||||
for (path, unix_absolute, windows_absolute) in [
|
||||
("relative.sock", false, false),
|
||||
("~/example.sock", false, false),
|
||||
("/tmp/example.sock", true, true),
|
||||
(r"C:\example.sock", false, true),
|
||||
("C:/example.sock", false, true),
|
||||
(r"\\server\share\example.sock", false, true),
|
||||
(r"\\?\C:\example.sock", false, true),
|
||||
(r"\\.\pipe\example", false, true),
|
||||
(r"C:example.sock", false, false),
|
||||
(r"\example.sock", false, false),
|
||||
(r"\\server", false, false),
|
||||
("/tmp/\0example.sock", false, false),
|
||||
("C:\\example\0.sock", false, false),
|
||||
] {
|
||||
let cfg = settings_with_unix_sockets(&[path]);
|
||||
for (executor_os, accepted) in [
|
||||
(NetworkProxyExecutorOs::Linux, unix_absolute),
|
||||
(NetworkProxyExecutorOs::Macos, unix_absolute),
|
||||
(NetworkProxyExecutorOs::Windows, windows_absolute),
|
||||
(
|
||||
NetworkProxyExecutorOs::Unknown,
|
||||
unix_absolute || windows_absolute,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_runtime(&cfg, executor_os).is_ok(),
|
||||
accepted,
|
||||
"{executor_os:?}: {path:?}"
|
||||
);
|
||||
}
|
||||
// Each platform's CI compares the portable implementation to native
|
||||
// Core acceptance, with the deliberate shared NUL rejection.
|
||||
assert_eq!(
|
||||
resolve_runtime(
|
||||
&cfg,
|
||||
NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS))
|
||||
)
|
||||
.is_ok(),
|
||||
(Path::new(path).is_absolute() || path.starts_with('/')) && !path.contains('\0'),
|
||||
"native parity: {path:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() {
|
||||
let cfg = settings_with_unix_sockets(&["/private/tmp/example.sock"]);
|
||||
let mut cfg = settings_with_unix_sockets(&[
|
||||
"/private/tmp/example.sock",
|
||||
"/tmp/../example.sock",
|
||||
r"/tmp/name\part.sock",
|
||||
]);
|
||||
for path in ["relative.sock", "~/example.sock", r"C:\example.sock", "\0"] {
|
||||
cfg.unix_sockets
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.entries
|
||||
.insert(path.to_string(), NetworkUnixSocketPermission::Deny);
|
||||
}
|
||||
|
||||
assert!(
|
||||
resolve_runtime(&cfg).is_ok(),
|
||||
"unix-style absolute allow_unix_sockets entry should be accepted"
|
||||
);
|
||||
for executor_os in [
|
||||
NetworkProxyExecutorOs::Linux,
|
||||
NetworkProxyExecutorOs::Macos,
|
||||
NetworkProxyExecutorOs::Windows,
|
||||
NetworkProxyExecutorOs::Unknown,
|
||||
] {
|
||||
assert!(
|
||||
resolve_runtime(&cfg, executor_os).is_ok(),
|
||||
"unix-style absolute allow_unix_sockets entry should be accepted"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Executor path semantics for policy validation, independent of the controller OS.
|
||||
//! Socket support and native path normalization remain executor runtime concerns.
|
||||
|
||||
/// OS of the executor whose network policy is being validated.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum NetworkProxyExecutorOs {
|
||||
Linux,
|
||||
Macos,
|
||||
Windows,
|
||||
/// Legacy executors may omit their OS. Accept absolute syntax supported by
|
||||
/// either platform here; the executor validates against its own OS at launch.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl NetworkProxyExecutorOs {
|
||||
/// Uses executor metadata, never the OS of the process doing the validation.
|
||||
pub fn from_platform_os(platform_os: Option<&str>) -> Self {
|
||||
match platform_os {
|
||||
Some("linux") => Self::Linux,
|
||||
Some("macos") => Self::Macos,
|
||||
Some("windows") => Self::Windows,
|
||||
Some(_) | None => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn socket_path_is_absolute(self, path: &str) -> bool {
|
||||
// Core also accepts Unix-style absolute paths on Windows, for portability.
|
||||
path.starts_with('/')
|
||||
|| match self {
|
||||
Self::Linux | Self::Macos => false,
|
||||
Self::Windows | Self::Unknown => windows_path_is_absolute(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match std::path's Windows absolute-path syntax without consulting the host.
|
||||
// Non-drive prefixes have an implicit root, including device and verbatim paths.
|
||||
fn windows_path_is_absolute(path: &str) -> bool {
|
||||
let bytes = path.as_bytes();
|
||||
if matches!(bytes, [drive, b':', b'\\' | b'/', ..] if drive.is_ascii_alphabetic()) {
|
||||
return true;
|
||||
}
|
||||
let [b'\\' | b'/', b'\\' | b'/', rest @ ..] = bytes else {
|
||||
return false;
|
||||
};
|
||||
if path.starts_with(r"\\?\") || matches!(rest, [b'.', b'\\' | b'/', ..]) {
|
||||
return true;
|
||||
}
|
||||
let mut components = rest.split(|byte| matches!(byte, b'\\' | b'/'));
|
||||
matches!(
|
||||
(components.next(), components.next()),
|
||||
(Some(server), Some(share)) if !server.is_empty() && !share.is_empty()
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod connect_policy;
|
||||
mod connection_lifecycle;
|
||||
mod credential_broker;
|
||||
mod environment_policy;
|
||||
mod executor_os;
|
||||
mod http_proxy;
|
||||
mod mitm;
|
||||
mod mitm_hook;
|
||||
@@ -60,6 +61,7 @@ pub use credential_broker::credential_broker_provider_context_env_keys;
|
||||
pub use credential_broker::credential_broker_provider_sources_allowed;
|
||||
pub use credential_broker::is_credential_broker_provider_env_key;
|
||||
pub use environment_policy::EnvironmentNetworkPolicy;
|
||||
pub use executor_os::NetworkProxyExecutorOs;
|
||||
pub use mitm_hook::InjectedHeaderConfig;
|
||||
pub use mitm_hook::MitmHookActionsConfig;
|
||||
pub use mitm_hook::MitmHookBodyConfig;
|
||||
|
||||
@@ -674,7 +674,12 @@ mod tests {
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
let config = network;
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap();
|
||||
let state = build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap();
|
||||
let reloader = Arc::new(StaticReloader {
|
||||
state: state.clone(),
|
||||
});
|
||||
|
||||
@@ -224,7 +224,7 @@ impl NetworkProxyBuilder {
|
||||
state
|
||||
.set_blocked_request_observer(self.blocked_request_observer.clone())
|
||||
.await;
|
||||
let current_cfg = state.current_cfg().await?;
|
||||
let (current_cfg, _, executor_os) = state.current_cfg_with_brokerage_provenance().await?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let (current_cfg, runtime_settings, mut windows_ingress) = {
|
||||
let current_cfg = config::NetworkProxyConfig {
|
||||
@@ -238,7 +238,7 @@ impl NetworkProxyBuilder {
|
||||
let (requested_http_addr, requested_socks_addr, reserved_listeners) = if self
|
||||
.managed_by_codex
|
||||
{
|
||||
let runtime = config::resolve_runtime(¤t_cfg)?;
|
||||
let runtime = config::resolve_runtime(¤t_cfg, executor_os)?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let shared_ingress_addrs = if self.managed_proxy_routing
|
||||
== ManagedProxyRouting::SharedIngress
|
||||
@@ -273,7 +273,7 @@ impl NetworkProxyBuilder {
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let runtime = config::resolve_runtime(¤t_cfg)?;
|
||||
let runtime = config::resolve_runtime(¤t_cfg, executor_os)?;
|
||||
(
|
||||
self.http_addr.unwrap_or(runtime.http_addr),
|
||||
self.socks_addr.unwrap_or(runtime.socks_addr),
|
||||
@@ -926,7 +926,7 @@ impl NetworkProxy {
|
||||
|
||||
/// Captures the static inputs needed to launch a matching executor-local proxy.
|
||||
pub async fn remote_launch_config(&self) -> Result<crate::RemoteNetworkProxyLaunchConfig> {
|
||||
let (mut config, brokerage_created_default_allowlist) =
|
||||
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 {
|
||||
@@ -1817,7 +1817,11 @@ mod tests {
|
||||
format!("{expected_bind_host}:8081").parse::<SocketAddr>()?,
|
||||
)
|
||||
);
|
||||
let replacement = crate::state::build_config_state(config.clone(), Default::default())?;
|
||||
let replacement = crate::state::build_config_state(
|
||||
config.clone(),
|
||||
Default::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
proxy.replace_config_state(replacement).await?;
|
||||
let expected_unix_sockets = if cfg!(target_os = "windows") {
|
||||
Vec::new()
|
||||
@@ -2087,6 +2091,7 @@ mod tests {
|
||||
..NetworkProxyConfig::default()
|
||||
},
|
||||
Default::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.expect("replacement config state");
|
||||
proxy
|
||||
@@ -2169,7 +2174,11 @@ mod tests {
|
||||
|
||||
for allow_all in [false, true] {
|
||||
config.dangerously_allow_all_unix_sockets = Some(allow_all);
|
||||
let replacement = crate::state::build_config_state(config.clone(), Default::default())?;
|
||||
let replacement = crate::state::build_config_state(
|
||||
config.clone(),
|
||||
Default::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
proxy.replace_config_state(replacement).await?;
|
||||
let prepared = proxy
|
||||
.prepare_for_optional_environment(HashMap::new(), /*environment_id*/ None)?;
|
||||
|
||||
@@ -4,9 +4,12 @@ use super::RemoteNetworkProxyConfig;
|
||||
use super::RemoteNetworkProxyLaunchConfig;
|
||||
use crate::MitmHookConfig;
|
||||
use crate::NetworkMode;
|
||||
use crate::NetworkProxy;
|
||||
use crate::NetworkProxyAuditMetadata;
|
||||
use crate::NetworkProxyConfig;
|
||||
use crate::NetworkProxyExecutorOs;
|
||||
use crate::NetworkProxyState;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn optional_socket_policy_preserves_input_and_resolves_at_remote_boundary() {
|
||||
@@ -38,27 +41,65 @@ fn optional_socket_policy_preserves_input_and_resolves_at_remote_boundary() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_preserves_supported_effective_settings() {
|
||||
let mut config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
enable_socks5: false,
|
||||
enable_socks5_udp: false,
|
||||
allow_upstream_proxy: false,
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
mode: NetworkMode::Limited,
|
||||
allow_local_binding: true,
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
config.set_allowed_domains(vec!["example.com".into()]);
|
||||
config.set_denied_domains(vec!["blocked.example.com".into()]);
|
||||
config.set_allow_unix_sockets(vec!["/var/run/example.sock".into()]);
|
||||
#[tokio::test]
|
||||
async fn round_trip_preserves_supported_effective_settings() {
|
||||
for (executor_os, socket) in [
|
||||
(NetworkProxyExecutorOs::Linux, "/var/run/example.sock"),
|
||||
(NetworkProxyExecutorOs::Macos, "/var/run/example.sock"),
|
||||
(NetworkProxyExecutorOs::Windows, r"C:\example.sock"),
|
||||
(NetworkProxyExecutorOs::Unknown, r"C:\example.sock"),
|
||||
] {
|
||||
let mut config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
enable_socks5: false,
|
||||
enable_socks5_udp: false,
|
||||
allow_upstream_proxy: false,
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
mode: NetworkMode::Limited,
|
||||
allow_local_binding: true,
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
config.set_allowed_domains(vec!["example.com".into()]);
|
||||
config.set_denied_domains(vec!["blocked.example.com".into()]);
|
||||
config.set_allow_unix_sockets(vec![socket.into()]);
|
||||
|
||||
let remote =
|
||||
RemoteNetworkProxyConfig::from_effective_config(&config).expect("supported remote config");
|
||||
let round_trip = remote.into_network_proxy_config();
|
||||
let remote = RemoteNetworkProxyConfig::from_effective_config(&config)
|
||||
.expect("supported remote config");
|
||||
let round_trip = remote.clone().into_network_proxy_config();
|
||||
|
||||
assert_eq!(round_trip, config);
|
||||
assert_eq!(round_trip, config);
|
||||
|
||||
let state = Arc::new(
|
||||
NetworkProxyState::from_remote_launch_config(
|
||||
RemoteNetworkProxyLaunchConfig::new(remote),
|
||||
executor_os,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
// Policy edits and carrier construction must retain the executor's OS even
|
||||
// when the controller itself cannot interpret the socket as a native path.
|
||||
state.add_allowed_domain("added.example.com").await.unwrap();
|
||||
let proxy = NetworkProxy::builder()
|
||||
.state(state)
|
||||
.managed_by_codex(/*managed_by_codex*/ false)
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
config.upsert_domain_permission(
|
||||
"added.example.com".into(),
|
||||
crate::NetworkDomainPermission::Allow,
|
||||
crate::normalize_host,
|
||||
);
|
||||
assert_eq!(
|
||||
proxy
|
||||
.remote_launch_config()
|
||||
.await
|
||||
.unwrap()
|
||||
.proxy
|
||||
.into_network_proxy_config(),
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -136,13 +177,16 @@ fn launch_config_materializes_audit_and_execution_attribution() {
|
||||
model: Some("model-1".to_string()),
|
||||
..NetworkProxyAuditMetadata::default()
|
||||
};
|
||||
let state = NetworkProxyState::from_remote_launch_config(RemoteNetworkProxyLaunchConfig {
|
||||
proxy,
|
||||
audit_metadata: audit_metadata.clone(),
|
||||
environment_id: Some("remote".to_string()),
|
||||
execution_id: Some("execution-1".to_string()),
|
||||
policy_decision_timeout_ms: None,
|
||||
})
|
||||
let state = NetworkProxyState::from_remote_launch_config(
|
||||
RemoteNetworkProxyLaunchConfig {
|
||||
proxy,
|
||||
audit_metadata: audit_metadata.clone(),
|
||||
environment_id: Some("remote".to_string()),
|
||||
execution_id: Some("execution-1".to_string()),
|
||||
policy_decision_timeout_ms: None,
|
||||
},
|
||||
NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.expect("remote launch state");
|
||||
|
||||
assert_eq!(state.audit_metadata(), &audit_metadata);
|
||||
|
||||
@@ -168,6 +168,8 @@ fn blocked_request_violation_log_line(entry: &BlockedRequest) -> String {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigState {
|
||||
/// Preserve the target when policy edits rebuild state on a controller.
|
||||
pub(crate) executor_os: crate::NetworkProxyExecutorOs,
|
||||
pub config: NetworkProxyConfig,
|
||||
pub(crate) brokerage_created_default_allowlist: bool,
|
||||
pub allow_set: GlobSet,
|
||||
@@ -285,8 +287,10 @@ impl Clone for NetworkProxyState {
|
||||
|
||||
impl NetworkProxyState {
|
||||
/// Builds runtime state for one executor-local proxy launch.
|
||||
/// The launching executor supplies its own OS; the wire policy cannot override it.
|
||||
pub fn from_remote_launch_config(
|
||||
launch: crate::RemoteNetworkProxyLaunchConfig,
|
||||
executor_os: crate::NetworkProxyExecutorOs,
|
||||
) -> Result<Self> {
|
||||
let crate::RemoteNetworkProxyLaunchConfig {
|
||||
proxy,
|
||||
@@ -300,7 +304,7 @@ impl NetworkProxyState {
|
||||
"executor-local network proxy launch requires an enabled proxy"
|
||||
);
|
||||
let config = proxy.into_network_proxy_config();
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default())?;
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default(), executor_os)?;
|
||||
Ok(Self {
|
||||
environment_id: environment_id.map(Into::into),
|
||||
execution_id: execution_id.map(Into::into),
|
||||
@@ -571,7 +575,7 @@ impl NetworkProxyState {
|
||||
pub async fn current_cfg(&self) -> Result<NetworkProxyConfig> {
|
||||
self.current_cfg_with_brokerage_provenance()
|
||||
.await
|
||||
.map(|(config, _)| config)
|
||||
.map(|(config, _, _)| config)
|
||||
}
|
||||
|
||||
pub(crate) fn credential_broker_config_revision(&self) -> u64 {
|
||||
@@ -580,7 +584,7 @@ impl NetworkProxyState {
|
||||
|
||||
pub(crate) async fn current_cfg_with_brokerage_provenance(
|
||||
&self,
|
||||
) -> Result<(NetworkProxyConfig, bool)> {
|
||||
) -> Result<(NetworkProxyConfig, bool, crate::NetworkProxyExecutorOs)> {
|
||||
// Callers treat `NetworkProxyState` as a live view of policy. We reload-on-demand so edits to
|
||||
// `config.toml` (including Codex-managed writes) take effect without a restart.
|
||||
self.reload_if_needed().await?;
|
||||
@@ -588,6 +592,7 @@ impl NetworkProxyState {
|
||||
Ok((
|
||||
guard.config.clone(),
|
||||
guard.brokerage_created_default_allowlist,
|
||||
guard.executor_os,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -934,13 +939,14 @@ impl NetworkProxyState {
|
||||
|
||||
loop {
|
||||
self.reload_if_needed().await?;
|
||||
let (previous_cfg, constraints, blocked, blocked_total) = {
|
||||
let (previous_cfg, constraints, blocked, blocked_total, executor_os) = {
|
||||
let guard = self.state.read().await;
|
||||
(
|
||||
guard.config.clone(),
|
||||
guard.constraints.clone(),
|
||||
guard.blocked.clone(),
|
||||
guard.blocked_total,
|
||||
guard.executor_os,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -967,13 +973,17 @@ impl NetworkProxyState {
|
||||
.map_err(NetworkProxyConstraintError::into_anyhow)
|
||||
.with_context(|| format!("{constraint_field} constrained by managed config"))?;
|
||||
|
||||
let mut new_state = build_config_state(candidate.clone(), constraints.clone())
|
||||
.with_context(|| format!("failed to compile updated network {list_name}"))?;
|
||||
let mut new_state =
|
||||
build_config_state(candidate.clone(), constraints.clone(), executor_os)
|
||||
.with_context(|| format!("failed to compile updated network {list_name}"))?;
|
||||
new_state.blocked = blocked;
|
||||
new_state.blocked_total = blocked_total;
|
||||
|
||||
let mut guard = self.state.write().await;
|
||||
if guard.constraints != constraints || guard.config != previous_cfg {
|
||||
if guard.constraints != constraints
|
||||
|| guard.config != previous_cfg
|
||||
|| guard.executor_os != executor_os
|
||||
{
|
||||
drop(guard);
|
||||
continue;
|
||||
}
|
||||
@@ -1178,6 +1188,7 @@ pub(crate) fn network_proxy_state_for_policy(
|
||||
config.set_allowed_domains(vec!["*".to_string()]);
|
||||
}
|
||||
let state = ConfigState {
|
||||
executor_os: crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
allow_set: crate::policy::compile_allowlist_globset(
|
||||
&config.allowed_domains().unwrap_or_default(),
|
||||
)
|
||||
@@ -1282,12 +1293,20 @@ mod tests {
|
||||
enabled: true,
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
let initial_state =
|
||||
build_config_state(config.clone(), NetworkProxyConstraints::default()).unwrap();
|
||||
let initial_state = build_config_state(
|
||||
config.clone(),
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap();
|
||||
let mut reloaded_config = config;
|
||||
reloaded_config.set_credential_broker_enabled(/*enabled*/ true);
|
||||
let reloaded_state =
|
||||
build_config_state(reloaded_config, NetworkProxyConstraints::default()).unwrap();
|
||||
let reloaded_state = build_config_state(
|
||||
reloaded_config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap();
|
||||
let state = NetworkProxyState::with_reloader(
|
||||
initial_state,
|
||||
Arc::new(StaticReloader {
|
||||
@@ -1314,6 +1333,7 @@ mod tests {
|
||||
enabled: Some(false),
|
||||
..NetworkProxyConstraints::default()
|
||||
},
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.expect("managed-disabled credential broker should fail open");
|
||||
let state = NetworkProxyState::with_reloader(config_state, Arc::new(NoopReloader));
|
||||
@@ -1361,8 +1381,12 @@ mod tests {
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
config.set_credential_broker_enabled(/*enabled*/ true);
|
||||
let config_state = build_config_state(config, NetworkProxyConstraints::default())
|
||||
.expect("valid credential-broker configuration");
|
||||
let config_state = build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.expect("valid credential-broker configuration");
|
||||
assert_eq!(config_state.brokerage_created_default_allowlist, !enabled);
|
||||
let state = NetworkProxyState::with_reloader(config_state, Arc::new(NoopReloader));
|
||||
|
||||
@@ -1448,7 +1472,12 @@ mod tests {
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
let state = NetworkProxyState::with_reloader(
|
||||
build_config_state(config, constraints).unwrap(),
|
||||
build_config_state(
|
||||
config,
|
||||
constraints,
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap(),
|
||||
Arc::new(NoopReloader),
|
||||
);
|
||||
|
||||
@@ -1475,7 +1504,12 @@ mod tests {
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
let state = NetworkProxyState::with_reloader(
|
||||
build_config_state(config, constraints).unwrap(),
|
||||
build_config_state(
|
||||
config,
|
||||
constraints,
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap(),
|
||||
Arc::new(NoopReloader),
|
||||
);
|
||||
|
||||
@@ -1500,7 +1534,12 @@ mod tests {
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
let state = NetworkProxyState::with_reloader(
|
||||
build_config_state(config, constraints).unwrap(),
|
||||
build_config_state(
|
||||
config,
|
||||
constraints,
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap(),
|
||||
Arc::new(NoopReloader),
|
||||
);
|
||||
|
||||
@@ -2168,7 +2207,14 @@ mod tests {
|
||||
let mut config = network_settings(&["*"], &[]);
|
||||
config.enabled = true;
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_ok());
|
||||
assert!(
|
||||
build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2176,7 +2222,14 @@ mod tests {
|
||||
let mut config = network_settings(&["[*]"], &[]);
|
||||
config.enabled = true;
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_ok());
|
||||
assert!(
|
||||
build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2184,7 +2237,14 @@ mod tests {
|
||||
let mut config = network_settings(&["example.com"], &["*"]);
|
||||
config.enabled = true;
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err());
|
||||
assert!(
|
||||
build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS))
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2192,7 +2252,14 @@ mod tests {
|
||||
let mut config = network_settings(&["example.com"], &["[*]"]);
|
||||
config.enabled = true;
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err());
|
||||
assert!(
|
||||
build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS))
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -902,7 +902,12 @@ mod tests {
|
||||
fn state_for_settings(network: NetworkProxyConfig) -> Arc<NetworkProxyState> {
|
||||
let config = network;
|
||||
let _mitm_config_state_guard = config.mitm.then(|| MITM_CONFIG_STATE_LOCK.lock().unwrap());
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap();
|
||||
let state = build_config_state(
|
||||
config,
|
||||
NetworkProxyConstraints::default(),
|
||||
crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)
|
||||
.unwrap();
|
||||
let reloader = Arc::new(StaticReloader {
|
||||
state: state.clone(),
|
||||
});
|
||||
|
||||
@@ -57,9 +57,12 @@ pub struct PartialNetworkProxyConfig {
|
||||
pub mitm_hooks: Option<Vec<MitmHookConfig>>,
|
||||
}
|
||||
|
||||
/// Compiles policy using the supplied executor OS, which may differ from this host.
|
||||
/// Socket paths are validated without native normalization or filesystem access.
|
||||
pub fn build_config_state(
|
||||
mut config: NetworkProxyConfig,
|
||||
constraints: NetworkProxyConstraints,
|
||||
executor_os: crate::NetworkProxyExecutorOs,
|
||||
) -> anyhow::Result<ConfigState> {
|
||||
if constraints.enabled == Some(false) {
|
||||
config.credential_broker = false;
|
||||
@@ -74,7 +77,7 @@ pub fn build_config_state(
|
||||
if brokerage_created_default_allowlist {
|
||||
config.set_allowed_domains(vec!["*".to_string()]);
|
||||
}
|
||||
crate::config::validate_unix_socket_allowlist_paths(&config)?;
|
||||
crate::config::validate_unix_socket_allowlist_paths(&config, executor_os)?;
|
||||
anyhow::ensure!(
|
||||
!config.credential_broker || config.mitm,
|
||||
"network.credential_broker requires network.mitm = true"
|
||||
@@ -94,6 +97,7 @@ pub fn build_config_state(
|
||||
None
|
||||
};
|
||||
Ok(ConfigState {
|
||||
executor_os,
|
||||
config,
|
||||
brokerage_created_default_allowlist,
|
||||
allow_set,
|
||||
|
||||
@@ -302,7 +302,11 @@ async fn build_proxy(
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
config.set_allowed_domains(vec![allowed_domain.to_string()]);
|
||||
let config_state = build_config_state(config, Default::default())?;
|
||||
let config_state = build_config_state(
|
||||
config,
|
||||
Default::default(),
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
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);
|
||||
|
||||
@@ -541,6 +541,7 @@ async fn linux_unix_socket_grant_uses_effective_managed_policy() -> anyhow::Resu
|
||||
..Default::default()
|
||||
},
|
||||
NetworkProxyConstraints::default(),
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
let network = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
|
||||
@@ -816,7 +816,11 @@ async fn prepared_managed_network_context_takes_precedence_over_live_proxy_socke
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
let state = build_config_state(network_config, NetworkProxyConstraints::default())?;
|
||||
let state = build_config_state(
|
||||
network_config,
|
||||
NetworkProxyConstraints::default(),
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
let network_proxy = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
state,
|
||||
@@ -1524,7 +1528,11 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a
|
||||
..Default::default()
|
||||
};
|
||||
network_config.set_allow_unix_sockets(vec![network_socket.to_string()]);
|
||||
let state = build_config_state(network_config, NetworkProxyConstraints::default())?;
|
||||
let state = build_config_state(
|
||||
network_config,
|
||||
NetworkProxyConstraints::default(),
|
||||
codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)),
|
||||
)?;
|
||||
let network_proxy = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
state,
|
||||
|
||||
Reference in New Issue
Block a user